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..a51fab6 100644 --- a/internal/bundb/group.go +++ b/internal/bundb/group.go @@ -130,6 +130,18 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error { return nil } +func (g *Group) DeleteMany(ctx context.Context, id ...string) error { + _, err := g.db.NewDelete(). + Model(&model.Group{}). + Returning("NULL"). + Where("id IN (?)", id). + 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 } diff --git a/internal/domain/group.go b/internal/domain/group.go index debb4f0..f56fca7 100644 --- a/internal/domain/group.go +++ b/internal/domain/group.go @@ -105,9 +105,16 @@ func (u UpdateGroupParams) IsZero() bool { !u.Barbarians.Valid } +type ListGroupsParamsTWServer struct { + VersionCode string + ServerKey string +} + type ListGroupsParams struct { - ServerIDs []string - EnabledNotifications NullBool // check if ChannelGains != null && ChannelLosses != null + ServerIDs []string // DC server IDs + Servers []ListGroupsParamsTWServer // version codes + tw server keys + 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..f327336 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,88 @@ 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.cleanUpOldWithDisabledNotifications(ctx); err != nil { + return err } - if !server.Open { - return domain.ServerIsClosedError{ - VersionCode: versionCode, - Key: serverKey, + if err := g.cleanUpOldWithClosedTWServers(ctx); err != nil { + return err + } + + return nil +} + +func (g *Group) cleanUpOldWithDisabledNotifications(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) cleanUpOldWithClosedTWServers(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 + } + + params := domain.ListGroupsParams{ + Servers: make([]domain.ListGroupsParamsTWServer, 0, len(servers)), + } + for _, s := range servers { + params.Servers = append(params.Servers, domain.ListGroupsParamsTWServer{ + VersionCode: v.Code, + ServerKey: 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 + } + + 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..73737cd 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(), 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 +}