From 5e99f68a91eed8b3e555429550797bc91d3b1add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Mon, 31 Oct 2022 05:52:20 +0000 Subject: [PATCH] feat: auto clean up old groups (#38) Reviewed-on: https://gitea.dwysokinski.me/twhelp/dcbot/pulls/38 --- cmd/dcbot/internal/run/run.go | 7 +- internal/bundb/group.go | 28 ++++ internal/bundb/group_test.go | 82 +++++++++++- internal/bundb/testdata/fixture.yml | 16 +++ internal/discord/bot.go | 28 +++- internal/discord/job_groups_cleanup.go | 30 +++++ internal/domain/group.go | 5 +- internal/service/group.go | 134 ++++++++++++++++--- internal/service/group_test.go | 172 ++++++++++++++++++++++++- internal/service/service.go | 5 + internal/twhelp/client.go | 54 ++++++-- internal/twhelp/twhelp.go | 9 ++ 12 files changed, 532 insertions(+), 38 deletions(-) create mode 100644 internal/discord/job_groups_cleanup.go diff --git a/cmd/dcbot/internal/run/run.go b/cmd/dcbot/internal/run/run.go index 79f5f52..7e5d3b6 100644 --- a/cmd/dcbot/internal/run/run.go +++ b/cmd/dcbot/internal/run/run.go @@ -47,7 +47,12 @@ func New() *cli.Command { monitorRepo := bundb.NewMonitor(db) choiceSvc := service.NewChoice(client) - groupSvc := service.NewGroup(groupRepo, client, cfg.MaxGroupsPerServer) + groupSvc := service.NewGroup( + groupRepo, + client, + logger, + cfg.MaxGroupsPerServer, + ) monitorSvc := service.NewMonitor( monitorRepo, groupRepo, diff --git a/internal/bundb/group.go b/internal/bundb/group.go index 33cb1f2..d7fbf5a 100644 --- a/internal/bundb/group.go +++ b/internal/bundb/group.go @@ -130,6 +130,22 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error { return nil } +func (g *Group) DeleteMany(ctx context.Context, ids ...string) error { + if len(ids) == 0 { + return nil + } + + _, err := g.db.NewDelete(). + Model(&model.Group{}). + Returning("NULL"). + Where("id IN (?)", bun.In(ids)). + Exec(ctx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("couldn't delete groups: %w", err) + } + return nil +} + type updateGroupsParamsApplier struct { params domain.UpdateGroupParams } @@ -182,5 +198,17 @@ func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { } } + if l.params.VersionCode.Valid { + q = q.Where("version_code = ?", l.params.VersionCode.String) + } + + if l.params.ServerKeys != nil { + q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys)) + } + + if !l.params.CreatedAtLTE.IsZero() { + q = q.Where("created_at <= ?", l.params.CreatedAtLTE) + } + return q } diff --git a/internal/bundb/group_test.go b/internal/bundb/group_test.go index 5690622..a60a16b 100644 --- a/internal/bundb/group_test.go +++ b/internal/bundb/group_test.go @@ -211,6 +211,44 @@ func TestGroup_List(t *testing.T) { "abeb6c8e-70b6-445c-989f-890cd2a1f87a", }, }, + { + name: "VersionCode=pl", + params: domain.ListGroupsParams{ + VersionCode: domain.NullString{ + String: "pl", + Valid: true, + }, + }, + expectedGroups: []string{ + "d56ad37f-2637-48ea-98f8-79627f3fcc96", + "429b790e-7186-4106-b531-4cc4931ce2ba", + "abeb6c8e-70b6-445c-989f-890cd2a1f87a", + "0be82203-4ca3-4b4c-a0c8-3a70099d88f7", + "982a9765-471c-43e8-9abb-1e4ee4f03738", + }, + }, + { + name: "VersionCode=pl,ServerKeys=[pl180]", + params: domain.ListGroupsParams{ + VersionCode: domain.NullString{ + String: "pl", + Valid: true, + }, + ServerKeys: []string{"pl180"}, + }, + expectedGroups: []string{ + "982a9765-471c-43e8-9abb-1e4ee4f03738", + }, + }, + { + name: "CreatedAtLTE=2022-03-15T15:00:10.000Z", + params: domain.ListGroupsParams{ + CreatedAtLTE: time.Date(2022, time.March, 15, 15, 00, 10, 0, time.UTC), + }, + expectedGroups: []string{ + "d56ad37f-2637-48ea-98f8-79627f3fcc96", + }, + }, } for _, tt := range tests { @@ -363,11 +401,53 @@ func TestGroup_Delete(t *testing.T) { }) } +func TestGroup_DeleteMany(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + groupRepo := bundb.NewGroup(db) + monitorRepo := bundb.NewMonitor(db) + group1 := getGroupFromFixture(t, fixture, "group-1-server-1") + group2 := getGroupFromFixture(t, fixture, "group-2-server-1") + ids := []string{group1.ID.String(), group2.ID.String()} + serverIDs := []string{group1.ServerID} + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + beforeDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{ + ServerIDs: serverIDs, + }) + assert.NoError(t, err) + + assert.NoError(t, groupRepo.DeleteMany(context.Background(), ids...)) + + afterDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{ + ServerIDs: serverIDs, + }) + assert.NoError(t, err) + assert.Len(t, afterDelete, len(beforeDelete)-len(ids)) + + // monitors should also be deleted + for _, id := range ids { + monitors, err := monitorRepo.List(context.Background(), id) + assert.NoError(t, err) + assert.Len(t, monitors, 0) + } + }) + + t.Run("OK: 0 ids", func(t *testing.T) { + t.Parallel() + assert.NoError(t, groupRepo.DeleteMany(context.Background())) + }) +} + func getAllGroupsFromFixture(tb testing.TB, fixture *dbfixture.Fixture) []model.Group { tb.Helper() //nolint:lll - ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2"} + ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2", "group-3-server-1", "group-3-server-2"} groups := make([]model.Group, 0, len(ids)) for _, id := range ids { diff --git a/internal/bundb/testdata/fixture.yml b/internal/bundb/testdata/fixture.yml index 1068143..adca02c 100644 --- a/internal/bundb/testdata/fixture.yml +++ b/internal/bundb/testdata/fixture.yml @@ -28,6 +28,22 @@ channel_gains: 1555 channel_losses: 1235 created_at: 2022-03-19T15:03:10.000Z + - _id: group-3-server-1 + id: 982a9765-471c-43e8-9abb-1e4ee4f03738 + server_id: server-3 + server_key: pl180 + version_code: pl + channel_gains: 1555 + channel_losses: 1235 + created_at: 2022-03-23T18:03:10.000Z + - _id: group-3-server-2 + id: f3a0a5d9-07fe-4770-8b43-79cbe10bb310 + server_id: server-3 + server_key: en130 + version_code: en + channel_gains: 1555 + channel_losses: 1235 + created_at: 2022-04-23T18:03:10.000Z - model: Monitor rows: - _id: monitor-1-group-1-server-1 diff --git a/internal/discord/bot.go b/internal/discord/bot.go index 444d9a2..5f21023 100644 --- a/internal/discord/bot.go +++ b/internal/discord/bot.go @@ -17,6 +17,7 @@ type GroupService interface { SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error) SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error) + CleanUp(ctx context.Context) error List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) Delete(ctx context.Context, id, serverID string) error } @@ -122,14 +123,27 @@ func (b *Bot) registerCommand(cmd command) error { } func (b *Bot) initCron() error { - _, err := b.c.AddJob("@every 1m", &executeMonitorsJob{ - svc: b.monitorSvc, - s: b.s, - logger: b.logger, - }) - if err != nil { - return err + jobs := []struct { + spec string + job cron.Job + }{ + { + spec: "@every 1m", + job: &executeMonitorsJob{svc: b.monitorSvc, s: b.s, logger: b.logger}, + }, + { + spec: "0 */8 * * *", + job: &cleanUpGroupsJob{svc: b.groupSvc, logger: b.logger}, + }, } + + for _, j := range jobs { + _, err := b.c.AddJob(j.spec, j.job) + if err != nil { + return err + } + } + return nil } diff --git a/internal/discord/job_groups_cleanup.go b/internal/discord/job_groups_cleanup.go new file mode 100644 index 0000000..0a3b2d2 --- /dev/null +++ b/internal/discord/job_groups_cleanup.go @@ -0,0 +1,30 @@ +package discord + +import ( + "context" + "time" + + "go.uber.org/zap" +) + +const ( + cleanUpGroupsJobTimeout = 10 * time.Minute +) + +type cleanUpGroupsJob struct { + svc GroupService + logger *zap.Logger +} + +func (j *cleanUpGroupsJob) Run() { + ctx, cancel := context.WithTimeout(context.Background(), cleanUpGroupsJobTimeout) + defer cancel() + + start := time.Now() + if err := j.svc.CleanUp(ctx); err != nil { + j.logger.Error("something went wrong while deleting old groups", zap.Error(err)) + return + } + + j.logger.Info("old groups have been deleted", zap.Duration("duration", time.Since(start))) +} diff --git a/internal/domain/group.go b/internal/domain/group.go index debb4f0..ccf4689 100644 --- a/internal/domain/group.go +++ b/internal/domain/group.go @@ -106,8 +106,11 @@ func (u UpdateGroupParams) IsZero() bool { } type ListGroupsParams struct { - ServerIDs []string + ServerIDs []string // DC server IDs + VersionCode NullString + ServerKeys []string EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null + CreatedAtLTE time.Time } type GroupLimitReachedError struct { diff --git a/internal/service/group.go b/internal/service/group.go index 21cd745..213a8d5 100644 --- a/internal/service/group.go +++ b/internal/service/group.go @@ -4,9 +4,11 @@ import ( "context" "errors" "fmt" + "time" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" + "go.uber.org/zap" ) //counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository @@ -16,18 +18,21 @@ type GroupRepository interface { List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) Get(ctx context.Context, id, serverID string) (domain.Group, error) Delete(ctx context.Context, id, serverID string) error + DeleteMany(ctx context.Context, id ...string) error } type Group struct { repo GroupRepository client TWHelpClient + logger *zap.Logger maxGroupsPerServer int } -func NewGroup(repo GroupRepository, client TWHelpClient, maxGroupsPerServer int) *Group { +func NewGroup(repo GroupRepository, client TWHelpClient, logger *zap.Logger, maxGroupsPerServer int) *Group { return &Group{ repo: repo, client: client, + logger: logger, maxGroupsPerServer: maxGroupsPerServer, } } @@ -59,6 +64,29 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do return group, nil } +func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string) error { + server, err := g.client.GetServer(ctx, versionCode, serverKey) + if err != nil { + var apiErr twhelp.APIError + if !errors.As(err, &apiErr) { + return fmt.Errorf("TWHelpClient.GetServer: %w", err) + } + return domain.ServerDoesNotExistError{ + VersionCode: versionCode, + Key: serverKey, + } + } + + if !server.Open { + return domain.ServerIsClosedError{ + VersionCode: versionCode, + Key: serverKey, + } + } + + return nil +} + func (g *Group) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error) { return g.update(ctx, id, serverID, domain.UpdateGroupParams{ ChannelGains: domain.NullString{ @@ -126,23 +154,97 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error { return nil } -func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string) error { - server, err := g.client.GetServer(ctx, versionCode, serverKey) - if err != nil { - var apiErr twhelp.APIError - if !errors.As(err, &apiErr) { - return fmt.Errorf("TWHelpClient.GetServer: %w", err) - } - return domain.ServerDoesNotExistError{ - VersionCode: versionCode, - Key: serverKey, - } +func (g *Group) CleanUp(ctx context.Context) error { + if err := g.deleteAllWithDisabledNotifications(ctx); err != nil { + return err } - if !server.Open { - return domain.ServerIsClosedError{ - VersionCode: versionCode, - Key: serverKey, + if err := g.deleteAllWithClosedTWServers(ctx); err != nil { + return err + } + + return nil +} + +func (g *Group) deleteAllWithDisabledNotifications(ctx context.Context) error { + groups, err := g.repo.List(ctx, domain.ListGroupsParams{ + EnabledNotifications: domain.NullBool{ + Bool: false, + Valid: true, + }, + CreatedAtLTE: time.Now().Add(-24 * time.Hour), + }) + if err != nil { + return fmt.Errorf("GroupRepository.List: %w", err) + } + + ids := make([]string, 0, len(groups)) + for _, group := range groups { + ids = append(ids, group.ID) + } + + if err = g.repo.DeleteMany(ctx, ids...); err != nil { + return fmt.Errorf("GroupRepository.DeleteMany: %w", err) + } + + return nil +} + +func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error { + versions, err := g.client.ListVersions(ctx) + if err != nil { + return fmt.Errorf("TWHelpClient.ListVersions: %w", err) + } + + for _, v := range versions { + servers, err := g.client.ListServers(ctx, v.Code, twhelp.ListServersQueryParams{ + Open: twhelp.NullBool{ + Bool: false, + Valid: true, + }, + }) + if err != nil { + g.logger.Warn("failed to fetch closed servers", zap.Error(err), zap.String("versionCode", v.Code)) + continue + } + + if len(servers) == 0 { + continue + } + + params := domain.ListGroupsParams{ + VersionCode: domain.NullString{ + String: v.Code, + Valid: true, + }, + ServerKeys: make([]string, 0, len(servers)), + } + for _, s := range servers { + params.ServerKeys = append(params.ServerKeys, s.Key) + } + + groups, err := g.repo.List(ctx, params) + if err != nil { + g.logger.Warn("failed to list groups", zap.Error(err), zap.String("versionCode", v.Code)) + continue + } + + if len(groups) == 0 { + continue + } + + ids := make([]string, 0, len(groups)) + for _, group := range groups { + ids = append(ids, group.ID) + } + + if err = g.repo.DeleteMany(ctx, ids...); err != nil { + g.logger.Warn( + "failed to delete groups", + zap.Error(err), + zap.String("versionCode", v.Code), + ) + continue } } diff --git a/internal/service/group_test.go b/internal/service/group_test.go index d890bb1..12977d0 100644 --- a/internal/service/group_test.go +++ b/internal/service/group_test.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/zap" ) func TestGroup_Create(t *testing.T) { @@ -57,7 +58,7 @@ func TestGroup_Create(t *testing.T) { }, nil }) - g, err := service.NewGroup(repo, client, 1).Create(context.Background(), params) + g, err := service.NewGroup(repo, client, zap.NewNop(), 1).Create(context.Background(), params) assert.NoError(t, err) assert.NotEmpty(t, g.ID) assert.Equal(t, params.ServerID(), g.ServerID) @@ -78,7 +79,7 @@ func TestGroup_Create(t *testing.T) { repo := &mock.FakeGroupRepository{} repo.ListReturns(make([]domain.Group, maxGroupsPerServer), nil) - g, err := service.NewGroup(repo, nil, maxGroupsPerServer).Create(context.Background(), params) + g, err := service.NewGroup(repo, nil, zap.NewNop(), maxGroupsPerServer).Create(context.Background(), params) assert.ErrorIs(t, err, domain.GroupLimitReachedError{ Current: maxGroupsPerServer, Limit: maxGroupsPerServer, @@ -100,7 +101,7 @@ func TestGroup_Create(t *testing.T) { } }) - g, err := service.NewGroup(repo, client, 1).Create(context.Background(), params) + g, err := service.NewGroup(repo, client, zap.NewNop(), 1).Create(context.Background(), params) assert.ErrorIs(t, err, domain.ServerDoesNotExistError{ VersionCode: params.VersionCode(), Key: params.ServerKey(), @@ -123,7 +124,7 @@ func TestGroup_Create(t *testing.T) { }, nil }) - g, err := service.NewGroup(repo, client, 1).Create(context.Background(), params) + g, err := service.NewGroup(repo, client, zap.NewNop(), 1).Create(context.Background(), params) assert.ErrorIs(t, err, domain.ServerIsClosedError{ VersionCode: params.VersionCode(), Key: params.ServerKey(), @@ -131,3 +132,166 @@ func TestGroup_Create(t *testing.T) { assert.Zero(t, g) }) } + +func TestGroup_CleanUp(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeGroupRepository{} + groupsWithDisabledNotifications := []domain.Group{ + { + ID: uuid.NewString(), + ServerID: uuid.NewString(), + ChannelGains: "", + ChannelLosses: "", + Internals: true, + Barbarians: true, + ServerKey: "pl181", + VersionCode: "pl", + CreatedAt: time.Now().Add(-48 * time.Hour), + }, + { + ID: uuid.NewString(), + ServerID: uuid.NewString(), + ChannelGains: "", + ChannelLosses: "", + Internals: true, + Barbarians: true, + ServerKey: "pl181", + VersionCode: "pl", + CreatedAt: time.Now().Add(-45 * time.Hour), + }, + } + repo.ListReturnsOnCall(0, groupsWithDisabledNotifications, nil) + groupsPL := []domain.Group{ + { + ID: uuid.NewString(), + ServerID: uuid.NewString(), + ChannelGains: uuid.NewString(), + ChannelLosses: "", + Internals: true, + Barbarians: true, + ServerKey: "pl181", + VersionCode: "pl", + CreatedAt: time.Now().Add(-120 * time.Hour), + }, + { + ID: uuid.NewString(), + ServerID: uuid.NewString(), + ChannelGains: uuid.NewString(), + ChannelLosses: uuid.NewString(), + Internals: true, + Barbarians: true, + ServerKey: "pl181", + VersionCode: "pl", + CreatedAt: time.Now().Add(-11 * time.Hour), + }, + } + repo.ListReturnsOnCall(1, groupsPL, nil) + groupsUK := []domain.Group{ + { + ID: uuid.NewString(), + ServerID: uuid.NewString(), + ChannelGains: uuid.NewString(), + ChannelLosses: uuid.NewString(), + Internals: true, + Barbarians: true, + ServerKey: "uk51", + VersionCode: "uk", + CreatedAt: time.Now().Add(-150 * time.Hour), + }, + { + ID: uuid.NewString(), + ServerID: uuid.NewString(), + ChannelGains: uuid.NewString(), + ChannelLosses: uuid.NewString(), + Internals: true, + Barbarians: true, + ServerKey: "uk52", + VersionCode: "uk", + CreatedAt: time.Now().Add(-200 * time.Hour), + }, + } + repo.ListReturnsOnCall(2, groupsUK, nil) + repo.DeleteManyReturns(nil) + + client := &mock.FakeTWHelpClient{} + versions := []twhelp.Version{ + { + Code: "pl", + Host: "www.plemiona.pl", + Name: "Poland", + Timezone: "Europe/Warsaw", + }, + { + Code: "uk", + Host: "www.tribalwars.co.uk", + Name: "United Kingdom", + Timezone: "Europe/London", + }, + { + Code: "tr", + Host: "www.klanlar.org", + Name: "Turkey", + Timezone: "Europe/Istanbul", + }, + } + client.ListVersionsReturns(versions, nil) + serversPL := []twhelp.Server{ + { + Key: "pl181", + URL: "https://pl181.plemiona.pl", + Open: false, + }, + } + client.ListServersReturnsOnCall(0, serversPL, nil) + serversUK := []twhelp.Server{ + { + Key: "uk51", + URL: "https://uk51.tribalwars.co.uk", + Open: false, + }, + { + Key: "uk52", + URL: "https://uk52.tribalwars.co.uk", + Open: false, + }, + } + client.ListServersReturnsOnCall(1, serversUK, nil) + client.ListServersReturnsOnCall(1, serversUK, nil) + client.ListServersReturnsOnCall(2, nil, nil) // Turkey + + assert.NoError(t, service.NewGroup(repo, client, zap.NewNop(), 1).CleanUp(context.Background())) + + require.Equal(t, 3, repo.ListCallCount()) + require.Equal(t, 3, repo.DeleteManyCallCount()) + for _, tt := range []struct { + i int + groups []domain.Group + }{ + { + i: 0, + groups: groupsWithDisabledNotifications, + }, + { + i: 1, + groups: groupsPL, + }, + { + i: 2, + groups: groupsUK, + }, + } { + _, ids := repo.DeleteManyArgsForCall(tt.i) + assert.Len(t, ids, len(tt.groups)) + for i, id := range ids { + assert.Equal(t, tt.groups[i].ID, id) + } + } + + require.Equal(t, 1, client.ListVersionsCallCount()) + require.Equal(t, 3, client.ListServersCallCount()) + }) +} diff --git a/internal/service/service.go b/internal/service/service.go index 9d072ad..c328c3d 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -11,6 +11,11 @@ import ( //counterfeiter:generate -o internal/mock/twhelp_client.gen.go . TWHelpClient type TWHelpClient interface { ListVersions(ctx context.Context) ([]twhelp.Version, error) + ListServers( + ctx context.Context, + version string, + params twhelp.ListServersQueryParams, + ) ([]twhelp.Server, error) GetServer(ctx context.Context, version, server string) (twhelp.Server, error) GetTribeByID(ctx context.Context, version, server string, id int64) (twhelp.Tribe, error) GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error) diff --git a/internal/twhelp/client.go b/internal/twhelp/client.go index 2ebb628..61f6eee 100644 --- a/internal/twhelp/client.go +++ b/internal/twhelp/client.go @@ -16,6 +16,7 @@ const ( defaultTimeout = 10 * time.Second endpointListVersions = "/api/v1/versions" + endpointListServers = "/api/v1/versions/%s/servers" endpointGetServer = "/api/v1/versions/%s/servers/%s" endpointGetTribeByID = "/api/v1/versions/%s/servers/%s/tribes/%d" endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s" @@ -64,6 +65,39 @@ func (c *Client) ListVersions(ctx context.Context) ([]Version, error) { 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.Itoa(int(params.Limit))) + } + + if params.Offset > 0 { + q.Set("offset", strconv.Itoa(int(params.Offset))) + } + + 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 { @@ -105,22 +139,26 @@ type ListEnnoblementsQueryParams struct { Sort []ListEnnoblementsSort } -func (c *Client) ListEnnoblements(ctx context.Context, version, server string, queryParams ListEnnoblementsQueryParams) ([]Ennoblement, error) { +func (c *Client) ListEnnoblements( + ctx context.Context, + version, server string, + params ListEnnoblementsQueryParams, +) ([]Ennoblement, error) { q := url.Values{} - if queryParams.Limit > 0 { - q.Set("limit", strconv.Itoa(int(queryParams.Limit))) + if params.Limit > 0 { + q.Set("limit", strconv.Itoa(int(params.Limit))) } - if queryParams.Offset > 0 { - q.Set("offset", strconv.Itoa(int(queryParams.Offset))) + if params.Offset > 0 { + q.Set("offset", strconv.Itoa(int(params.Offset))) } - if !queryParams.Since.IsZero() { - q.Set("since", queryParams.Since.Format(time.RFC3339)) + if !params.Since.IsZero() { + q.Set("since", params.Since.Format(time.RFC3339)) } - for _, s := range queryParams.Sort { + for _, s := range params.Sort { q.Add("sort", s.String()) } diff --git a/internal/twhelp/twhelp.go b/internal/twhelp/twhelp.go index fcf149f..33aa4ce 100644 --- a/internal/twhelp/twhelp.go +++ b/internal/twhelp/twhelp.go @@ -53,6 +53,10 @@ type getServerResp struct { Data Server `json:"data"` } +type listServersResp struct { + Data []Server `json:"data"` +} + type Tribe struct { ID int64 `json:"id"` Tag string `json:"tag"` @@ -152,3 +156,8 @@ type Ennoblement struct { type listEnnoblementsResp struct { Data []Ennoblement `json:"data"` } + +type NullBool struct { + Bool bool + Valid bool // Valid is true if Bool is not NULL +}