aboutsummaryrefslogtreecommitdiff
path: root/pkg/eax/eax.go
blob: 34f57021c5341c239ddc90f0c7f178ecf56dceed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
}