dcbot/internal/twhelp/client.go

292 lines
6.2 KiB
Go

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
}