package twhelp import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "time" "golang.org/x/time/rate" ) const ( defaultUserAgent = "TWHelpDCBot/development" defaultTimeout = 10 * time.Second endpointListVersions = "/api/v1/versions" endpointListServers = "/api/v1/versions/%s/servers" endpointGetServer = "/api/v1/versions/%s/servers/%s" endpointListTribes = "/api/v1/versions/%s/servers/%s/tribes" endpointGetTribeByID = "/api/v1/versions/%s/servers/%s/tribes/%d" endpointListEnnoblements = "/api/v1/versions/%s/servers/%s/ennoblements" endpointListVillages = "/api/v1/versions/%s/servers/%s/villages" ) type Client struct { userAgent string client *http.Client baseURL *url.URL rateLimiter *rate.Limiter } type ClientOption func(c *Client) func WithHTTPClient(hc *http.Client) ClientOption { return func(c *Client) { c.client = hc } } func WithUserAgent(ua string) ClientOption { return func(c *Client) { c.userAgent = ua } } func WithRateLimiter(rl *rate.Limiter) ClientOption { return func(c *Client) { c.rateLimiter = rl } } func NewClient(baseURL *url.URL, opts ...ClientOption) *Client { c := &Client{ baseURL: baseURL, userAgent: defaultUserAgent, client: &http.Client{ Timeout: defaultTimeout, }, } for _, opt := range opts { opt(c) } return c } func (c *Client) ListVersions(ctx context.Context) ([]Version, error) { var resp listVersionsResp if err := c.getJSON(ctx, endpointListVersions, &resp); err != nil { return nil, err } return resp.Data, nil } type ListServersQueryParams struct { Limit int32 Offset int32 Open NullBool } func (c *Client) ListServers( ctx context.Context, version string, params ListServersQueryParams, ) ([]Server, error) { q := url.Values{} if params.Limit > 0 { q.Set("limit", strconv.FormatInt(int64(params.Limit), 10)) } if params.Offset > 0 { q.Set("offset", strconv.FormatInt(int64(params.Offset), 10)) } if params.Open.Valid { q.Set("open", strconv.FormatBool(params.Open.Bool)) } var resp listServersResp if err := c.getJSON(ctx, fmt.Sprintf(endpointListServers, version)+"?"+q.Encode(), &resp); err != nil { return nil, err } return resp.Data, nil } func (c *Client) GetServer(ctx context.Context, version, server string) (Server, error) { var resp getServerResp if err := c.getJSON(ctx, fmt.Sprintf(endpointGetServer, version, server), &resp); err != nil { return Server{}, err } return resp.Data, nil } type ListTribesQueryParams struct { Limit int32 Offset int32 Tags []string Deleted NullBool } func (c *Client) ListTribes( ctx context.Context, version, server string, params ListTribesQueryParams, ) ([]Tribe, error) { q := url.Values{} if params.Limit > 0 { q.Set("limit", strconv.FormatInt(int64(params.Limit), 10)) } if params.Offset > 0 { q.Set("offset", strconv.FormatInt(int64(params.Offset), 10)) } if params.Deleted.Valid { q.Set("deleted", strconv.FormatBool(params.Deleted.Bool)) } if params.Tags != nil { for _, t := range params.Tags { q.Add("tag", t) } } var resp listTribesResp if err := c.getJSON(ctx, fmt.Sprintf(endpointListTribes, version, server)+"?"+q.Encode(), &resp); err != nil { return nil, err } return resp.Data, nil } func (c *Client) GetTribeByID(ctx context.Context, version, server string, id int64) (Tribe, error) { var resp getTribeResp if err := c.getJSON(ctx, fmt.Sprintf(endpointGetTribeByID, version, server, id), &resp); err != nil { return Tribe{}, err } return resp.Data, nil } type EnnoblementSort string const ( EnnoblementSortCreatedAtASC EnnoblementSort = "createdAt:ASC" ) func (s EnnoblementSort) String() string { return string(s) } type ListEnnoblementsQueryParams struct { Limit int32 Offset int32 Since time.Time Sort []EnnoblementSort } func (c *Client) ListEnnoblements( ctx context.Context, version, server string, params ListEnnoblementsQueryParams, ) ([]Ennoblement, error) { q := url.Values{} if params.Limit > 0 { q.Set("limit", strconv.FormatInt(int64(params.Limit), 10)) } if params.Offset > 0 { q.Set("offset", strconv.FormatInt(int64(params.Offset), 10)) } if !params.Since.IsZero() { q.Set("since", params.Since.Format(time.RFC3339)) } for _, s := range params.Sort { q.Add("sort", s.String()) } var resp listEnnoblementsResp if err := c.getJSON(ctx, fmt.Sprintf(endpointListEnnoblements, version, server)+"?"+q.Encode(), &resp); err != nil { return nil, err } return resp.Data, nil } type ListVillagesQueryParams struct { Limit int32 Offset int32 Coords []string } func (c *Client) ListVillages( ctx context.Context, version, server string, params ListVillagesQueryParams, ) ([]Village, error) { q := url.Values{} if params.Limit > 0 { q.Set("limit", strconv.FormatInt(int64(params.Limit), 10)) } if params.Offset > 0 { q.Set("offset", strconv.FormatInt(int64(params.Offset), 10)) } for _, co := range params.Coords { q.Add("coords", co) } var resp listVillagesResp if err := c.getJSON(ctx, fmt.Sprintf(endpointListVillages, version, server)+"?"+q.Encode(), &resp); err != nil { return nil, err } return resp.Data, nil } func (c *Client) getJSON(ctx context.Context, urlStr string, v any) error { u, err := c.baseURL.Parse(urlStr) if err != nil { return fmt.Errorf("c.baseURL.Parse: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return fmt.Errorf("http.NewRequestWithContext: %w", err) } // headers req.Header.Set("User-Agent", c.userAgent) // rate limiter if c.rateLimiter != nil { if err = c.rateLimiter.Wait(ctx); err != nil { return err } } resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("client.Do: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { var errResp errorResp if err = json.NewDecoder(resp.Body).Decode(&errResp); err != nil { _, _ = io.Copy(io.Discard, resp.Body) return fmt.Errorf("got non-ok HTTP status: %d", resp.StatusCode) } return errResp.Error } if err = json.NewDecoder(resp.Body).Decode(v); err != nil { return fmt.Errorf("couldn't decode resp body: %w", err) } return nil }