diff options
Diffstat (limited to 'pkg/eax/eax.go')
-rw-r--r-- | pkg/eax/eax.go | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/pkg/eax/eax.go b/pkg/eax/eax.go new file mode 100644 index 0000000..34f5702 --- /dev/null +++ b/pkg/eax/eax.go @@ -0,0 +1,145 @@ +// 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 +} |