Compare commits

..

14 Commits

17 changed files with 396 additions and 95 deletions

4
api.go
View File

@ -97,6 +97,10 @@ func (s *Sorare) Debug() *Sorare {
return s return s
} }
func (s *Sorare) SetApiKey(apiKey string) {
s.Client.SetApiKey(apiKey)
}
func (s *Sorare) SetJWTToken(token graphql.JwtToken, audience string) { func (s *Sorare) SetJWTToken(token graphql.JwtToken, audience string) {
s.Client.SetJWTToken(token, audience) s.Client.SetJWTToken(token, audience)
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"os" "os"
"time" "time"
@ -21,7 +20,7 @@ func main() {
if audience == "" { if audience == "" {
log.Fatal("No audience provided") log.Fatal("No audience provided")
} }
api := sorare.New().Debug() api := sorare.New()
api.SetJWTToken( api.SetJWTToken(
graphql.JwtToken{ graphql.JwtToken{
Token: token, Token: token,
@ -32,18 +31,57 @@ func main() {
ctx := context.Background() ctx := context.Background()
//cu, err := api.Country.Get(ctx, graphql.SlugParams{Slug: "fr"}) p, err := api.Football.Player.Get(ctx, graphql.SlugParams{Slug: "joshua-kimmich"})
params := make(map[string][]string)
params["0c7d5f17-7c01-4427-90b8-a736a44ea632"] = []string{"joshua-kimmich", "leon-goretzka"}
params["0d641a1d-c64a-40c9-85b0-7ddeca428abf"] = []string{"joshua-kimmich", "leon-goretzka"}
cu, err := api.Football.GamesScores.Get(
ctx,
params,
)
if err != nil { if err != nil {
panic(err) panic(err)
} }
for _, game := range cu { log.Printf("%+v", p)
log.Println(game.Game.Id.Value + " " + game.Player.Slug + " " + fmt.Sprintf("%s", game.Score))
} // si, err := api.ShopItems.Get(ctx, sorare.ShopItemsParams{
// Types: []sorare.ShopItemType{sorare.ShopItemTypeLevelUp},
// UnlockedOnly: true,
// })
// if err != nil {
// panic(err)
// }
// for _, item := range si {
// log.Printf(
// "%s %d %d %s %s",
// item.TypeName,
// item.ShopItemInterface.MyBalance,
// item.ShopItemInterface.Price,
// item.LevelUpShopItem.Rarity,
// time.Until(item.ShopItemInterface.MyLimitResetAt).String(),
// )
// }
// cu, err := api.Users.CurrentUser.Get(ctx, graphql.EmptyParams{})
// if err != nil {
// panic(err)
// }
// log.Printf("%d", cu.CoinBalance)
// cards, err := api.Users.FootballCards("gigiz22").Get(ctx, football.CardsParams{
// Rarities: []types.Rarity{types.RarityLimited},
// })
// if err != nil {
// panic(err)
// }
// sort.Slice(cards, func(i, j int) bool {
// return cards[i].XpNeededForNextGrade-cards[i].Xp > cards[j].XpNeededForNextGrade-cards[j].Xp
// })
// for _, card := range cards {
// if !card.InSeasonEligible {
// continue
// }
// log.Printf(
// "%60s %2d %6d %6d %6d (%d/%d)",
// card.Token.Name,
// card.Grade,
// card.Xp-card.XpNeededForCurrentGrade,
// card.Xp,
// card.XpNeededForNextGrade-card.Xp,
// card.LevelUpAppliedCount,
// card.MaxLevelUpAppliedCount,
// )
// }
} }

View File

@ -9,13 +9,19 @@ import (
) )
type Card struct { type Card struct {
Token tokens.Token `graphql:"token"`
Power decimal.Decimal `graphql:"power"` Power decimal.Decimal `graphql:"power"`
PowerMalusAfterTransfer decimal.Decimal `graphql:"powerMalusAfterTransfer"` PowerMalusAfterTransfer decimal.Decimal `graphql:"powerMalusAfterTransfer"`
RivalsPower decimal.Decimal `graphql:"rivalsPower"` RivalsPower decimal.Decimal `graphql:"rivalsPower"`
Grade int `graphql:"grade"` Grade int `graphql:"grade"`
GradeAfterTransfer int `graphql:"gradeAfterTransfer"` GradeAfterTransfer int `graphql:"gradeAfterTransfer"`
Xp int `graphql:"xp"` Xp int `graphql:"xp"`
Token tokens.Token `graphql:"token"` XpAfterTransfer int `graphql:"xpAfterTransfer"`
XpNeededForNextGrade int `graphql:"xpNeededForNextGrade"`
XpNeededForCurrentGrade int `graphql:"xpNeededForCurrentGrade"`
InSeasonEligible bool `graphql:"inSeasonEligible"`
LevelUpAppliedCount int `graphql:"levelUpAppliedCount"`
MaxLevelUpAppliedCount int `graphql:"maxLevelUpAppliedCount"`
} }
type CardsParams struct { type CardsParams struct {

View File

@ -26,7 +26,8 @@ type Football struct {
Players *graphql.Query[[]Player, graphql.SlugsParams] Players *graphql.Query[[]Player, graphql.SlugsParams]
Season *graphql.Query[Season, SeasonParams] Season *graphql.Query[Season, SeasonParams]
So5 *So5 So5 *So5
Rivals *Rivals
} }
func NewFootball(c *graphql.Client) *Football { func NewFootball(c *graphql.Client) *Football {
@ -109,13 +110,14 @@ func NewFootball(c *graphql.Client) *Football {
c, c,
"players", "players",
[]string{"football"}, []string{"football"},
), ).WithMaxPageSize(100),
Season: graphql.NewQuery[Season, SeasonParams]( Season: graphql.NewQuery[Season, SeasonParams](
c, c,
"season", "season",
[]string{"football"}, []string{"football"},
), ),
So5: NewSo5(c), So5: NewSo5(c),
Rivals: NewRivals(c),
} }
} }

View File

@ -30,6 +30,46 @@ type Suspension struct {
} `graphql:"competition"` } `graphql:"competition"`
} }
type CardSupply struct {
Season struct {
StartYear int `graphql:"startYear"`
} `graphql:"season"`
Limited int `graphql:"limited"`
Rare int `graphql:"rare"`
SuperRare int `graphql:"superRare"`
Unique int `graphql:"unique"`
}
type MembershipStats struct {
Season struct {
StartYear int `graphql:"startYear"`
} `graphql:"season"`
Appearances int `graphql:"appearances"`
Goals int `graphql:"goals"`
Assists int `graphql:"assists"`
YellowCards int `graphql:"yellowCards"`
RedCards int `graphql:"redCards"`
MinutesPlayed int `graphql:"minutesPlayed"`
SubstituteIn int `graphql:"substituteIn"`
SubstituteOut int `graphql:"substituteOut"`
}
type Membership struct {
Id graphql.Id `graphql:"id"`
StartDate time.Time `graphql:"startDate"`
EndDate *time.Time `graphql:"endDate"`
MembershipTeam struct {
TypeName string `graphql:"__typename"`
Club struct {
Slug string `graphql:"slug"`
} `graphql:"... on Club"`
NationalTeam struct {
Slug string `graphql:"slug"`
} `graphql:"... on NationalTeam"`
} `graphql:"membershipTeam"`
//Stats []MembershipStats `graphql:"stats"`
}
type Player struct { type Player struct {
ActiveClub struct { ActiveClub struct {
Slug string `graphql:"slug"` Slug string `graphql:"slug"`
@ -48,7 +88,9 @@ type Player struct {
BestFoot string `graphql:"bestFoot"` BestFoot string `graphql:"bestFoot"`
BirthDate time.Time `graphql:"birthDate"` BirthDate time.Time `graphql:"birthDate"`
CardPositions []types.Position `graphql:"cardPositions"` CardPositions []types.Position `graphql:"cardPositions"`
CardSupply []CardSupply `graphql:"cardSupply"`
Id graphql.Id `graphql:"id"` Id graphql.Id `graphql:"id"`
Memberships []Membership `graphql:"memberships"`
Slug string `graphql:"slug"` Slug string `graphql:"slug"`
DisplayName string `graphql:"displayName"` DisplayName string `graphql:"displayName"`
FirstName string `graphql:"firstName"` FirstName string `graphql:"firstName"`

17
football/rivals.go Normal file
View File

@ -0,0 +1,17 @@
package football
import "git.lehouerou.net/laurent/sorare/graphql"
type Rivals struct {
c *graphql.Client
PastGames *graphql.PaginatedQuery[RivalsGame, graphql.EmptyParams]
}
func NewRivals(c *graphql.Client) *Rivals {
return &Rivals{
c: c,
PastGames: graphql.NewPaginatedQuery[RivalsGame, graphql.EmptyParams](c, "pastGamesPaginated", []string{"football", "rivals"}).WithOverrideComplexity(107),
}
}

32
football/rivals_game.go Normal file
View File

@ -0,0 +1,32 @@
package football
import (
"git.lehouerou.net/laurent/sorare/graphql"
"git.lehouerou.net/laurent/sorare/types"
)
type RivalsDraftableObject struct {
Id graphql.Id `graphql:"id"`
CapValue float64 `graphql:"capValue"`
Player struct {
Slug string `graphql:"slug"`
ActiveClub struct {
Slug string `graphql:"slug"`
}
ActiveNationalTeam struct {
Slug string `graphql:"slug"`
}
} `graphql:"player"`
Position types.Position `graphql:"position"`
}
type RivalsGame struct {
Id graphql.Id `graphql:"id"`
Slug string `graphql:"slug"`
Cap int `graphql:"cap"`
DraftablePlayers []RivalsDraftableObject `graphql:"draftablePlayers"`
FormationKnown bool `graphql:"formationKnown"`
Game struct {
Id graphql.Id `graphql:"id"`
} `graphql:"game"`
}

View File

@ -22,28 +22,28 @@ func NewSo5(c *graphql.Client) *So5 {
So5Fixture: graphql.NewQuery[So5Fixture, So5FixtureParams]( So5Fixture: graphql.NewQuery[So5Fixture, So5FixtureParams](
c, c,
"so5Fixture", "so5Fixture",
[]string{"football", "so5"}, []string{"so5"},
), ),
So5Fixtures: graphql.NewPaginatedQuery[So5Fixture, So5FixturesParams]( So5Fixtures: graphql.NewPaginatedQuery[So5Fixture, So5FixturesParams](
c, c,
"so5Fixtures", "so5Fixtures",
[]string{"football", "so5"}, []string{"so5"},
), ),
So5Score: graphql.NewQuery[So5Score, graphql.IdParams]( So5Score: graphql.NewQuery[So5Score, graphql.IdParams](
c, c,
"so5Score", "so5Score",
[]string{"football", "so5"}, []string{"so5"},
), ),
So5Leaderboard: graphql.NewQuery[So5Leaderboard, graphql.SlugParams]( So5Leaderboard: graphql.NewQuery[So5Leaderboard, graphql.SlugParams](
c, c,
"so5Leaderboard", "so5Leaderboard",
[]string{"football", "so5"}, []string{"so5"},
), ),
UpcomingLeaderboards: graphql.NewQuery[[]So5Leaderboard, UpcomingLeaderboardsParams]( UpcomingLeaderboards: graphql.NewQuery[[]So5Leaderboard, UpcomingLeaderboardsParams](
c, c,
"upcomingLeaderboards", "upcomingLeaderboards",
[]string{"football", "so5"}, []string{"so5"},
), ),
} }
} }

12
go.mod
View File

@ -9,17 +9,17 @@ require (
github.com/llehouerou/go-graphql-client v0.9.7 github.com/llehouerou/go-graphql-client v0.9.7
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.32.0 github.com/rs/zerolog v1.32.0
github.com/shopspring/decimal v1.3.1 github.com/shopspring/decimal v1.4.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.21.0 golang.org/x/crypto v0.22.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
) )
require ( require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.19.0 // indirect
nhooyr.io/websocket v1.8.10 // indirect nhooyr.io/websocket v1.8.11 // indirect
) )

12
go.sum
View File

@ -22,6 +22,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -30,19 +32,29 @@ github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View File

@ -57,10 +57,10 @@ type JwtToken struct {
type SignIn struct { type SignIn struct {
CurrentUser struct { CurrentUser struct {
Slug string Slug string
JwtToken JwtToken `graphql:"jwtToken(aud: $aud)"`
} `graphql:"currentUser"` } `graphql:"currentUser"`
OtpSessionChallenge string OtpSessionChallenge string
JwtToken JwtToken `graphql:"jwtToken(aud: $aud)"`
Errors []struct { Errors []struct {
Message string Message string
} `graphql:"errors"` } `graphql:"errors"`
@ -116,12 +116,12 @@ func (c *Client) getNewToken(
} }
if len(res.Errors) == 0 { if len(res.Errors) == 0 {
if res.CurrentUser.JwtToken.Token == "" { if res.JwtToken.Token == "" {
return JwtToken{}, errors.New( return JwtToken{}, errors.New(
"no errors & no jwt token returned on email/password mutation", "no errors & no jwt token returned on email/password mutation",
) )
} }
return res.CurrentUser.JwtToken, nil return res.JwtToken, nil
} }
if res.Errors[0].Message == "invalid" { if res.Errors[0].Message == "invalid" {
@ -146,10 +146,10 @@ func (c *Client) getNewToken(
} }
if len(resOtp.Errors) == 0 { if len(resOtp.Errors) == 0 {
if resOtp.CurrentUser.JwtToken.Token == "" { if resOtp.JwtToken.Token == "" {
return JwtToken{}, errors.New("no errors & no jwt token returned on otp mutation") return JwtToken{}, errors.New("no errors & no jwt token returned on otp mutation")
} }
return resOtp.CurrentUser.JwtToken, nil return resOtp.JwtToken, nil
} }
return JwtToken{}, errors.Errorf("validating otp: %s", resOtp.Errors[0].Message) return JwtToken{}, errors.Errorf("validating otp: %s", resOtp.Errors[0].Message)
} }

View File

@ -3,6 +3,7 @@ package graphql
import ( import (
"context" "context"
"net/http" "net/http"
"reflect"
"sync" "sync"
"time" "time"
@ -22,9 +23,11 @@ type Client struct {
httpClient *http.Client httpClient *http.Client
gql *graphql.Client gql *graphql.Client
rl *rate.Limiter rl *rate.Limiter
apirl *rate.Limiter
lock *sync.Mutex lock *sync.Mutex
authenticated bool authenticated bool
token JwtToken token JwtToken
apiKey string
} }
func NewClient(httpclient *http.Client, baseUrl string) *Client { func NewClient(httpclient *http.Client, baseUrl string) *Client {
@ -32,6 +35,7 @@ func NewClient(httpclient *http.Client, baseUrl string) *Client {
httpClient: httpclient, httpClient: httpclient,
gql: graphql.NewClient(baseUrl, httpclient), gql: graphql.NewClient(baseUrl, httpclient),
rl: rate.NewLimiter(rate.Every(rateLimitPeriod), rateLimitBurst), rl: rate.NewLimiter(rate.Every(rateLimitPeriod), rateLimitBurst),
apirl: rate.NewLimiter(rate.Every(rateLimitPeriod/10), rateLimitBurst*10),
lock: &sync.Mutex{}, lock: &sync.Mutex{},
authenticated: false, authenticated: false,
} }
@ -50,12 +54,18 @@ func (c *Client) SetJWTToken(token JwtToken, audience string) {
c.authenticated = true c.authenticated = true
} }
func (c *Client) MaxComplexity() int { func (c *Client) SetApiKey(apiKey string) {
if c.authenticated { c.gql = c.gql.WithRequestModifier(func(request *http.Request) {
request.Header.Set("APIKEY", apiKey)
})
c.apiKey = apiKey
}
func (c *Client) MaxQueryComplexity() int {
if c.apiKey != "" || c.authenticated {
return MaxAuthenticatedQueryComplexity return MaxAuthenticatedQueryComplexity
} else {
return MaxAnonymousQueryComplexity
} }
return MaxAnonymousQueryComplexity
} }
func (c *Client) ConstructRawQuery( func (c *Client) ConstructRawQuery(
@ -71,9 +81,16 @@ func (c *Client) Query(
variables interface{}, variables interface{},
options ...graphql.Option, options ...graphql.Option,
) error { ) error {
err := c.rl.Wait(ctx) if c.apiKey != "" {
if err != nil { err := c.apirl.Wait(ctx)
return errors.Wrap(err, "waiting for rate limit") if err != nil {
return errors.Wrap(err, "waiting for rate limit")
}
} else {
err := c.rl.Wait(ctx)
if err != nil {
return errors.Wrap(err, "waiting for rate limit")
}
} }
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
@ -86,9 +103,16 @@ func (c *Client) QueryRaw(
variables interface{}, variables interface{},
options ...graphql.Option, options ...graphql.Option,
) ([]byte, error) { ) ([]byte, error) {
err := c.rl.Wait(ctx) if c.apiKey != "" {
if err != nil { err := c.apirl.Wait(ctx)
return nil, errors.Wrap(err, "waiting for rate limit") if err != nil {
return nil, errors.Wrap(err, "waiting for rate limit")
}
} else {
err := c.rl.Wait(ctx)
if err != nil {
return nil, errors.Wrap(err, "waiting for rate limit")
}
} }
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
@ -101,11 +125,67 @@ func (c *Client) Mutate(
variables interface{}, variables interface{},
options ...graphql.Option, options ...graphql.Option,
) error { ) error {
err := c.rl.Wait(ctx) if c.apiKey != "" {
if err != nil { err := c.apirl.Wait(ctx)
return errors.Wrap(err, "waiting for rate limit") if err != nil {
return errors.Wrap(err, "waiting for rate limit")
}
} else {
err := c.rl.Wait(ctx)
if err != nil {
return errors.Wrap(err, "waiting for rate limit")
}
} }
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
return c.gql.Mutate(ctx, q, variables, options...) return c.gql.Mutate(ctx, q, variables, options...)
} }
func GetTypeComplexity(t reflect.Type) int {
var checkStruct func(t reflect.Type, complexity *int)
checkStruct = func(t reflect.Type, complexity *int) {
if t.Kind() != reflect.Struct {
return
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("graphql")
if field.Type.Kind() == reflect.Struct {
*complexity++
if len(tag) > 6 && tag[:6] == "... on" {
*complexity--
}
checkStruct(field.Type, complexity)
continue
}
if field.Type.Kind() == reflect.Slice {
if isSimpleType(field.Type.Elem()) {
*complexity++
} else {
*complexity++
tmpcomplexity := 0
checkStruct(field.Type.Elem(), &tmpcomplexity)
*complexity += tmpcomplexity * 10
}
continue
}
if tag != "" && tag != "__typename" {
*complexity++
}
}
}
complexity := 0
checkStruct(t, &complexity)
return complexity
}
func isSimpleType(t reflect.Type) bool {
switch t.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String:
return true
default:
return false
}
}

View File

@ -3,8 +3,6 @@ package graphql
import ( import (
"context" "context"
"reflect" "reflect"
"github.com/rs/zerolog/log"
) )
type PaginatedQuery[ResultType any, Params any] struct { type PaginatedQuery[ResultType any, Params any] struct {
@ -14,6 +12,8 @@ type PaginatedQuery[ResultType any, Params any] struct {
additionalPayloadParams map[string]interface{} additionalPayloadParams map[string]interface{}
additionalQueryParams map[string]interface{} additionalQueryParams map[string]interface{}
overrideComplexity int
} }
func NewPaginatedQuery[ResultType any, Params any]( func NewPaginatedQuery[ResultType any, Params any](
@ -27,6 +27,7 @@ func NewPaginatedQuery[ResultType any, Params any](
containerLayers: containerLayers, containerLayers: containerLayers,
additionalPayloadParams: make(map[string]interface{}), additionalPayloadParams: make(map[string]interface{}),
additionalQueryParams: make(map[string]interface{}), additionalQueryParams: make(map[string]interface{}),
overrideComplexity: 0,
} }
} }
@ -46,6 +47,11 @@ func (pq *PaginatedQuery[ResultType, Params]) WithQueryParam(
return pq return pq
} }
func (pq *PaginatedQuery[ResultType, Params]) WithOverrideComplexity(complexity int) *PaginatedQuery[ResultType, Params] {
pq.overrideComplexity = complexity
return pq
}
type PaginatedQueryGetOptions struct { type PaginatedQueryGetOptions struct {
Limit int Limit int
} }
@ -70,18 +76,26 @@ func (pq *PaginatedQuery[ResultType, Params]) Get(
var res []ResultType var res []ResultType
after := "" after := ""
var noop ResultType var noop ResultType
pageSize := (pq.c.MaxComplexity() - 9) / (GetComplexity(reflect.TypeOf(noop)) + 1) complexity := pq.overrideComplexity
log.Debug().Msgf("using page size %d", pageSize) if complexity == 0 {
complexity = GetTypeComplexity(reflect.TypeOf(noop))
}
maxPageSize := (pq.c.MaxQueryComplexity() - 7 - len(pq.containerLayers)) / complexity
for { for {
pageSize := maxPageSize
if opts.Limit > 0 {
remaining := opts.Limit - len(res)
if remaining < pageSize {
pageSize = remaining
}
}
page, pi, err := pq.getPage(ctx, params, after, pageSize) page, pi, err := pq.getPage(ctx, params, after, pageSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, item := range page { res = append(res, page...)
res = append(res, item) if opts.Limit > 0 && len(res) >= opts.Limit {
if opts.Limit > 0 && len(res) >= opts.Limit { return res[:opts.Limit], nil
return res, nil
}
} }
if !pi.HasNextPage { if !pi.HasNextPage {
break break
@ -125,35 +139,3 @@ func (pq *PaginatedQuery[ResultType, Params]) getPage(
} }
return res.Nodes, res.PageInfo, nil return res.Nodes, res.PageInfo, nil
} }
func GetComplexity(t reflect.Type) int {
var checkStruct func(t reflect.Type, complexity *int)
checkStruct = func(t reflect.Type, complexity *int) {
if t.Kind() != reflect.Struct {
return
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Type.Kind() == reflect.Struct {
*complexity++
tag := field.Tag.Get("graphql")
if len(tag) > 6 && tag[:6] == "... on" {
*complexity--
}
checkStruct(field.Type, complexity)
continue
}
if field.Type.Kind() == reflect.Slice {
*complexity++
tmpcomplexity := 0
checkStruct(field.Type.Elem(), &tmpcomplexity)
*complexity += tmpcomplexity * 10
continue
}
*complexity++
}
}
complexity := 0
checkStruct(t, &complexity)
return complexity
}

View File

@ -3,6 +3,7 @@ package graphql
import ( import (
"context" "context"
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -15,6 +16,8 @@ type Query[ReturnType any, Params any] struct {
additionalPayloadParams map[string]interface{} additionalPayloadParams map[string]interface{}
additionalQueryParams map[string]interface{} additionalQueryParams map[string]interface{}
maxPageSize int
} }
func NewQuery[ReturnType any, Params any]( func NewQuery[ReturnType any, Params any](
@ -29,6 +32,7 @@ func NewQuery[ReturnType any, Params any](
additionalPayloadParams: make(map[string]interface{}), additionalPayloadParams: make(map[string]interface{}),
additionalQueryParams: make(map[string]interface{}), additionalQueryParams: make(map[string]interface{}),
maxPageSize: 0,
} }
} }
@ -48,6 +52,27 @@ func (r *Query[ReturnType, Params]) WithQueryParam(
return r return r
} }
func (r *Query[ReturnType, Params]) WithMaxPageSize(maxPageSize int) *Query[ReturnType, Params] {
r.maxPageSize = maxPageSize
return r
}
func (r *Query[ReturnType, Params]) GetPageSize() int {
t := reflect.TypeOf((*ReturnType)(nil)).Elem()
if t.Kind() == reflect.Slice {
t = t.Elem()
}
complexity := GetTypeComplexity(t)
maxComplexity := r.c.MaxQueryComplexity()
layers := len(r.containerLayers)
computedPageSize := (maxComplexity - layers - 1) / complexity
if r.maxPageSize > 0 && r.maxPageSize < computedPageSize {
return r.maxPageSize
}
return computedPageSize
}
func (r *Query[ReturnType, Params]) Get(ctx context.Context, params Params) (ReturnType, error) { func (r *Query[ReturnType, Params]) Get(ctx context.Context, params Params) (ReturnType, error) {
paramsMap := convertParamsToMap(params) paramsMap := convertParamsToMap(params)

View File

@ -0,0 +1,50 @@
package mutations
import (
"context"
gql "github.com/llehouerou/go-graphql-client"
"github.com/pkg/errors"
"git.lehouerou.net/laurent/sorare/graphql"
)
type buyShopItemInput struct {
ClientMutationId string `json:"clientMutationId"`
ShopItemId gql.ID `json:"shopItemId"`
}
type BuyShopItemParams struct {
Input buyShopItemInput `graphql:"input"`
}
func (m *Mutations) newBuyShopItemMutation() *graphql.Mutation[BaseMutationPayload, BuyShopItemParams] {
return graphql.NewMutation[BaseMutationPayload, BuyShopItemParams](
m.c,
"buyShopItem",
)
}
func (m *Mutations) BuyShopItem(
ctx context.Context,
shopItemId string,
) error {
res, err := m.newBuyShopItemMutation().Execute(ctx, BuyShopItemParams{
Input: buyShopItemInput{
ClientMutationId: GetRandomId(),
ShopItemId: gql.ID(shopItemId),
},
})
if err != nil {
return errors.Wrap(err, "executing buyShopItem mutation")
}
if res.Errors != nil {
return errors.Wrap(
errors.New(res.Errors[0].Message),
"executing buyShopItem mutation",
)
}
return nil
}

View File

@ -1,16 +1,27 @@
package sorare package sorare
import (
"time"
)
type ShopItem struct { type ShopItem struct {
TypeName string `graphql:"__typename"`
ShopItemInterface struct { ShopItemInterface struct {
Id string `graphql:"id"` Id string `graphql:"id"`
Name string `graphql:"name"` Name string `graphql:"name"`
Price int `graphql:"price"` Price int `graphql:"price"`
MyBalance int `graphql:"myBalance"`
LimitPerUser int `graphql:"limitPerUser"`
MyLimitResetAt time.Time `graphql:"myLimitResetAt"`
MyPurchasesCount int `graphql:"myPurchasesCount"`
} `graphql:"... on ShopItemInterface"` } `graphql:"... on ShopItemInterface"`
JerseyShopItem struct { JerseyShopItem struct {
CurrentStockCount int `graphql:"currentStockCount"` CurrentStockCount int `graphql:"currentStockCount"`
InitialStockCount int `graphql:"initialStockCount"` InitialStockCount int `graphql:"initialStockCount"`
LimitPerUser int `graphql:"limitPerUser"`
} `graphql:"... on JerseyShopItem"` } `graphql:"... on JerseyShopItem"`
LevelUpShopItem struct {
Rarity string `graphql:"rarity"`
} `graphql:"... on LevelUpShopItem"`
} }
type ShopItemType string type ShopItemType string

View File

@ -12,8 +12,8 @@ import (
) )
type baseUser struct { type baseUser struct {
Active bool `graphql:"active"` Active bool `graphql:"active"`
CardCounts struct { FootballCardCounts struct {
Common int `graphql:"common"` Common int `graphql:"common"`
CustomSeries int `graphql:"customSeries"` CustomSeries int `graphql:"customSeries"`
Limited int `graphql:"limited"` Limited int `graphql:"limited"`
@ -21,7 +21,7 @@ type baseUser struct {
SuperRare int `graphql:"superRare"` SuperRare int `graphql:"superRare"`
Unique int `graphql:"unique"` Unique int `graphql:"unique"`
Total int `graphql:"total"` Total int `graphql:"total"`
} `graphql:"cardCounts"` } `graphql:"footballCardCounts"`
CreatedAt time.Time `graphql:"createdAt"` CreatedAt time.Time `graphql:"createdAt"`
EthVaultId int `graphql:"ethVaultId"` EthVaultId int `graphql:"ethVaultId"`
EthereumAddress string `graphql:"ethereumAddress"` EthereumAddress string `graphql:"ethereumAddress"`