diff options
Diffstat (limited to 'pkg/eax')
-rw-r--r-- | pkg/eax/eax.go | 145 | ||||
-rw-r--r-- | pkg/eax/updatemgr.go | 180 |
2 files changed, 0 insertions, 325 deletions
diff --git a/pkg/eax/eax.go b/pkg/eax/eax.go deleted file mode 100644 index 34f5702..0000000 --- a/pkg/eax/eax.go +++ /dev/null @@ -1,145 +0,0 @@ -// Package eax queries the EA App API. -package eax - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "mime" - "net/http" - "net/url" - "strconv" -) - -type Client struct { - // The [net/http.Client] to use for requests. If not provided, - // [net/http.DefaultClient] is used. - Client *http.Client - - // The UpdateMgr for requests which require version information. - UpdateMgr *UpdateMgr -} - -var ErrVersionRequired = errors.New("client version is required for this endpoint") - -// PlayerID contains basic identifiers and names for a player. -type PlayerID struct { - PD uint64 // origin ID - PSD uint64 // ? - DisplayName string // in-game name - Nickname string // social name? -} - -// PlayerByPd gets basic information about an Origin ID. If the player does not -// exist, nil will be returned. -func (c *Client) PlayerIDByPD(ctx context.Context, pd uint64) (*PlayerID, error) { - var obj struct { - PlayerByPD *struct { - PD string `json:"pd"` - PSD string `json:"psd"` - DisplayName string `json:"displayName"` - Nickname string `json:"nickname"` - } `json:"playerByPd"` - } - if err := c.gql1(ctx, true, `query { playerByPd (pd: `+strconv.FormatUint(pd, 10)+`) { pd psd displayName nickname } }`, &obj); err != nil { - return nil, err - } - if obj.PlayerByPD == nil { - return nil, nil - } - res := &PlayerID{ - DisplayName: obj.PlayerByPD.DisplayName, - Nickname: obj.PlayerByPD.Nickname, - } - if s := obj.PlayerByPD.PD; s != "" { - if v, err := strconv.ParseUint(s, 10, 64); err == nil { - res.PD = v - } else { - return res, fmt.Errorf("parse pd %q: %w", s, err) - } - } - if s := obj.PlayerByPD.PSD; s != "" { - if v, err := strconv.ParseUint(s, 10, 64); err == nil { - res.PD = v - } else { - return res, fmt.Errorf("parse psd %q: %w", s, err) - } - } - return res, nil -} - -// gql1 performs a basic GraphQL query. -func (c *Client) gql1(ctx context.Context, ver bool, query string, out any) error { - req, err := c.req(ctx, ver, http.MethodGet, "https://service-aggregation-layer.juno.ea.com/graphql?query="+url.QueryEscape(query), nil) - if err != nil { - return err - } - - resp, err := c.do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if mt, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")); mt != "application/json" { - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("response status %d (%s) with content-type %q", resp.StatusCode, resp.Status, mt) - } - return fmt.Errorf("unexpected content-type %q", mt) - } - - buf, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - var obj struct { - Data json.RawMessage `json:"data"` - Errors []struct { - Message string `json:"message"` - } `json:"errors"` - } - if err := json.Unmarshal(buf, &obj); err != nil { - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("response status %d (%s) with invalid json", resp.StatusCode, resp.Status) - } - return fmt.Errorf("invalid json resp: %w", err) - } - if len(obj.Errors) != 0 { - return fmt.Errorf("got %d errors, including %q", len(obj.Errors), obj.Errors[0].Message) - } - if err := json.Unmarshal([]byte(obj.Data), out); err != nil { - return fmt.Errorf("invalid json data: %w", err) - } - return nil -} - -func (c *Client) do(r *http.Request) (*http.Response, error) { - if c.Client == nil { - return http.DefaultClient.Do(r) - } - return c.Client.Do(r) -} - -func (c *Client) req(ctx context.Context, ver bool, method, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err == nil { - if ver { - if c.UpdateMgr == nil { - return nil, ErrVersionRequired - } - ver, _, err := c.UpdateMgr.Update(false) - if err != nil { - return nil, fmt.Errorf("%w: failed to update version: %v", ErrVersionRequired, err) - } - if ver == "" { - return nil, ErrVersionRequired - } - req.Header.Set("User-Agent", "EADesktop/"+ver) - } - req.Header.Set("x-client-id", "EAX-JUNO-CLIENT") - } - return req, err -} diff --git a/pkg/eax/updatemgr.go b/pkg/eax/updatemgr.go deleted file mode 100644 index d1a6a59..0000000 --- a/pkg/eax/updatemgr.go +++ /dev/null @@ -1,180 +0,0 @@ -package eax - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" -) - -// UpdateMgr manages EAX client version information. -type UpdateMgr struct { - // HTTP client to use. If not provided, [net/http.DefaultClient] will be - // used. - Client *http.Client - - // Timeout is the timeout for refreshing tokens. If zero, a reasonable - // default is used. If negative, there is no timeout. - Timeout time.Duration - - // Interval to update at. If zero, will not auto-update. - AutoUpdateInterval time.Duration - - // Auto-update staged roll-out bucket. - AutoUpdateBucket int - - // Auto-update backoff, if provided, checks if another auto-update is - // allowed after a failure. If it returns false, ErrAutoUpdateBackoff will be - // returned from the function triggering the auto-update. - AutoUpdateBackoff func(err error, time time.Time, count int) bool - - // AutoUpdateHook is called for every auto-update attempt with the new (or - // current if error) version, and any error which occurred. - AutoUpdateHook func(v string, err error) - - verInit sync.Once - verPf bool // ensures only one auto-update runs at a time - verCv *sync.Cond // allows other goroutines to wait for that update to complete - verErr error // last auto-update error - verErrTime time.Time // last auto-update error time - verErrCount int // consecutive auto-update errors - ver string // current version - verTime time.Time // last update check time -} - -var ErrAutoUpdateBackoff = errors.New("not updating eax client version due to backoff") - -func (u *UpdateMgr) init() { - u.verInit.Do(func() { - u.verCv = sync.NewCond(new(sync.Mutex)) - }) -} - -// SetVersion sets the current version. -func (u *UpdateMgr) SetVersion(v string) { - u.init() - u.verCv.L.Lock() - for u.verPf { - u.verCv.Wait() - } - u.ver = v - u.verErr = nil - u.verTime = time.Now() - u.verCv.L.Unlock() -} - -// Update gets the latest version, following u.AutoUpdateInterval if provided, -// unless the version is not set or force is true. If another update is in -// progress, it waits for the result of it. True is returned (on success or -// failure) if this call performed a update. This function may block for up to -// Timeout. -func (u *UpdateMgr) Update(force bool) (string, bool, error) { - u.init() - if u.verCv.L.Lock(); u.verPf { - for u.verPf { - u.verCv.Wait() - } - defer u.verCv.L.Unlock() - return u.ver, false, u.verErr - } else { - if force || u.ver == "" || (u.AutoUpdateInterval != 0 && time.Since(u.verTime) > u.AutoUpdateInterval) { - u.verPf = true - u.verCv.L.Unlock() - defer func() { - u.verCv.L.Lock() - u.verCv.Broadcast() - u.verPf = false - u.verCv.L.Unlock() - }() - } else { - defer u.verCv.L.Unlock() - return u.ver, false, u.verErr - } - } - if u.verErr != nil && u.AutoUpdateBackoff != nil { - if !u.AutoUpdateBackoff(u.verErr, u.verErrTime, u.verErrCount) { - return u.ver, true, fmt.Errorf("%w (%d attempts, last error: %v)", ErrAutoUpdateBackoff, u.verErrCount, u.verErr) - } - } - u.verErr = func() error { - var ctx context.Context - var cancel context.CancelFunc - if u.Timeout > 0 { - ctx, cancel = context.WithTimeout(context.Background(), u.Timeout) - } else if u.Timeout == 0 { - ctx, cancel = context.WithTimeout(context.Background(), time.Second*15) - } else { - ctx, cancel = context.WithCancel(context.Background()) - } - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://autopatch.juno.ea.com/autopatch/upgrade/buckets/"+strconv.Itoa(u.AutoUpdateBucket), nil) - if err != nil { - return err - } - if u.ver != "" { - req.Header.Set("User-Agent", "EADesktop/"+u.ver) - } else { - req.Header.Set("User-Agent", "") - } - - cl := u.Client - if cl == nil { - cl = http.DefaultClient - } - - resp, err := cl.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - var obj struct { - Minimum struct { - Version string `json:"version"` - ActivationDate string `json:"activationDate"` - } `json:"minimum"` - Recommended struct { - Version string `json:"version"` - } `json:"recommended"` - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("response status %d (%s)", resp.StatusCode, resp.Status) - } - if err := json.NewDecoder(resp.Body).Decode(&obj); err != nil { - return fmt.Errorf("parse autopatch response: %w", err) - } - - var version string - if v := obj.Minimum.Version; v != "" { - version = v - } - if v := obj.Recommended.Version; v != "" { - version = v - } - if version == "" { - return fmt.Errorf("parse autopatch response: missing minimum or recommended version") - } - u.ver = version - u.verTime = time.Now() - return nil - }() - if u.verErrCount != 0 { - u.verErr = fmt.Errorf("%w (attempt %d)", u.verErr, u.verErrCount) - } - if u.verErr != nil { - u.verErrCount++ - u.verErrTime = time.Now() - } else { - u.verErrCount = 0 - u.verErrTime = time.Time{} - } - if u.AutoUpdateHook != nil { - go u.AutoUpdateHook(u.ver, u.verErr) - } - return u.ver, true, u.verErr -} |