refactor: group & monitor refactor (#107)
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #107
This commit is contained in:
Dawid Wysokiński 2023-06-18 06:47:51 +00:00
parent e6d15ac950
commit e110807619
30 changed files with 2885 additions and 2403 deletions

View File

@ -47,36 +47,23 @@ func New() *cli.Command {
} }
groupRepo := bundb.NewGroup(db) groupRepo := bundb.NewGroup(db)
monitorRepo := bundb.NewMonitor(db)
choiceSvc := service.NewChoice(client) choiceSvc := service.NewChoice(client)
groupSvc := service.NewGroup( groupSvc := service.NewGroup(groupRepo, client, logger, cfg.MaxGroupsPerServer, cfg.MaxMonitorsPerGroup)
groupRepo,
client,
logger,
cfg.MaxGroupsPerServer,
)
monitorSvc := service.NewMonitor(
monitorRepo,
groupRepo,
client,
logger,
cfg.MaxMonitorsPerGroup,
)
bot, err := discord.NewBot(cfg.Token, groupSvc, monitorSvc, choiceSvc, logger) bot, err := discord.NewBot(cfg.Token, groupSvc, choiceSvc, logger)
if err != nil { if err != nil {
return fmt.Errorf("discord.NewBot: %w", err) return fmt.Errorf("discord.NewBot: %w", err)
} }
defer func() {
_ = bot.Close()
}()
go func() { go func() {
if runErr := bot.Run(); runErr != nil { if runErr := bot.Run(); runErr != nil {
logger.Fatal("something went wrong while starting the bot", zap.Error(runErr)) logger.Fatal("something went wrong while starting the bot", zap.Error(runErr))
} }
}() }()
defer func() {
_ = bot.Close()
}()
if err = os.WriteFile(healthyFilePath, []byte("healthy"), healthyFilePerm); err != nil { if err = os.WriteFile(healthyFilePath, []byte("healthy"), healthyFilePerm); err != nil {
return fmt.Errorf("couldn't create file (path=%s): %w", healthyFilePath, err) return fmt.Errorf("couldn't create file (path=%s): %w", healthyFilePath, err)
@ -93,7 +80,7 @@ func New() *cli.Command {
type botConfig struct { type botConfig struct {
Token string `envconfig:"TOKEN" required:"true"` Token string `envconfig:"TOKEN" required:"true"`
MaxGroupsPerServer int `envconfig:"MAX_GROUPS_PER_SERVER" default:"10"` MaxGroupsPerServer int `envconfig:"MAX_GROUPS_PER_SERVER" default:"5"`
MaxMonitorsPerGroup int `envconfig:"MAX_MONITORS_PER_GROUP" default:"10"` MaxMonitorsPerGroup int `envconfig:"MAX_MONITORS_PER_GROUP" default:"10"`
} }

View File

@ -8,11 +8,15 @@ import (
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"golang.org/x/time/rate"
) )
type twhelpClientConfig struct { type twhelpClientConfig struct {
URL *url.URL `envconfig:"URL" required:"true"` URL *url.URL `envconfig:"URL" required:"true"`
Timeout time.Duration `envconfig:"TIMEOUT" default:"10s"` Timeout time.Duration `envconfig:"TIMEOUT" default:"10s"`
RateLimiterEnabled bool `envconfig:"RATE_LIMITER_ENABLED" default:"false"`
RateLimiterMaxRequestsPerSecond float64 `envconfig:"RATE_LIMITER_MAX_REQUESTS_PER_SECOND" default:"10"`
RateLimiterRequestBurst int `envconfig:"RATE_LIMITER_REQUEST_BURST" default:"5"`
} }
func NewTWHelpClient(version string) (*twhelp.Client, error) { func NewTWHelpClient(version string) (*twhelp.Client, error) {
@ -21,11 +25,16 @@ func NewTWHelpClient(version string) (*twhelp.Client, error) {
return nil, fmt.Errorf("envconfig.Process: %w", err) return nil, fmt.Errorf("envconfig.Process: %w", err)
} }
return twhelp.NewClient( opts := []twhelp.ClientOption{
cfg.URL,
twhelp.WithHTTPClient(&http.Client{ twhelp.WithHTTPClient(&http.Client{
Timeout: cfg.Timeout, Timeout: cfg.Timeout,
}), }),
twhelp.WithUserAgent("TWHelpDCBot/"+version), twhelp.WithUserAgent("TWHelpDCBot/" + version),
), nil }
if cfg.RateLimiterEnabled {
opts = append(opts, twhelp.WithRateLimiter(rate.NewLimiter(rate.Limit(cfg.RateLimiterMaxRequestsPerSecond), cfg.RateLimiterRequestBurst)))
}
return twhelp.NewClient(cfg.URL, opts...), nil
} }

1
go.mod
View File

@ -18,6 +18,7 @@ require (
github.com/uptrace/bun/driver/pgdriver v1.1.14 github.com/uptrace/bun/driver/pgdriver v1.1.14
github.com/urfave/cli/v2 v2.25.5 github.com/urfave/cli/v2 v2.25.5
go.uber.org/zap v1.24.0 go.uber.org/zap v1.24.0
golang.org/x/time v0.3.0
) )
require ( require (

2
go.sum
View File

@ -147,6 +147,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=

View File

@ -149,7 +149,7 @@ func loadFixtures(tb testing.TB, bunDB *bun.DB) *bunfixture {
return &bunfixture{fixture} return &bunfixture{fixture}
} }
func (f *bunfixture) group(tb testing.TB, id string) domain.Group { func (f *bunfixture) group(tb testing.TB, id string) domain.GroupWithMonitors {
tb.Helper() tb.Helper()
row, err := f.Row("Group." + id) row, err := f.Row("Group." + id)
@ -159,13 +159,13 @@ func (f *bunfixture) group(tb testing.TB, id string) domain.Group {
return g.ToDomain() return g.ToDomain()
} }
func (f *bunfixture) groups(tb testing.TB) []domain.Group { func (f *bunfixture) groups(tb testing.TB) []domain.GroupWithMonitors {
tb.Helper() tb.Helper()
//nolint:lll //nolint:lll
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"} 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([]domain.Group, 0, len(ids)) groups := make([]domain.GroupWithMonitors, 0, len(ids))
for _, id := range ids { for _, id := range ids {
groups = append(groups, f.group(tb, id)) groups = append(groups, f.group(tb, id))
} }
@ -173,6 +173,16 @@ func (f *bunfixture) groups(tb testing.TB) []domain.Group {
return groups return groups
} }
func (f *bunfixture) monitor(tb testing.TB, id string) domain.Monitor {
tb.Helper()
row, err := f.Row("Monitor." + id)
require.NoError(tb, err)
m, ok := row.(*model.Monitor)
require.True(tb, ok)
return m.ToDomain()
}
func generateSchema() string { func generateSchema() string {
return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber) return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber)
} }

View File

@ -9,7 +9,9 @@ import (
"gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model" "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgerrcode"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/uptrace/bun/driver/pgdriver"
) )
type Group struct { type Group struct {
@ -20,7 +22,7 @@ func NewGroup(db *bun.DB) *Group {
return &Group{db: db} return &Group{db: db}
} }
func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) { func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error) {
group := model.Group{ group := model.Group{
ServerID: params.ServerID(), ServerID: params.ServerID(),
VersionCode: params.VersionCode(), VersionCode: params.VersionCode(),
@ -35,52 +37,103 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do
Model(&group). Model(&group).
Returning("*"). Returning("*").
Exec(ctx); err != nil { Exec(ctx); err != nil {
return domain.Group{}, fmt.Errorf("something went wrong while inserting group into the db: %w", err) return domain.GroupWithMonitors{}, fmt.Errorf("something went wrong while inserting group into the db: %w", err)
} }
return group.ToDomain(), nil return group.ToDomain(), nil
} }
func (g *Group) Update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.Group, error) { func (g *Group) Update(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) {
if params.IsZero() { if params.IsZero() {
return domain.Group{}, domain.ErrNothingToUpdate return domain.GroupWithMonitors{}, domain.ErrNothingToUpdate
} }
if _, err := uuid.Parse(id); err != nil { if _, err := uuid.Parse(id); err != nil {
return domain.Group{}, domain.GroupNotFoundError{ID: id} return domain.GroupWithMonitors{}, domain.GroupNotFoundError{ID: id}
} }
var group model.Group var group model.Group
res, err := g.db.NewUpdate(). res, err := g.db.NewUpdate().
Model(&group). Model(&group).
Returning("*"). Returning("NULL").
Where("id = ?", id). Where("id = ?", id).
Where("server_id = ?", serverID).
Apply(updateGroupsParamsApplier{params}.apply). Apply(updateGroupsParamsApplier{params}.apply).
Exec(ctx) Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) { if err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.Group{}, fmt.Errorf("couldn't update group (id=%s,serverID=%s): %w", id, serverID, err) return domain.GroupWithMonitors{}, fmt.Errorf("couldn't update group (id=%s): %w", id, err)
} }
if affected, _ := res.RowsAffected(); affected == 0 { if affected, _ := res.RowsAffected(); affected == 0 {
return domain.Group{}, domain.GroupNotFoundError{ID: id} return domain.GroupWithMonitors{}, domain.GroupNotFoundError{ID: id}
} }
return group.ToDomain(), nil return g.Get(ctx, id)
} }
func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) { func (g *Group) AddMonitor(ctx context.Context, id string, tribeID int64) (domain.GroupWithMonitors, error) {
parsedID, err := uuid.Parse(id)
if err != nil {
return domain.GroupWithMonitors{}, domain.GroupDoesNotExistError{ID: id}
}
monitor := model.Monitor{
GroupID: parsedID,
TribeID: tribeID,
}
if _, err = g.db.NewInsert().
Model(&monitor).
Returning("NULL").
Exec(ctx); err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf(
"something went wrong while inserting monitor into the db: %w",
mapAddMonitorError(err, id, tribeID),
)
}
return g.Get(ctx, id)
}
func (g *Group) DeleteMonitors(ctx context.Context, id string, monitorIDs ...string) (domain.GroupWithMonitors, error) {
if _, err := uuid.Parse(id); err != nil {
return domain.GroupWithMonitors{}, domain.GroupNotFoundError{ID: id}
}
for _, monitorID := range monitorIDs {
if _, err := uuid.Parse(monitorID); err != nil {
return domain.GroupWithMonitors{}, domain.MonitorNotFoundError{ID: monitorID}
}
}
_, err := g.db.NewDelete().
Model(&model.Monitor{}).
Returning("NULL").
Where("id IN (?)", bun.In(monitorIDs)).
Where("group_id = ?", id).
Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.GroupWithMonitors{}, fmt.Errorf("couldn't delete monitors: %w", err)
}
return g.Get(ctx, id)
}
func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.GroupWithMonitors, error) {
var groups []model.Group var groups []model.Group
if err := g.db.NewSelect(). if err := g.db.NewSelect().
Model(&groups). Model(&groups).
Order("created_at ASC"). Order("created_at ASC").
Relation("Monitors", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Order("created_at ASC")
}).
Apply(listGroupsParamsApplier{params}.apply). Apply(listGroupsParamsApplier{params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select groups from the db: %w", err) return nil, fmt.Errorf("couldn't select groups from the db: %w", err)
} }
result := make([]domain.Group, 0, len(groups)) result := make([]domain.GroupWithMonitors, 0, len(groups))
for _, group := range groups { for _, group := range groups {
result = append(result, group.ToDomain()) result = append(result, group.ToDomain())
} }
@ -88,29 +141,41 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom
return result, nil return result, nil
} }
func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) { func (g *Group) Get(ctx context.Context, id string) (domain.GroupWithMonitors, error) {
group, err := g.get(ctx, id, true)
if err != nil {
return domain.GroupWithMonitors{}, err
}
return group.ToDomain(), nil
}
func (g *Group) get(ctx context.Context, id string, withMonitors bool) (model.Group, error) {
if _, err := uuid.Parse(id); err != nil { if _, err := uuid.Parse(id); err != nil {
return domain.Group{}, domain.GroupNotFoundError{ID: id} return model.Group{}, domain.GroupNotFoundError{ID: id}
} }
var group model.Group var group model.Group
err := g.db.NewSelect(). q := g.db.NewSelect().
Model(&group). Model(&group).
Where("id = ?", id). Where("id = ?", id)
Where("server_id = ?", serverID). if withMonitors {
Scan(ctx) q = q.Relation("Monitors", func(q *bun.SelectQuery) *bun.SelectQuery {
if err != nil { return q.Order("created_at ASC")
if errors.Is(err, sql.ErrNoRows) { })
return domain.Group{}, domain.GroupNotFoundError{ID: id}
}
return domain.Group{}, fmt.Errorf("couldn't select group (id=%s,serverID=%s) from the db: %w", id, serverID, err)
} }
return group.ToDomain(), nil if err := q.Scan(ctx); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return model.Group{}, domain.GroupNotFoundError{ID: id}
}
return model.Group{}, fmt.Errorf("couldn't select group (id=%s) from the db: %w", id, err)
}
return group, nil
} }
func (g *Group) Delete(ctx context.Context, id, serverID string) error { func (g *Group) Delete(ctx context.Context, id string) error {
if _, err := uuid.Parse(id); err != nil { if _, err := uuid.Parse(id); err != nil {
return domain.GroupNotFoundError{ID: id} return domain.GroupNotFoundError{ID: id}
} }
@ -119,14 +184,14 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error {
Model(&model.Group{}). Model(&model.Group{}).
Returning("NULL"). Returning("NULL").
Where("id = ?", id). Where("id = ?", id).
Where("server_id = ?", serverID).
Exec(ctx) Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) { if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("couldn't delete group (id=%s,serverID=%s): %w", id, serverID, err) return fmt.Errorf("couldn't delete group (id=%s): %w", id, err)
} }
if affected, _ := res.RowsAffected(); affected == 0 { if affected, _ := res.RowsAffected(); affected == 0 {
return domain.GroupNotFoundError{ID: id} return domain.GroupNotFoundError{ID: id}
} }
return nil return nil
} }
@ -183,7 +248,7 @@ type listGroupsParamsApplier struct {
} }
func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if l.params.ServerIDs != nil { if len(l.params.ServerIDs) > 0 {
q = q.Where("server_id IN (?)", bun.In(l.params.ServerIDs)) q = q.Where("server_id IN (?)", bun.In(l.params.ServerIDs))
} }
@ -202,7 +267,7 @@ func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("version_code = ?", l.params.VersionCode.String) q = q.Where("version_code = ?", l.params.VersionCode.String)
} }
if l.params.ServerKeys != nil { if len(l.params.ServerKeys) > 0 {
q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys)) q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys))
} }
@ -212,3 +277,26 @@ func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
return q return q
} }
func mapAddMonitorError(err error, groupID string, tribeID int64) error {
var pgError pgdriver.Error
if !errors.As(err, &pgError) {
return err
}
code := pgError.Field('C')
constraint := pgError.Field('n')
switch {
case code == pgerrcode.ForeignKeyViolation && constraint == "monitors_group_id_fkey":
return domain.GroupDoesNotExistError{
ID: groupID,
}
case code == pgerrcode.UniqueViolation && constraint == "monitors_group_id_tribe_id_key":
return domain.MonitorAlreadyExistsError{
TribeID: tribeID,
GroupID: groupID,
}
default:
return err
}
}

View File

@ -37,8 +37,7 @@ func TestGroup_Create(t *testing.T) {
group, err := repo.Create(context.Background(), params) group, err := repo.Create(context.Background(), params)
assert.NoError(t, err) assert.NoError(t, err)
_, err = uuid.Parse(group.ID) assert.NotZero(t, group.ID)
assert.NoError(t, err)
assert.Equal(t, params.ServerID(), group.ServerID) assert.Equal(t, params.ServerID(), group.ServerID)
assert.Equal(t, params.ServerKey(), group.ServerKey) assert.Equal(t, params.ServerKey(), group.ServerKey)
assert.Equal(t, params.VersionCode(), group.VersionCode) assert.Equal(t, params.VersionCode(), group.VersionCode)
@ -84,7 +83,7 @@ func TestGroup_Update(t *testing.T) {
}, },
} }
updatedGroup, err := repo.Update(context.Background(), group.ID, group.ServerID, params) updatedGroup, err := repo.Update(context.Background(), group.ID, params)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, params.ChannelGains.String, updatedGroup.ChannelGains) assert.Equal(t, params.ChannelGains.String, updatedGroup.ChannelGains)
assert.Equal(t, params.ChannelLosses.String, updatedGroup.ChannelLosses) assert.Equal(t, params.ChannelLosses.String, updatedGroup.ChannelLosses)
@ -95,7 +94,7 @@ func TestGroup_Update(t *testing.T) {
t.Run("ERR: nothing to update", func(t *testing.T) { t.Run("ERR: nothing to update", func(t *testing.T) {
t.Parallel() t.Parallel()
updatedGroup, err := repo.Update(context.Background(), "", "", domain.UpdateGroupParams{}) updatedGroup, err := repo.Update(context.Background(), "", domain.UpdateGroupParams{})
assert.ErrorIs(t, err, domain.ErrNothingToUpdate) assert.ErrorIs(t, err, domain.ErrNothingToUpdate)
assert.Zero(t, updatedGroup) assert.Zero(t, updatedGroup)
}) })
@ -106,22 +105,14 @@ func TestGroup_Update(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
id string id string
serverID string
}{ }{
{ {
name: "ID - not UUID", name: "ID - not UUID",
id: "test", id: "test",
serverID: group.ServerID,
}, },
{ {
name: "ID - random UUID", name: "ID - random UUID",
id: uuid.NewString(), id: uuid.NewString(),
serverID: group.ServerID,
},
{
name: "ServerID - random UUID",
id: group.ID,
serverID: uuid.NewString(),
}, },
} }
@ -131,7 +122,7 @@ func TestGroup_Update(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
updatedGroup, err := repo.Update(context.Background(), tt.id, tt.serverID, domain.UpdateGroupParams{ updatedGroup, err := repo.Update(context.Background(), tt.id, domain.UpdateGroupParams{
ChannelGains: domain.NullString{ ChannelGains: domain.NullString{
String: "update", String: "update",
Valid: true, Valid: true,
@ -144,6 +135,168 @@ func TestGroup_Update(t *testing.T) {
}) })
} }
func TestGroup_AddMonitor(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewGroup(db)
group := fixture.group(t, "group-1-server-1")
t.Run("OK", func(t *testing.T) {
t.Parallel()
var tribeID int64 = 91283718
updatedGroup, err := repo.AddMonitor(context.Background(), group.ID, tribeID)
assert.NoError(t, err)
var found bool
for _, m := range updatedGroup.Monitors {
if m.GroupID == group.ID && m.TribeID == tribeID {
found = true
break
}
}
assert.True(t, found)
})
t.Run("ERR: group doesn't exist", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
}{
{
name: "ID - not UUID",
id: "test",
},
{
name: "ID - random UUID",
id: uuid.NewString(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
updatedGroup, err := repo.AddMonitor(context.Background(), tt.id, 125)
assert.ErrorIs(t, err, domain.GroupDoesNotExistError{ID: tt.id})
assert.Zero(t, updatedGroup)
})
}
})
t.Run("ERR: monitor already exists", func(t *testing.T) {
t.Parallel()
monitor := fixture.monitor(t, "monitor-2-group-1-server-1")
updatedGroup, err := repo.AddMonitor(context.Background(), monitor.GroupID, monitor.TribeID)
assert.ErrorIs(t, err, domain.MonitorAlreadyExistsError{
TribeID: monitor.TribeID,
GroupID: monitor.GroupID,
})
assert.Zero(t, updatedGroup)
})
}
func TestGroup_DeleteMonitors(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewGroup(db)
t.Run("OK", func(t *testing.T) {
t.Parallel()
monitor := fixture.monitor(t, "monitor-1-group-1-server-1")
updatedGroup, err := repo.DeleteMonitors(context.Background(), monitor.GroupID, monitor.ID)
assert.NoError(t, err)
var found bool
for _, m := range updatedGroup.Monitors {
if m.ID == monitor.ID {
found = true
break
}
}
assert.False(t, found)
})
t.Run("ERR: group not found", func(t *testing.T) {
t.Parallel()
monitor := fixture.monitor(t, "monitor-2-group-1-server-1")
tests := []struct {
name string
id string
}{
{
name: "group ID - not UUID",
id: "id",
},
{
name: "group ID - not UUID",
id: uuid.NewString(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
updatedGroup, err := repo.DeleteMonitors(context.Background(), tt.id, monitor.ID)
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id})
assert.Zero(t, updatedGroup)
})
}
})
t.Run("ERR: monitor not found", func(t *testing.T) {
t.Parallel()
monitor := fixture.monitor(t, "monitor-2-group-1-server-1")
tests := []struct {
name string
monitorID string
}{
{
name: "monitor ID - not UUID",
monitorID: "id",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
updatedGroup, err := repo.DeleteMonitors(context.Background(), monitor.GroupID, tt.monitorID)
assert.ErrorIs(t, err, domain.MonitorNotFoundError{ID: tt.monitorID})
assert.Zero(t, updatedGroup)
})
}
})
}
func TestGroup_List(t *testing.T) { func TestGroup_List(t *testing.T) {
t.Parallel() t.Parallel()
@ -299,9 +452,9 @@ func TestGroup_Get(t *testing.T) {
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
g, err := repo.Get(context.Background(), group.ID, group.ServerID) g, err := repo.Get(context.Background(), group.ID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, group, g) assert.Equal(t, group.ID, g.ID)
}) })
t.Run("ERR: group not found (unknown ID)", func(t *testing.T) { t.Run("ERR: group not found (unknown ID)", func(t *testing.T) {
@ -310,22 +463,14 @@ func TestGroup_Get(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
id string id string
serverID string
}{ }{
{ {
name: "ID - not UUID", name: "ID - not UUID",
id: "test", id: "test",
serverID: group.ServerID,
}, },
{ {
name: "ID - random UUID", name: "ID - random UUID",
id: uuid.NewString(), id: uuid.NewString(),
serverID: group.ServerID,
},
{
name: "ServerID - random UUID",
id: group.ID,
serverID: uuid.NewString(),
}, },
} }
@ -335,7 +480,7 @@ func TestGroup_Get(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
g, err := repo.Get(context.Background(), tt.id, tt.serverID) g, err := repo.Get(context.Background(), tt.id)
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id}) assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id})
assert.Zero(t, g) assert.Zero(t, g)
}) })
@ -353,7 +498,6 @@ func TestGroup_Delete(t *testing.T) {
db := newDB(t) db := newDB(t)
fixture := loadFixtures(t, db) fixture := loadFixtures(t, db)
groupRepo := bundb.NewGroup(db) groupRepo := bundb.NewGroup(db)
monitorRepo := bundb.NewMonitor(db)
group := fixture.group(t, "group-1-server-1") group := fixture.group(t, "group-1-server-1")
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
@ -364,18 +508,13 @@ func TestGroup_Delete(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, groupRepo.Delete(context.Background(), group.ID, group.ServerID)) assert.NoError(t, groupRepo.Delete(context.Background(), group.ID))
afterDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{ afterDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{
ServerIDs: []string{group.ServerID}, ServerIDs: []string{group.ServerID},
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, afterDelete, len(beforeDelete)-1) assert.Len(t, afterDelete, len(beforeDelete)-1)
// monitors should also be deleted
monitors, err := monitorRepo.List(context.Background(), group.ID)
assert.NoError(t, err)
assert.Len(t, monitors, 0)
}) })
t.Run("ERR: group not found", func(t *testing.T) { t.Run("ERR: group not found", func(t *testing.T) {
@ -384,22 +523,14 @@ func TestGroup_Delete(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
id string id string
serverID string
}{ }{
{ {
name: "ID - not UUID", name: "ID - not UUID",
id: "test", id: "test",
serverID: group.ServerID,
}, },
{ {
name: "ID - random UUID", name: "ID - random UUID",
id: uuid.NewString(), id: uuid.NewString(),
serverID: group.ServerID,
},
{
name: "ServerID - random UUID",
id: group.ID,
serverID: uuid.NewString(),
}, },
} }
@ -409,11 +540,7 @@ func TestGroup_Delete(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
assert.ErrorIs( assert.ErrorIs(t, groupRepo.Delete(context.Background(), tt.id), domain.GroupNotFoundError{ID: tt.id})
t,
groupRepo.Delete(context.Background(), tt.id, tt.serverID),
domain.GroupNotFoundError{ID: tt.id},
)
}) })
} }
}) })
@ -429,7 +556,6 @@ func TestGroup_DeleteMany(t *testing.T) {
db := newDB(t) db := newDB(t)
fixture := loadFixtures(t, db) fixture := loadFixtures(t, db)
groupRepo := bundb.NewGroup(db) groupRepo := bundb.NewGroup(db)
monitorRepo := bundb.NewMonitor(db)
group1 := fixture.group(t, "group-1-server-1") group1 := fixture.group(t, "group-1-server-1")
group2 := fixture.group(t, "group-2-server-1") group2 := fixture.group(t, "group-2-server-1")
ids := []string{group1.ID, group2.ID} ids := []string{group1.ID, group2.ID}
@ -450,13 +576,6 @@ func TestGroup_DeleteMany(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, afterDelete, len(beforeDelete)-len(ids)) 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.Run("OK: 0 ids", func(t *testing.T) {

View File

@ -20,10 +20,16 @@ type Group struct {
ServerKey string `bun:"server_key,nullzero"` ServerKey string `bun:"server_key,nullzero"`
VersionCode string `bun:"version_code,nullzero"` VersionCode string `bun:"version_code,nullzero"`
CreatedAt time.Time `bun:"created_at,nullzero"` CreatedAt time.Time `bun:"created_at,nullzero"`
Monitors []Monitor `bun:"monitors,rel:has-many,join:id=group_id"`
} }
func (g Group) ToDomain() domain.Group { func (g Group) ToDomain() domain.GroupWithMonitors {
return domain.Group{ monitors := make([]domain.Monitor, 0, len(g.Monitors))
for _, m := range g.Monitors {
monitors = append(monitors, m.ToDomain())
}
return domain.GroupWithMonitors{
Group: domain.Group{
ID: g.ID.String(), ID: g.ID.String(),
ServerID: g.ServerID, ServerID: g.ServerID,
ChannelGains: g.ChannelGains, ChannelGains: g.ChannelGains,
@ -33,5 +39,7 @@ func (g Group) ToDomain() domain.Group {
ServerKey: g.ServerKey, ServerKey: g.ServerKey,
VersionCode: g.VersionCode, VersionCode: g.VersionCode,
CreatedAt: g.CreatedAt, CreatedAt: g.CreatedAt,
},
Monitors: monitors,
} }
} }

View File

@ -1,137 +0,0 @@
package bundb
import (
"context"
"database/sql"
"errors"
"fmt"
"gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid"
"github.com/jackc/pgerrcode"
"github.com/uptrace/bun"
"github.com/uptrace/bun/driver/pgdriver"
)
type Monitor struct {
db *bun.DB
}
func NewMonitor(db *bun.DB) *Monitor {
return &Monitor{db: db}
}
func (m *Monitor) Create(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) {
groupID, err := uuid.Parse(params.GroupID())
if err != nil {
return domain.Monitor{}, domain.GroupDoesNotExistError{
ID: params.GroupID(),
}
}
monitor := model.Monitor{
GroupID: groupID,
TribeID: params.TribeID(),
}
if _, err = m.db.NewInsert().
Model(&monitor).
Returning("*").
Exec(ctx); err != nil {
return domain.Monitor{}, fmt.Errorf(
"something went wrong while inserting monitor into the db: %w",
mapCreateMonitorError(err, params),
)
}
return monitor.ToDomain(), nil
}
func (m *Monitor) List(ctx context.Context, groupID string) ([]domain.Monitor, error) {
if _, err := uuid.Parse(groupID); err != nil {
return nil, nil
}
var monitors []model.Monitor
if err := m.db.NewSelect().
Model(&monitors).
Order("created_at ASC").
Where("group_id = ?", groupID).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select monitors from the db: %w", err)
}
result := make([]domain.Monitor, 0, len(monitors))
for _, monitor := range monitors {
result = append(result, monitor.ToDomain())
}
return result, nil
}
func (m *Monitor) Get(ctx context.Context, id string) (domain.Monitor, error) {
if _, err := uuid.Parse(id); err != nil {
return domain.Monitor{}, domain.MonitorNotFoundError{ID: id}
}
var monitor model.Monitor
err := m.db.NewSelect().
Model(&monitor).
Where("id = ?", id).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.Monitor{}, domain.MonitorNotFoundError{ID: id}
}
return domain.Monitor{}, fmt.Errorf("couldn't select monitor from the db (id=%s): %w", id, err)
}
return monitor.ToDomain(), nil
}
func (m *Monitor) Delete(ctx context.Context, id string) error {
if _, err := uuid.Parse(id); err != nil {
return domain.MonitorNotFoundError{ID: id}
}
res, err := m.db.NewDelete().
Model(&model.Monitor{}).
Returning("NULL").
Where("id = ?", id).
Exec(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("couldn't delete monitor (id=%s): %w", id, err)
}
if affected, _ := res.RowsAffected(); affected == 0 {
return domain.MonitorNotFoundError{ID: id}
}
return nil
}
func mapCreateMonitorError(err error, params domain.CreateMonitorParams) error {
var pgError pgdriver.Error
if !errors.As(err, &pgError) {
return err
}
code := pgError.Field('C')
constraint := pgError.Field('n')
switch {
case code == pgerrcode.ForeignKeyViolation && constraint == "monitors_group_id_fkey":
return domain.GroupDoesNotExistError{
ID: params.GroupID(),
}
case code == pgerrcode.UniqueViolation && constraint == "monitors_group_id_tribe_id_key":
return domain.MonitorAlreadyExistsError{
TribeID: params.TribeID(),
GroupID: params.GroupID(),
}
default:
return err
}
}

View File

@ -1,262 +0,0 @@
package bundb_test
import (
"context"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/bundb"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMonitor_Create(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewMonitor(db)
t.Run("OK", func(t *testing.T) {
t.Parallel()
group := fixture.group(t, "group-1-server-1")
params, err := domain.NewCreateMonitorParams(group.ID, 1234)
require.Zero(t, err)
monitor, err := repo.Create(context.Background(), params)
assert.NoError(t, err)
_, err = uuid.Parse(monitor.ID)
assert.NoError(t, err)
assert.Equal(t, params.GroupID(), monitor.GroupID)
assert.Equal(t, params.TribeID(), monitor.TribeID)
assert.WithinDuration(t, time.Now(), monitor.CreatedAt, 1*time.Second)
// check unique index
monitor, err = repo.Create(context.Background(), params)
assert.ErrorIs(t, err, domain.MonitorAlreadyExistsError{
TribeID: params.TribeID(),
GroupID: params.GroupID(),
})
assert.Zero(t, monitor)
})
t.Run("ERR: group doesn't exist", func(t *testing.T) {
t.Parallel()
t.Run("foreign key violation", func(t *testing.T) {
t.Parallel()
groupID := uuid.NewString()
params, err := domain.NewCreateMonitorParams(groupID, 1234)
require.Zero(t, err)
monitor, err := repo.Create(context.Background(), params)
assert.ErrorIs(t, err, domain.GroupDoesNotExistError{
ID: groupID,
})
assert.Zero(t, monitor)
})
t.Run("not UUID", func(t *testing.T) {
t.Parallel()
groupID := "592292203234328587"
params, err := domain.NewCreateMonitorParams(groupID, 1234)
require.Zero(t, err)
monitor, err := repo.Create(context.Background(), params)
assert.ErrorIs(t, err, domain.GroupDoesNotExistError{
ID: groupID,
})
assert.Zero(t, monitor)
})
})
}
func TestMonitor_List(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewMonitor(db)
tests := []struct {
name string
groupID string
expectedMonitors []string
}{
{
name: "group-1-server-1",
groupID: fixture.group(t, "group-1-server-1").ID,
expectedMonitors: []string{
"e3017ba8-4fba-4bb1-ac8d-e4e9477a04d2",
"89719460-58ab-46b8-8682-46f161546949",
},
},
{
name: "group-2-server-2",
groupID: fixture.group(t, "group-2-server-2").ID,
expectedMonitors: []string{
"c7d63c3d-55f6-432e-b9d8-006f8c5ab407",
},
},
{
name: "random UUID",
groupID: uuid.NewString(),
expectedMonitors: nil,
},
{
name: "not UUID",
groupID: "test",
expectedMonitors: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := repo.List(context.Background(), tt.groupID)
assert.NoError(t, err)
assert.Len(t, res, len(tt.expectedMonitors))
for _, id := range tt.expectedMonitors {
found := false
for _, m := range res {
if m.ID == id {
found = true
break
}
}
assert.True(t, found, "monitor (id=%s) not found", id)
}
})
}
}
func TestMonitor_Get(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewMonitor(db)
group := fixture.group(t, "group-1-server-1")
t.Run("OK", func(t *testing.T) {
t.Parallel()
monitors, err := repo.List(context.Background(), group.ID)
require.NoError(t, err)
require.Greater(t, len(monitors), 0)
m, err := repo.Get(context.Background(), monitors[0].ID)
assert.NoError(t, err)
assert.Equal(t, monitors[0], m)
})
t.Run("ERR: monitor not found", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
}{
{
name: "ID - not UUID",
id: "test",
},
{
name: "ID - random UUID",
id: uuid.NewString(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
monitor, err := repo.Get(context.Background(), tt.id)
assert.ErrorIs(t, err, domain.MonitorNotFoundError{ID: tt.id})
assert.Zero(t, monitor)
})
}
})
}
func TestMonitor_Delete(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewMonitor(db)
group := fixture.group(t, "group-1-server-1")
t.Run("OK", func(t *testing.T) {
t.Parallel()
beforeDelete, err := repo.List(context.Background(), group.ID)
assert.NoError(t, err)
assert.Greater(t, len(beforeDelete), 0)
assert.NoError(t, repo.Delete(context.Background(), beforeDelete[0].ID))
afterDelete, err := repo.List(context.Background(), group.ID)
assert.NoError(t, err)
assert.Len(t, afterDelete, len(beforeDelete)-1)
})
t.Run("ERR: monitor not found", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
}{
{
name: "ID - not UUID",
id: "test",
},
{
name: "ID - random UUID",
id: uuid.NewString(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.ErrorIs(
t,
repo.Delete(context.Background(), tt.id),
domain.MonitorNotFoundError{ID: tt.id},
)
})
}
})
}

View File

@ -12,20 +12,17 @@ import (
) )
type GroupService interface { type GroupService interface {
Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error)
SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error) AddMonitor(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error)
SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error) DeleteMonitor(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error)
SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error)
SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error) SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error)
CleanUp(ctx context.Context) error SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.GroupWithMonitors, error)
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.GroupWithMonitors, error)
Delete(ctx context.Context, id, serverID string) error
}
type MonitorService interface {
Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error)
List(ctx context.Context, groupID, serverID string) ([]domain.MonitorWithTribe, error)
Execute(ctx context.Context) ([]domain.EnnoblementNotification, error) Execute(ctx context.Context) ([]domain.EnnoblementNotification, error)
CleanUp(ctx context.Context) error
ListServer(ctx context.Context, serverID string) ([]domain.GroupWithMonitors, error)
GetWithTribes(ctx context.Context, id, serverID string) (domain.GroupWithMonitorsAndTribes, error)
Delete(ctx context.Context, id, serverID string) error Delete(ctx context.Context, id, serverID string) error
} }
@ -37,7 +34,6 @@ type Bot struct {
s *discordgo.Session s *discordgo.Session
c *cron.Cron c *cron.Cron
groupSvc GroupService groupSvc GroupService
monitorSvc MonitorService
choiceSvc ChoiceService choiceSvc ChoiceService
logger *zap.Logger logger *zap.Logger
} }
@ -45,7 +41,6 @@ type Bot struct {
func NewBot( func NewBot(
token string, token string,
groupSvc GroupService, groupSvc GroupService,
monitorSvc MonitorService,
client ChoiceService, client ChoiceService,
logger *zap.Logger, logger *zap.Logger,
) (*Bot, error) { ) (*Bot, error) {
@ -65,7 +60,6 @@ func NewBot(
), ),
), ),
groupSvc: groupSvc, groupSvc: groupSvc,
monitorSvc: monitorSvc,
choiceSvc: client, choiceSvc: client,
logger: logger, logger: logger,
} }
@ -103,7 +97,6 @@ type command interface {
func (b *Bot) registerCommands() error { func (b *Bot) registerCommands() error {
commands := []command{ commands := []command{
&groupCommand{groupSvc: b.groupSvc, choiceSvc: b.choiceSvc}, &groupCommand{groupSvc: b.groupSvc, choiceSvc: b.choiceSvc},
&monitorCommand{svc: b.monitorSvc},
} }
for _, c := range commands { for _, c := range commands {
@ -129,7 +122,7 @@ func (b *Bot) initCron() error {
}{ }{
{ {
spec: "@every 1m", spec: "@every 1m",
job: &executeMonitorsJob{svc: b.monitorSvc, s: b.s, logger: b.logger}, job: &executeMonitorsJob{svc: b.groupSvc, s: b.s, logger: b.logger},
}, },
{ {
spec: "0 */8 * * *", spec: "0 */8 * * *",
@ -159,7 +152,8 @@ func (b *Bot) logCommands(_ *discordgo.Session, i *discordgo.InteractionCreate)
cmdData := i.ApplicationCommandData() cmdData := i.ApplicationCommandData()
cmd := cmdData.Name cmd := cmdData.Name
options := cmdData.Options options := cmdData.Options
for len(options) > 0 && options[0].Type == discordgo.ApplicationCommandOptionSubCommand { for len(options) > 0 &&
(options[0].Type == discordgo.ApplicationCommandOptionSubCommand || options[0].Type == discordgo.ApplicationCommandOptionSubCommandGroup) {
cmd += " " + options[0].Name cmd += " " + options[0].Name
options = options[0].Options options = options[0].Options
} }

View File

@ -3,12 +3,17 @@ package discord
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
const (
tribeTagMaxLength = 10
)
type groupCommand struct { type groupCommand struct {
groupSvc GroupService groupSvc GroupService
choiceSvc ChoiceService choiceSvc ChoiceService
@ -98,9 +103,69 @@ func (c *groupCommand) create(s *discordgo.Session) error {
Description: "Lists all created groups", Description: "Lists all created groups",
Type: discordgo.ApplicationCommandOptionSubCommand, Type: discordgo.ApplicationCommandOptionSubCommand,
}, },
{
Name: "details",
Description: "Displays group details (including tribes added to the group)",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "group",
Description: "Group ID",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
},
},
{
Name: "monitor",
Description: "Manages monitors",
Type: discordgo.ApplicationCommandOptionSubCommandGroup,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "add",
Description: "Adds a new monitor to a group",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "group",
Description: "Group ID",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
{
Name: "tag",
Description: "Tribe tag",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
MaxLength: tribeTagMaxLength,
},
},
},
{
Name: "delete",
Description: "Deletes a monitor",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "group",
Description: "Group ID",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
{
Name: "tag",
Description: "Tribe tag",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
MaxLength: tribeTagMaxLength,
},
},
},
},
},
{ {
Name: "set", Name: "set",
Description: "Sets various properties in a group configuration", Description: "Sets various properties in group configuration",
Type: discordgo.ApplicationCommandOptionSubCommandGroup, Type: discordgo.ApplicationCommandOptionSubCommandGroup,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{ {
@ -266,29 +331,27 @@ func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCrea
return return
} }
ctx := context.Background()
switch cmdData.Options[0].Name { switch cmdData.Options[0].Name {
case "create": case "create":
c.handleCreate(s, i) c.handleCreate(ctx, s, i)
return
case "list": case "list":
c.handleList(s, i) c.handleList(ctx, s, i)
return case "details":
c.handleDetails(ctx, s, i)
case "set": case "set":
c.handleSet(s, i) c.handleSet(ctx, s, i)
return
case "unset": case "unset":
c.handleUnset(s, i) c.handleUnset(ctx, s, i)
return case "monitor":
c.handleMonitor(ctx, s, i)
case "delete": case "delete":
c.handleDelete(s, i) c.handleDelete(ctx, s, i)
return
default:
} }
} }
func (c *groupCommand) handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleCreate(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
version := "" version := ""
server := "" server := ""
channelGains := "" channelGains := ""
@ -345,17 +408,13 @@ func (c *groupCommand) handleCreate(s *discordgo.Session, i *discordgo.Interacti
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: "group has been successfully created (ID=" + group.ID + ")", Content: "group has been successfully created (id=" + group.ID + ")",
}, },
}) })
} }
func (c *groupCommand) handleList(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background() groups, err := c.groupSvc.ListServer(ctx, i.GuildID)
groups, err := c.groupSvc.List(ctx, domain.ListGroupsParams{
ServerIDs: []string{i.GuildID},
})
if err != nil { if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
@ -379,12 +438,13 @@ func (c *groupCommand) handleList(s *discordgo.Session, i *discordgo.Interaction
fields = append(fields, &discordgo.MessageEmbedField{ fields = append(fields, &discordgo.MessageEmbedField{
Name: g.ID, Name: g.ID,
Value: fmt.Sprintf( Value: fmt.Sprintf(
"**Server**: %s\n**Channel gains**: %s\n**Channel losses**: %s\n**Internals**: %s\n **Barbarians**: %s", "**Server**: %s\n**Channel gains**: %s\n**Channel losses**: %s\n**Internals**: %s\n **Barbarians**: %s\n **Number of monitored monitors**: %d",
g.ServerKey, g.ServerKey,
channelGains, channelGains,
channelLosses, channelLosses,
boolToEmoji(g.Internals), boolToEmoji(g.Internals),
boolToEmoji(g.Barbarians), boolToEmoji(g.Barbarians),
len(g.Monitors),
), ),
Inline: false, Inline: false,
}) })
@ -405,27 +465,75 @@ func (c *groupCommand) handleList(s *discordgo.Session, i *discordgo.Interaction
}) })
} }
func (c *groupCommand) handleSet(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
g, err := c.groupSvc.GetWithTribes(ctx, i.ApplicationCommandData().Options[0].Options[0].StringValue(), i.GuildID)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
},
})
return
}
var builderMonitors strings.Builder
for i, m := range g.Monitors {
if i > 0 {
builderMonitors.WriteString(" ")
}
builderMonitors.WriteString(buildLink(m.Tribe.Tag, m.Tribe.ProfileURL))
}
if builderMonitors.Len() == 0 {
builderMonitors.WriteString("None")
}
channelGains := "Not set"
if g.ChannelGains != "" {
channelGains = buildChannelMention(g.ChannelGains)
}
channelLosses := "Not set"
if g.ChannelLosses != "" {
channelLosses = buildChannelMention(g.ChannelLosses)
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Type: discordgo.EmbedTypeRich,
Title: g.ID,
Description: fmt.Sprintf(
"**Server**: %s\n**Channel gains**: %s\n**Channel losses**: %s\n**Internals**: %s\n **Barbarians**: %s\n **Monitored tribes**: %s",
g.ServerKey,
channelGains,
channelLosses,
boolToEmoji(g.Internals),
boolToEmoji(g.Barbarians),
builderMonitors.String(),
),
Timestamp: formatTimestamp(time.Now()),
},
},
},
})
}
func (c *groupCommand) handleSet(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.ApplicationCommandData().Options[0].Options[0].Name { switch i.ApplicationCommandData().Options[0].Options[0].Name {
case "channel-gains": case "channel-gains":
c.handleSetChannelGains(s, i) c.handleSetChannelGains(ctx, s, i)
return
case "channel-losses": case "channel-losses":
c.handleSetChannelLosses(s, i) c.handleSetChannelLosses(ctx, s, i)
return
case "internals": case "internals":
c.handleSetInternals(s, i) c.handleSetInternals(ctx, s, i)
return
case "barbarians": case "barbarians":
c.handleSetBarbarians(s, i) c.handleSetBarbarians(ctx, s, i)
return
default:
} }
} }
func (c *groupCommand) handleSetChannelGains(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := "" group := ""
channel := "" channel := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options { for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -459,9 +567,7 @@ func (c *groupCommand) handleSetChannelGains(s *discordgo.Session, i *discordgo.
}) })
} }
func (c *groupCommand) handleSetChannelLosses(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleSetChannelLosses(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := "" group := ""
channel := "" channel := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options { for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -495,9 +601,7 @@ func (c *groupCommand) handleSetChannelLosses(s *discordgo.Session, i *discordgo
}) })
} }
func (c *groupCommand) handleSetInternals(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleSetInternals(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := "" group := ""
internals := false internals := false
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options { for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -531,9 +635,7 @@ func (c *groupCommand) handleSetInternals(s *discordgo.Session, i *discordgo.Int
}) })
} }
func (c *groupCommand) handleSetBarbarians(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleSetBarbarians(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := "" group := ""
barbarians := false barbarians := false
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options { for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -567,21 +669,16 @@ func (c *groupCommand) handleSetBarbarians(s *discordgo.Session, i *discordgo.In
}) })
} }
func (c *groupCommand) handleUnset(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleUnset(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.ApplicationCommandData().Options[0].Options[0].Name { switch i.ApplicationCommandData().Options[0].Options[0].Name {
case "channel-gains": case "channel-gains":
c.handleUnsetChannelGains(s, i) c.handleUnsetChannelGains(ctx, s, i)
return
case "channel-losses": case "channel-losses":
c.handleUnsetChannelLosses(s, i) c.handleUnsetChannelLosses(ctx, s, i)
return
default:
} }
} }
func (c *groupCommand) handleUnsetChannelGains(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleUnsetChannelGains(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := i.ApplicationCommandData().Options[0].Options[0].Options[0].StringValue() group := i.ApplicationCommandData().Options[0].Options[0].Options[0].StringValue()
_, err := c.groupSvc.SetChannelGains(ctx, group, i.GuildID, "") _, err := c.groupSvc.SetChannelGains(ctx, group, i.GuildID, "")
if err != nil { if err != nil {
@ -602,9 +699,7 @@ func (c *groupCommand) handleUnsetChannelGains(s *discordgo.Session, i *discordg
}) })
} }
func (c *groupCommand) handleUnsetChannelLosses(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleUnsetChannelLosses(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := i.ApplicationCommandData().Options[0].Options[0].Options[0].StringValue() group := i.ApplicationCommandData().Options[0].Options[0].Options[0].StringValue()
_, err := c.groupSvc.SetChannelLosses(ctx, group, i.GuildID, "") _, err := c.groupSvc.SetChannelLosses(ctx, group, i.GuildID, "")
if err != nil { if err != nil {
@ -625,9 +720,84 @@ func (c *groupCommand) handleUnsetChannelLosses(s *discordgo.Session, i *discord
}) })
} }
func (c *groupCommand) handleDelete(s *discordgo.Session, i *discordgo.InteractionCreate) { func (c *groupCommand) handleMonitor(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background() switch i.ApplicationCommandData().Options[0].Options[0].Name {
case "add":
c.handleMonitorAdd(ctx, s, i)
case "delete":
c.handleMonitorDelete(ctx, s, i)
}
}
func (c *groupCommand) handleMonitorAdd(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
group := ""
tag := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
if opt == nil {
continue
}
switch opt.Name {
case "group":
group = opt.StringValue()
case "tag":
tag = opt.StringValue()
}
}
_, err := c.groupSvc.AddMonitor(ctx, group, i.GuildID, tag)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "monitor has been successfully added",
},
})
}
func (c *groupCommand) handleMonitorDelete(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
group := ""
tag := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
if opt == nil {
continue
}
switch opt.Name {
case "group":
group = opt.StringValue()
case "tag":
tag = opt.StringValue()
}
}
_, err := c.groupSvc.DeleteMonitor(ctx, group, i.GuildID, tag)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "monitor has been successfully deleted",
},
})
}
func (c *groupCommand) handleDelete(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
group := i.ApplicationCommandData().Options[0].Options[0].StringValue() group := i.ApplicationCommandData().Options[0].Options[0].StringValue()
if err := c.groupSvc.Delete(ctx, group, i.GuildID); err != nil { if err := c.groupSvc.Delete(ctx, group, i.GuildID); err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{

View File

@ -1,215 +0,0 @@
package discord
import (
"context"
"fmt"
"time"
"github.com/bwmarrin/discordgo"
)
const (
tribeTagMaxLength = 10
)
type monitorCommand struct {
svc MonitorService
}
func (c *monitorCommand) name() string {
return "monitor"
}
func (c *monitorCommand) register(s *discordgo.Session) error {
if err := c.create(s); err != nil {
return err
}
s.AddHandler(c.handle)
return nil
}
func (c *monitorCommand) create(s *discordgo.Session) error {
var perm int64 = discordgo.PermissionAdministrator
dm := false
_, err := s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{
Name: c.name(),
Description: "Manages monitors",
DefaultMemberPermissions: &perm,
DMPermission: &dm,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "create",
Description: "Creates a new monitor",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "group",
Description: "Group ID",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
{
Name: "tag",
Description: "Tribe tag",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
MaxLength: tribeTagMaxLength,
},
},
},
{
Name: "list",
Description: "Lists all monitors associated with a particular group",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "group",
Description: "Group ID",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
},
},
{
Name: "delete",
Description: "Deletes a monitor",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "monitor",
Description: "Monitor ID",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
},
},
},
})
if err != nil {
return fmt.Errorf("s.ApplicationCommandCreate: %w", err)
}
return nil
}
func (c *monitorCommand) handle(s *discordgo.Session, i *discordgo.InteractionCreate) {
cmdData := i.ApplicationCommandData()
if cmdData.Name != c.name() {
return
}
switch cmdData.Options[0].Name {
case "create":
c.handleCreate(s, i)
return
case "list":
c.handleList(s, i)
return
case "delete":
c.handleDelete(s, i)
return
default:
}
}
func (c *monitorCommand) handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := ""
tag := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options {
if opt == nil {
continue
}
switch opt.Name {
case "group":
group = opt.StringValue()
case "tag":
tag = opt.StringValue()
}
}
monitor, err := c.svc.Create(ctx, group, i.GuildID, tag)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "monitor has been successfully created (ID=" + monitor.ID + ")",
},
})
}
func (c *monitorCommand) handleList(s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
group := i.ApplicationCommandData().Options[0].Options[0].StringValue()
monitors, err := c.svc.List(ctx, group, i.GuildID)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
},
})
return
}
fields := make([]*discordgo.MessageEmbedField, 0, len(monitors))
for _, m := range monitors {
fields = append(fields, &discordgo.MessageEmbedField{
Name: m.ID,
Value: fmt.Sprintf("**Tribe**: %s", buildLink(m.Tribe.Tag, m.Tribe.ProfileURL)),
Inline: false,
})
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Type: discordgo.EmbedTypeRich,
Title: "Monitor list",
Description: "**Group**: " + group,
Fields: fields,
Timestamp: formatTimestamp(time.Now()),
},
},
},
})
}
func (c *monitorCommand) handleDelete(s *discordgo.Session, i *discordgo.InteractionCreate) {
ctx := context.Background()
monitor := i.ApplicationCommandData().Options[0].Options[0].StringValue()
if err := c.svc.Delete(ctx, monitor, i.GuildID); err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "monitor has been successfully deleted",
},
})
}

View File

@ -19,7 +19,7 @@ const (
type executeMonitorsJob struct { type executeMonitorsJob struct {
s *discordgo.Session s *discordgo.Session
svc MonitorService svc GroupService
logger *zap.Logger logger *zap.Logger
} }

View File

@ -3,7 +3,7 @@ package domain
type EnnoblementNotificationType uint8 type EnnoblementNotificationType uint8
const ( const (
EnnoblementNotificationTypeGain = iota EnnoblementNotificationTypeGain EnnoblementNotificationType = iota
EnnoblementNotificationTypeLoss EnnoblementNotificationTypeLoss
) )

View File

@ -17,6 +17,18 @@ type Group struct {
CreatedAt time.Time CreatedAt time.Time
} }
type GroupWithMonitors struct {
Group
Monitors []Monitor
}
type GroupWithMonitorsAndTribes struct {
Group
Monitors []MonitorWithTribe
}
type CreateGroupParams struct { type CreateGroupParams struct {
serverID string serverID string
serverKey string serverKey string
@ -135,7 +147,7 @@ type GroupNotFoundError struct {
} }
func (e GroupNotFoundError) Error() string { func (e GroupNotFoundError) Error() string {
return fmt.Sprintf("group (ID=%s) not found", e.ID) return fmt.Sprintf("group (id=%s) not found", e.ID)
} }
func (e GroupNotFoundError) UserError() string { func (e GroupNotFoundError) UserError() string {
@ -151,7 +163,7 @@ type GroupDoesNotExistError struct {
} }
func (e GroupDoesNotExistError) Error() string { func (e GroupDoesNotExistError) Error() string {
return fmt.Sprintf("group (ID=%s) doesn't exist", e.ID) return fmt.Sprintf("group (id=%s) doesn't exist", e.ID)
} }
func (e GroupDoesNotExistError) UserError() string { func (e GroupDoesNotExistError) UserError() string {

View File

@ -204,7 +204,7 @@ func TestGroupNotFoundError(t *testing.T) {
ID: uuid.NewString(), ID: uuid.NewString(),
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("group (ID=%s) not found", err.ID), err.Error()) assert.Equal(t, fmt.Sprintf("group (id=%s) not found", err.ID), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code()) assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
} }
@ -216,7 +216,7 @@ func TestGroupDoesNotExistError(t *testing.T) {
ID: uuid.NewString(), ID: uuid.NewString(),
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("group (ID=%s) doesn't exist", err.ID), err.Error()) assert.Equal(t, fmt.Sprintf("group (id=%s) doesn't exist", err.ID), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
} }

View File

@ -5,10 +5,6 @@ import (
"time" "time"
) )
const (
tribeIDMin = 1
)
type Monitor struct { type Monitor struct {
ID string ID string
TribeID int64 TribeID int64
@ -21,46 +17,13 @@ type MonitorWithTribe struct {
Tribe TribeMeta Tribe TribeMeta
} }
type CreateMonitorParams struct {
tribeID int64
groupID string
}
func NewCreateMonitorParams(groupID string, tribeID int64) (CreateMonitorParams, error) {
if groupID == "" {
return CreateMonitorParams{}, ValidationError{
Field: "GroupID",
Err: ErrRequired,
}
}
if tribeID < tribeIDMin {
return CreateMonitorParams{}, ValidationError{
Field: "TribeID",
Err: MinError{
Min: tribeIDMin,
},
}
}
return CreateMonitorParams{tribeID: tribeID, groupID: groupID}, nil
}
func (c CreateMonitorParams) TribeID() int64 {
return c.tribeID
}
func (c CreateMonitorParams) GroupID() string {
return c.groupID
}
type MonitorAlreadyExistsError struct { type MonitorAlreadyExistsError struct {
TribeID int64 TribeID int64
GroupID string GroupID string
} }
func (e MonitorAlreadyExistsError) Error() string { func (e MonitorAlreadyExistsError) Error() string {
return fmt.Sprintf("monitor (GroupID=%s,TribeID=%d) already exists", e.GroupID, e.TribeID) return fmt.Sprintf("monitor (groupID=%s,tribeID=%d) already exists", e.GroupID, e.TribeID)
} }
func (e MonitorAlreadyExistsError) UserError() string { func (e MonitorAlreadyExistsError) UserError() string {
@ -93,7 +56,7 @@ type MonitorNotFoundError struct {
} }
func (e MonitorNotFoundError) Error() string { func (e MonitorNotFoundError) Error() string {
return fmt.Sprintf("monitor (ID=%s) not found", e.ID) return fmt.Sprintf("monitor (id=%s) not found", e.ID)
} }
func (e MonitorNotFoundError) UserError() string { func (e MonitorNotFoundError) UserError() string {

View File

@ -9,63 +9,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNewCreateMonitorParams(t *testing.T) {
t.Parallel()
tests := []struct {
name string
tribeID int64
groupID string
err error
}{
{
name: "OK",
tribeID: 15,
groupID: uuid.NewString(),
},
{
name: "ERR: GroupID cannot be blank",
groupID: "",
err: domain.ValidationError{
Field: "GroupID",
Err: domain.ErrRequired,
},
},
{
name: "ERR: TribeID < 1",
groupID: uuid.NewString(),
tribeID: 0,
err: domain.ValidationError{
Field: "TribeID",
Err: domain.MinError{
Min: 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := domain.NewCreateMonitorParams(
tt.groupID,
tt.tribeID,
)
if tt.err != nil {
assert.ErrorIs(t, err, tt.err)
assert.Zero(t, res)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.groupID, res.GroupID())
assert.Equal(t, tt.tribeID, res.TribeID())
})
}
}
func TestMonitorAlreadyExistsError(t *testing.T) { func TestMonitorAlreadyExistsError(t *testing.T) {
t.Parallel() t.Parallel()
@ -74,7 +17,7 @@ func TestMonitorAlreadyExistsError(t *testing.T) {
TribeID: 1234, TribeID: 1234,
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("monitor (GroupID=%s,TribeID=%d) already exists", err.GroupID, err.TribeID), err.Error()) assert.Equal(t, fmt.Sprintf("monitor (groupID=%s,tribeID=%d) already exists", err.GroupID, err.TribeID), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeAlreadyExists, err.Code()) assert.Equal(t, domain.ErrorCodeAlreadyExists, err.Code())
} }
@ -99,7 +42,7 @@ func TestMonitorNotFoundError(t *testing.T) {
ID: uuid.NewString(), ID: uuid.NewString(),
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("monitor (ID=%s) not found", err.ID), err.Error()) assert.Equal(t, fmt.Sprintf("monitor (id=%s) not found", err.ID), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code()) assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
} }

View File

@ -49,7 +49,7 @@ type ServerDoesNotExistError struct {
} }
func (e ServerDoesNotExistError) Error() string { func (e ServerDoesNotExistError) Error() string {
return fmt.Sprintf("server (VersionCode=%s,Key=%s) doesn't exist", e.VersionCode, e.Key) return fmt.Sprintf("server (versionCode=%s,key=%s) doesn't exist", e.VersionCode, e.Key)
} }
func (e ServerDoesNotExistError) UserError() string { func (e ServerDoesNotExistError) UserError() string {
@ -66,7 +66,7 @@ type ServerIsClosedError struct {
} }
func (e ServerIsClosedError) Error() string { func (e ServerIsClosedError) Error() string {
return fmt.Sprintf("server (VersionCode=%s,Key=%s) is closed", e.VersionCode, e.Key) return fmt.Sprintf("server (versionCode=%s,key=%s) is closed", e.VersionCode, e.Key)
} }
func (e ServerIsClosedError) UserError() string { func (e ServerIsClosedError) UserError() string {
@ -82,7 +82,7 @@ type TribeDoesNotExistError struct {
} }
func (e TribeDoesNotExistError) Error() string { func (e TribeDoesNotExistError) Error() string {
return fmt.Sprintf("tribe (Tag=%s) doesn't exist", e.Tag) return fmt.Sprintf("tribe (tag=%s) doesn't exist", e.Tag)
} }
func (e TribeDoesNotExistError) UserError() string { func (e TribeDoesNotExistError) UserError() string {
@ -92,3 +92,19 @@ func (e TribeDoesNotExistError) UserError() string {
func (e TribeDoesNotExistError) Code() ErrorCode { func (e TribeDoesNotExistError) Code() ErrorCode {
return ErrorCodeValidationError return ErrorCodeValidationError
} }
type TribeNotFoundError struct {
Tag string
}
func (e TribeNotFoundError) Error() string {
return fmt.Sprintf("tribe (tag=%s) not found", e.Tag)
}
func (e TribeNotFoundError) UserError() string {
return e.Error()
}
func (e TribeNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}

View File

@ -16,7 +16,7 @@ func TestServerDoesNotExistError(t *testing.T) {
Key: "pl151", Key: "pl151",
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("server (VersionCode=%s,Key=%s) doesn't exist", err.VersionCode, err.Key), err.Error()) assert.Equal(t, fmt.Sprintf("server (versionCode=%s,key=%s) doesn't exist", err.VersionCode, err.Key), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
} }
@ -29,7 +29,7 @@ func TestServerIsClosedError(t *testing.T) {
Key: "pl151", Key: "pl151",
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("server (VersionCode=%s,Key=%s) is closed", err.VersionCode, err.Key), err.Error()) assert.Equal(t, fmt.Sprintf("server (versionCode=%s,key=%s) is closed", err.VersionCode, err.Key), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
} }
@ -41,7 +41,19 @@ func TestTribeDoesNotExistError(t *testing.T) {
Tag: "*TAG*", Tag: "*TAG*",
} }
var _ domain.Error = err var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("tribe (Tag=%s) doesn't exist", err.Tag), err.Error()) assert.Equal(t, fmt.Sprintf("tribe (tag=%s) doesn't exist", err.Tag), err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
} }
func TestTribeNotFoundError(t *testing.T) {
t.Parallel()
err := domain.TribeNotFoundError{
Tag: "*TAG*",
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("tribe (tag=%s) not found", err.Tag), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -0,0 +1,148 @@
package service
import (
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
)
type ennoblementNotificationBuilder struct {
groups []domain.GroupWithMonitors
ennoblements listEnnoblementsResult
}
func (b ennoblementNotificationBuilder) build() []domain.EnnoblementNotification {
var notifications []domain.EnnoblementNotification
for _, g := range b.groups {
notifications = append(notifications, b.buildGroup(g)...)
}
return notifications
}
func (b ennoblementNotificationBuilder) buildGroup(g domain.GroupWithMonitors) []domain.EnnoblementNotification {
var notifications []domain.EnnoblementNotification
for _, e := range b.ennoblements.find(g.VersionCode, g.ServerKey) {
if b.canSendEnnoblementNotificationTypeGain(g, e) {
notifications = append(notifications, domain.EnnoblementNotification{
Type: domain.EnnoblementNotificationTypeGain,
ServerID: g.ServerID,
ChannelID: g.ChannelGains,
Ennoblement: b.ennoblementToDomainModel(e),
})
}
if b.canSendEnnoblementNotificationTypeLoss(g, e) {
notifications = append(notifications, domain.EnnoblementNotification{
Type: domain.EnnoblementNotificationTypeLoss,
ServerID: g.ServerID,
ChannelID: g.ChannelLosses,
Ennoblement: b.ennoblementToDomainModel(e),
})
}
}
return notifications
}
func (b ennoblementNotificationBuilder) canSendEnnoblementNotificationTypeGain(g domain.GroupWithMonitors, e twhelp.Ennoblement) bool {
if g.ChannelGains == "" {
return false
}
if !g.Barbarians && b.isBarbarian(e) {
return false
}
if !g.Internals && b.isInternal(e, g.Monitors) {
return false
}
return b.isGain(e, g.Monitors)
}
func (b ennoblementNotificationBuilder) canSendEnnoblementNotificationTypeLoss(g domain.GroupWithMonitors, e twhelp.Ennoblement) bool {
if g.ChannelLosses == "" {
return false
}
if b.isInternal(e, g.Monitors) {
return false
}
return b.isLoss(e, g.Monitors)
}
func (b ennoblementNotificationBuilder) isInternal(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
var n, o bool
for _, m := range monitors {
if m.TribeID == e.NewOwner.Player.Tribe.Tribe.ID {
n = true
}
if m.TribeID == e.Village.Player.Player.Tribe.Tribe.ID {
o = true
}
}
return n && o
}
func (b ennoblementNotificationBuilder) isBarbarian(e twhelp.Ennoblement) bool {
return !e.Village.Player.Valid
}
func (b ennoblementNotificationBuilder) isGain(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
var n bool
for _, m := range monitors {
if m.TribeID == e.NewOwner.Player.Tribe.Tribe.ID {
n = true
break
}
}
return n && e.NewOwner.Player.ID != e.Village.Player.Player.ID
}
func (b ennoblementNotificationBuilder) isLoss(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
var o bool
for _, m := range monitors {
if m.TribeID == e.Village.Player.Player.Tribe.Tribe.ID {
o = true
break
}
}
return o && e.NewOwner.Player.ID != e.Village.Player.Player.ID
}
func (b ennoblementNotificationBuilder) ennoblementToDomainModel(e twhelp.Ennoblement) domain.Ennoblement {
return domain.Ennoblement{
ID: e.ID,
Village: domain.VillageMeta{
ID: e.Village.ID,
FullName: e.Village.FullName,
ProfileURL: e.Village.ProfileURL,
Player: domain.NullPlayerMeta{
Player: domain.PlayerMeta{
ID: e.Village.Player.Player.ID,
Name: e.Village.Player.Player.Name,
ProfileURL: e.Village.Player.Player.ProfileURL,
Tribe: domain.NullTribeMeta{
Tribe: domain.TribeMeta(e.Village.Player.Player.Tribe.Tribe),
Valid: e.Village.Player.Player.Tribe.Valid,
},
},
Valid: e.Village.Player.Valid,
},
},
NewOwner: domain.NullPlayerMeta{
Player: domain.PlayerMeta{
ID: e.NewOwner.Player.ID,
Name: e.NewOwner.Player.Name,
ProfileURL: e.NewOwner.Player.ProfileURL,
Tribe: domain.NullTribeMeta{
Tribe: domain.TribeMeta(e.NewOwner.Player.Tribe.Tribe),
Valid: e.NewOwner.Player.Tribe.Valid,
},
},
Valid: e.NewOwner.Valid,
},
CreatedAt: e.CreatedAt,
}
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sync"
"time" "time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
@ -13,11 +14,13 @@ import (
//counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository //counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository
type GroupRepository interface { type GroupRepository interface {
Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error)
Update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.Group, error) Update(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error)
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) AddMonitor(ctx context.Context, id string, tribeID int64) (domain.GroupWithMonitors, error)
Get(ctx context.Context, id, serverID string) (domain.Group, error) DeleteMonitors(ctx context.Context, id string, monitorID ...string) (domain.GroupWithMonitors, error)
Delete(ctx context.Context, id, serverID string) error List(ctx context.Context, params domain.ListGroupsParams) ([]domain.GroupWithMonitors, error)
Get(ctx context.Context, id string) (domain.GroupWithMonitors, error)
Delete(ctx context.Context, id string) error
DeleteMany(ctx context.Context, id ...string) error DeleteMany(ctx context.Context, id ...string) error
} }
@ -26,39 +29,43 @@ type Group struct {
client TWHelpClient client TWHelpClient
logger *zap.Logger logger *zap.Logger
maxGroupsPerServer int maxGroupsPerServer int
maxMonitorsPerGroup int
ennoblementsMu sync.Mutex // ennoblementsMu is used by listEnnoblements
ennoblementsSince map[string]time.Time // ennoblementsSince is used by listEnnoblements
} }
func NewGroup(repo GroupRepository, client TWHelpClient, logger *zap.Logger, maxGroupsPerServer int) *Group { func NewGroup(repo GroupRepository, client TWHelpClient, logger *zap.Logger, maxGroupsPerServer, maxMonitorsPerGroup int) *Group {
return &Group{ return &Group{
repo: repo, repo: repo,
client: client, client: client,
logger: logger, logger: logger,
maxGroupsPerServer: maxGroupsPerServer, maxGroupsPerServer: maxGroupsPerServer,
maxMonitorsPerGroup: maxMonitorsPerGroup,
} }
} }
func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) { func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error) {
groups, err := g.repo.List(ctx, domain.ListGroupsParams{ groups, err := g.repo.List(ctx, domain.ListGroupsParams{
ServerIDs: []string{params.ServerID()}, ServerIDs: []string{params.ServerID()},
}) })
if err != nil { if err != nil {
return domain.Group{}, fmt.Errorf("GroupRepository.List: %w", err) return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.List: %w", err)
} }
if len(groups) >= g.maxGroupsPerServer { if len(groups) >= g.maxGroupsPerServer {
return domain.Group{}, domain.GroupLimitReachedError{ return domain.GroupWithMonitors{}, domain.GroupLimitReachedError{
Current: len(groups), Current: len(groups),
Limit: g.maxGroupsPerServer, Limit: g.maxGroupsPerServer,
} }
} }
if err = g.checkTWServer(ctx, params.VersionCode(), params.ServerKey()); err != nil { if err = g.checkTWServer(ctx, params.VersionCode(), params.ServerKey()); err != nil {
return domain.Group{}, err return domain.GroupWithMonitors{}, err
} }
group, err := g.repo.Create(ctx, params) group, err := g.repo.Create(ctx, params)
if err != nil { if err != nil {
return domain.Group{}, fmt.Errorf("GroupRepository.Create: %w", err) return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.Create: %w", err)
} }
return group, nil return group, nil
@ -68,7 +75,7 @@ func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string
server, err := g.client.GetServer(ctx, versionCode, serverKey) server, err := g.client.GetServer(ctx, versionCode, serverKey)
if err != nil { if err != nil {
var apiErr twhelp.APIError var apiErr twhelp.APIError
if !errors.As(err, &apiErr) { if !errors.As(err, &apiErr) || apiErr.Code != twhelp.ErrorCodeEntityNotFound {
return fmt.Errorf("TWHelpClient.GetServer: %w", err) return fmt.Errorf("TWHelpClient.GetServer: %w", err)
} }
return domain.ServerDoesNotExistError{ return domain.ServerDoesNotExistError{
@ -87,7 +94,86 @@ func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string
return nil return nil
} }
func (g *Group) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error) { func (g *Group) AddMonitor(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error) {
// check if group exists
groupBeforeUpdate, err := g.Get(ctx, id, serverID)
if err != nil {
if errors.Is(err, domain.GroupNotFoundError{ID: id}) {
return domain.GroupWithMonitors{}, domain.GroupDoesNotExistError{ID: id}
}
return domain.GroupWithMonitors{}, err
}
// check limit
if len(groupBeforeUpdate.Monitors) >= g.maxMonitorsPerGroup {
return domain.GroupWithMonitors{}, domain.MonitorLimitReachedError{
Current: len(groupBeforeUpdate.Monitors),
Limit: g.maxMonitorsPerGroup,
}
}
tribes, err := g.client.ListTribes(ctx, groupBeforeUpdate.VersionCode, groupBeforeUpdate.ServerKey, twhelp.ListTribesQueryParams{
Limit: 1,
Tags: []string{tribeTag},
Deleted: twhelp.NullBool{
Valid: true,
Bool: false,
},
})
if err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf("TWHelpClient.ListTribes: %w", err)
}
if len(tribes) == 0 {
return domain.GroupWithMonitors{}, domain.TribeDoesNotExistError{
Tag: tribeTag,
}
}
group, err := g.repo.AddMonitor(ctx, groupBeforeUpdate.ID, tribes[0].ID)
if err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.AddMonitor: %w", err)
}
return group, nil
}
func (g *Group) DeleteMonitor(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error) {
// check if group exists
groupBeforeUpdate, err := g.Get(ctx, id, serverID)
if err != nil {
return domain.GroupWithMonitors{}, err
}
tribes, err := g.client.ListTribes(ctx, groupBeforeUpdate.VersionCode, groupBeforeUpdate.ServerKey, twhelp.ListTribesQueryParams{
Tags: []string{tribeTag},
})
if err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf("TWHelpClient.ListTribes: %w", err)
}
if len(tribes) == 0 {
return domain.GroupWithMonitors{}, domain.TribeNotFoundError{
Tag: tribeTag,
}
}
var ids []string
for _, t := range tribes {
for _, m := range groupBeforeUpdate.Monitors {
if m.TribeID == t.ID {
ids = append(ids, m.ID)
}
}
}
group, err := g.repo.DeleteMonitors(ctx, groupBeforeUpdate.ID, ids...)
if err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.DeleteMonitors: %w", err)
}
return group, nil
}
func (g *Group) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error) {
return g.update(ctx, id, serverID, domain.UpdateGroupParams{ return g.update(ctx, id, serverID, domain.UpdateGroupParams{
ChannelGains: domain.NullString{ ChannelGains: domain.NullString{
String: channel, String: channel,
@ -96,7 +182,7 @@ func (g *Group) SetChannelGains(ctx context.Context, id, serverID, channel strin
}) })
} }
func (g *Group) SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error) { func (g *Group) SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error) {
return g.update(ctx, id, serverID, domain.UpdateGroupParams{ return g.update(ctx, id, serverID, domain.UpdateGroupParams{
ChannelLosses: domain.NullString{ ChannelLosses: domain.NullString{
String: channel, String: channel,
@ -105,7 +191,7 @@ func (g *Group) SetChannelLosses(ctx context.Context, id, serverID, channel stri
}) })
} }
func (g *Group) SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error) { func (g *Group) SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.GroupWithMonitors, error) {
return g.update(ctx, id, serverID, domain.UpdateGroupParams{ return g.update(ctx, id, serverID, domain.UpdateGroupParams{
Internals: domain.NullBool{ Internals: domain.NullBool{
Bool: internals, Bool: internals,
@ -114,7 +200,7 @@ func (g *Group) SetInternals(ctx context.Context, id, serverID string, internals
}) })
} }
func (g *Group) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error) { func (g *Group) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.GroupWithMonitors, error) {
return g.update(ctx, id, serverID, domain.UpdateGroupParams{ return g.update(ctx, id, serverID, domain.UpdateGroupParams{
Barbarians: domain.NullBool{ Barbarians: domain.NullBool{
Bool: barbarians, Bool: barbarians,
@ -123,37 +209,249 @@ func (g *Group) SetBarbarians(ctx context.Context, id, serverID string, barbaria
}) })
} }
func (g *Group) update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.Group, error) { func (g *Group) update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) {
group, err := g.repo.Update(ctx, id, serverID, params) // check if group exists
if err != nil { if _, err := g.Get(ctx, id, serverID); err != nil {
return domain.Group{}, fmt.Errorf("GroupRepository.Update: %w", err) return domain.GroupWithMonitors{}, err
} }
group, err := g.repo.Update(ctx, id, params)
if err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.Update: %w", err)
}
return group, nil return group, nil
} }
func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) { func (g *Group) ListServer(ctx context.Context, serverID string) ([]domain.GroupWithMonitors, error) {
groups, err := g.repo.List(ctx, params) groups, err := g.repo.List(ctx, domain.ListGroupsParams{
ServerIDs: []string{serverID},
})
if err != nil { if err != nil {
return nil, fmt.Errorf("GroupRepository.List: %w", err) return nil, fmt.Errorf("GroupRepository.List: %w", err)
} }
return groups, nil return groups, nil
} }
func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) { func (g *Group) Get(ctx context.Context, id, serverID string) (domain.GroupWithMonitors, error) {
group, err := g.repo.Get(ctx, id, serverID) group, err := g.repo.Get(ctx, id)
if err != nil { if err != nil {
return domain.Group{}, fmt.Errorf("GroupRepository.Get: %w", err) return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.Get: %w", err)
} }
if group.ServerID != serverID {
return domain.GroupWithMonitors{}, domain.GroupNotFoundError{
ID: id,
}
}
return group, nil return group, nil
} }
type getTribeResult struct {
index int
monitor domain.Monitor
tribe twhelp.Tribe
err error
}
func (g *Group) GetWithTribes(ctx context.Context, id, serverID string) (domain.GroupWithMonitorsAndTribes, error) {
group, err := g.Get(ctx, id, serverID)
if err != nil {
return domain.GroupWithMonitorsAndTribes{}, err
}
var wg sync.WaitGroup
ch := make(chan getTribeResult)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
for i, monitor := range group.Monitors {
wg.Add(1)
go func(i int, monitor domain.Monitor) {
defer wg.Done()
res := getTribeResult{
index: i,
monitor: monitor,
}
res.tribe, res.err = g.client.GetTribeByID(
ctx,
group.VersionCode,
group.ServerKey,
monitor.TribeID,
)
ch <- res
}(i, monitor)
}
go func() {
wg.Wait()
close(ch)
}()
var firstErr error
monitors := make([]domain.MonitorWithTribe, len(group.Monitors))
for res := range ch {
if res.err != nil && firstErr == nil {
firstErr = fmt.Errorf("couldn't load tribe (monitorID=%s): %w", res.monitor.ID, res.err)
cancel()
continue
}
if res.err != nil {
continue
}
monitors[res.index] = domain.MonitorWithTribe{
Monitor: res.monitor,
Tribe: domain.TribeMeta{
ID: res.tribe.ID,
Name: res.tribe.Name,
Tag: res.tribe.Tag,
ProfileURL: res.tribe.ProfileURL,
},
}
}
if firstErr != nil {
return domain.GroupWithMonitorsAndTribes{}, firstErr
}
return domain.GroupWithMonitorsAndTribes{
Group: group.Group,
Monitors: monitors,
}, nil
}
func (g *Group) Delete(ctx context.Context, id, serverID string) error { func (g *Group) Delete(ctx context.Context, id, serverID string) error {
if err := g.repo.Delete(ctx, id, serverID); err != nil { // check if group exists
if _, err := g.Get(ctx, id, serverID); err != nil {
return err
}
if err := g.repo.Delete(ctx, id); err != nil {
return fmt.Errorf("GroupRepository.Delete: %w", err) return fmt.Errorf("GroupRepository.Delete: %w", err)
} }
return nil return nil
} }
func (g *Group) Execute(ctx context.Context) ([]domain.EnnoblementNotification, error) {
groups, err := g.repo.List(ctx, domain.ListGroupsParams{
EnabledNotifications: domain.NullBool{
Bool: true,
Valid: true,
},
})
if err != nil {
return nil, fmt.Errorf("GroupRepository.List: %w", err)
}
res, err := g.listEnnoblements(ctx, groups)
if err != nil {
return nil, err
}
return ennoblementNotificationBuilder{
groups: groups,
ennoblements: res,
}.build(), nil
}
type listEnnoblementsSingleResult struct {
versionCode string
serverKey string
ennoblements []twhelp.Ennoblement
}
type listEnnoblementsResult []listEnnoblementsSingleResult
func (r listEnnoblementsResult) find(versionCode, serverKey string) []twhelp.Ennoblement {
for _, res := range r {
if res.serverKey == serverKey && res.versionCode == versionCode {
return res.ennoblements
}
}
return nil
}
func (g *Group) listEnnoblements(ctx context.Context, groups []domain.GroupWithMonitors) (listEnnoblementsResult, error) {
g.ennoblementsMu.Lock()
defer g.ennoblementsMu.Unlock()
var wg sync.WaitGroup
ch := make(chan listEnnoblementsSingleResult)
ennoblementsSince := g.ennoblementsSince
skip := make(map[string]struct{}, len(ennoblementsSince))
for _, group := range groups {
key := g.buildEnnoblementsSinceKey(group.VersionCode, group.ServerKey)
if _, ok := skip[key]; ok {
continue
}
skip[key] = struct{}{}
since := ennoblementsSince[key]
if since.IsZero() {
since = time.Now().Add(-1 * time.Minute)
}
wg.Add(1)
go func(group domain.GroupWithMonitors, since time.Time) {
defer wg.Done()
ennoblements, err := g.client.ListEnnoblements(
ctx,
group.VersionCode,
group.ServerKey,
twhelp.ListEnnoblementsQueryParams{
Since: since,
Sort: []twhelp.ListEnnoblementsSort{twhelp.ListEnnoblementsSortCreatedAtASC},
},
)
if err != nil {
g.logger.Warn(
"couldn't list ennoblements",
zap.String("versionCode", group.VersionCode),
zap.String("serverKey", group.ServerKey),
zap.Error(err),
)
return
}
ch <- listEnnoblementsSingleResult{
versionCode: group.VersionCode,
serverKey: group.ServerKey,
ennoblements: ennoblements,
}
}(group, since)
}
go func() {
wg.Wait()
close(ch)
}()
// reinitialize ennoblementsSince
g.ennoblementsSince = make(map[string]time.Time)
results := make(listEnnoblementsResult, 0, len(skip))
for res := range ch {
key := g.buildEnnoblementsSinceKey(res.versionCode, res.serverKey)
if l := len(res.ennoblements); l > 0 {
g.ennoblementsSince[key] = res.ennoblements[l-1].CreatedAt.Add(time.Second)
} else {
g.ennoblementsSince[key] = ennoblementsSince[key]
}
results = append(results, res)
}
return results, nil
}
func (g *Group) buildEnnoblementsSinceKey(versionCode, serverKey string) string {
return versionCode + ":" + serverKey
}
func (g *Group) CleanUp(ctx context.Context) error { func (g *Group) CleanUp(ctx context.Context) error {
if err := g.deleteAllWithDisabledNotifications(ctx); err != nil { if err := g.deleteAllWithDisabledNotifications(ctx); err != nil {
return err return err
@ -175,7 +473,7 @@ func (g *Group) deleteAllWithDisabledNotifications(ctx context.Context) error {
CreatedAtLTE: time.Now().Add(-24 * time.Hour), CreatedAtLTE: time.Now().Add(-24 * time.Hour),
}) })
if err != nil { if err != nil {
return fmt.Errorf("GroupRepository.List: %w", err) return fmt.Errorf("couldn't find groups with disabled notifications: %w", err)
} }
ids := make([]string, 0, len(groups)) ids := make([]string, 0, len(groups))
@ -184,7 +482,7 @@ func (g *Group) deleteAllWithDisabledNotifications(ctx context.Context) error {
} }
if err = g.repo.DeleteMany(ctx, ids...); err != nil { if err = g.repo.DeleteMany(ctx, ids...); err != nil {
return fmt.Errorf("GroupRepository.DeleteMany: %w", err) return fmt.Errorf("couldn't delete groups with disabled notifications: %w", err)
} }
return nil return nil
@ -204,7 +502,7 @@ func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
}, },
}) })
if err != nil { if err != nil {
g.logger.Warn("failed to fetch closed servers", zap.Error(err), zap.String("versionCode", v.Code)) g.logger.Warn("couldn't list closed servers", zap.Error(err), zap.String("versionCode", v.Code))
continue continue
} }
@ -225,7 +523,7 @@ func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
groups, err := g.repo.List(ctx, params) groups, err := g.repo.List(ctx, params)
if err != nil { if err != nil {
g.logger.Warn("failed to list groups", zap.Error(err), zap.String("versionCode", v.Code)) g.logger.Warn("couldn't list groups", zap.Error(err), zap.String("versionCode", v.Code))
continue continue
} }
@ -239,11 +537,7 @@ func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
} }
if err = g.repo.DeleteMany(ctx, ids...); err != nil { if err = g.repo.DeleteMany(ctx, ids...); err != nil {
g.logger.Warn( g.logger.Warn("couldn't delete groups", zap.Error(err), zap.String("versionCode", v.Code))
"failed to delete groups",
zap.Error(err),
zap.String("versionCode", v.Code),
)
continue continue
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,474 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"sync"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
"go.uber.org/zap"
)
//counterfeiter:generate -o internal/mock/monitor_repository.gen.go . MonitorRepository
type MonitorRepository interface {
Create(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error)
List(ctx context.Context, groupID string) ([]domain.Monitor, error)
Get(ctx context.Context, id string) (domain.Monitor, error)
Delete(ctx context.Context, id string) error
}
//counterfeiter:generate -o internal/mock/group_reader.gen.go . GroupReader
type GroupReader interface {
Get(ctx context.Context, id, serverID string) (domain.Group, error)
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error)
}
type Monitor struct {
repo MonitorRepository
client TWHelpClient
groupSvc GroupReader
maxMonitorsPerGroup int
logger *zap.Logger
ennoblementsMu sync.Mutex // ennoblementsMu is used by Monitor.fetchEnnoblements
ennoblementsSince map[string]time.Time // ennoblementsSince is used by Monitor.fetchEnnoblements
}
func NewMonitor(
repo MonitorRepository,
groupSvc GroupReader,
client TWHelpClient,
logger *zap.Logger,
maxMonitorsPerGroup int,
) *Monitor {
return &Monitor{
repo: repo,
client: client,
groupSvc: groupSvc,
logger: logger,
maxMonitorsPerGroup: maxMonitorsPerGroup,
ennoblementsSince: make(map[string]time.Time),
}
}
func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) {
// check if group exists
group, err := m.groupSvc.Get(ctx, groupID, serverID)
if err != nil {
if errors.Is(err, domain.GroupNotFoundError{ID: groupID}) {
return domain.Monitor{}, domain.GroupDoesNotExistError{ID: groupID}
}
return domain.Monitor{}, fmt.Errorf("GroupService.Get: %w", err)
}
// check limit
monitors, err := m.repo.List(ctx, group.ID)
if err != nil {
return domain.Monitor{}, fmt.Errorf("MonitorRepository.List: %w", err)
}
if len(monitors) >= m.maxMonitorsPerGroup {
return domain.Monitor{}, domain.MonitorLimitReachedError{
Current: len(monitors),
Limit: m.maxMonitorsPerGroup,
}
}
tribes, err := m.client.ListTribes(ctx, group.VersionCode, group.ServerKey, twhelp.ListTribesQueryParams{
Limit: 1,
Tags: []string{tribeTag},
Deleted: twhelp.NullBool{
Valid: true,
Bool: false,
},
})
if err != nil {
var apiErr twhelp.APIError
if !errors.As(err, &apiErr) {
return domain.Monitor{}, fmt.Errorf("TWHelpClient.GetServer: %w", err)
}
return domain.Monitor{}, domain.TribeDoesNotExistError{
Tag: tribeTag,
}
}
if len(tribes) == 0 {
return domain.Monitor{}, domain.TribeDoesNotExistError{
Tag: tribeTag,
}
}
tribe := tribes[0]
params, err := domain.NewCreateMonitorParams(group.ID, tribe.ID)
if err != nil {
return domain.Monitor{}, fmt.Errorf("domain.NewCreateMonitorParams: %w", err)
}
monitor, err := m.repo.Create(ctx, params)
if err != nil {
return domain.Monitor{}, fmt.Errorf("MonitorRepository.Create: %w", err)
}
return monitor, nil
}
type getTribeResult struct {
index int
monitor domain.Monitor
tribe twhelp.Tribe
err error
}
func (m *Monitor) List(ctx context.Context, groupID, serverID string) ([]domain.MonitorWithTribe, error) {
group, err := m.groupSvc.Get(ctx, groupID, serverID)
if err != nil {
return nil, fmt.Errorf("GroupService.Get: %w", err)
}
monitors, err := m.repo.List(ctx, group.ID)
if err != nil {
return nil, fmt.Errorf("MonitorRepository.Delete: %w", err)
}
var wg sync.WaitGroup
ch := make(chan getTribeResult)
for i, monitor := range monitors {
wg.Add(1)
go func(i int, monitor domain.Monitor) {
res := getTribeResult{
index: i,
monitor: monitor,
}
res.tribe, res.err = m.client.GetTribeByID(
ctx,
group.VersionCode,
group.ServerKey,
monitor.TribeID,
)
ch <- res
}(i, monitor)
}
go func() {
wg.Wait()
close(ch)
}()
var firstErr error
results := make([]domain.MonitorWithTribe, len(monitors))
for res := range ch {
wg.Done()
if res.err != nil && firstErr == nil {
firstErr = fmt.Errorf("couldn't load tribe (monitorID=%s): %w", res.monitor.ID, res.err)
continue
}
results[res.index] = domain.MonitorWithTribe{
Monitor: res.monitor,
Tribe: domain.TribeMeta{
ID: res.tribe.ID,
Name: res.tribe.Name,
Tag: res.tribe.Tag,
ProfileURL: res.tribe.ProfileURL,
},
}
}
if firstErr != nil {
return nil, firstErr
}
return results, nil
}
func (m *Monitor) Delete(ctx context.Context, id, serverID string) error {
monitor, err := m.repo.Get(ctx, id)
if err != nil {
return fmt.Errorf("MonitorRepository.Get: %w", err)
}
_, err = m.groupSvc.Get(ctx, monitor.GroupID, serverID)
if err != nil {
return fmt.Errorf("GroupService.Get: %w", err)
}
if err = m.repo.Delete(ctx, id); err != nil {
return fmt.Errorf("MonitorRepository.Delete: %w", err)
}
return nil
}
func (m *Monitor) Execute(ctx context.Context) ([]domain.EnnoblementNotification, error) {
groups, err := m.groupSvc.List(ctx, domain.ListGroupsParams{
EnabledNotifications: domain.NullBool{
Bool: true,
Valid: true,
},
})
if err != nil {
return nil, fmt.Errorf("GroupService.List: %w", err)
}
res, err := m.fetchEnnoblements(ctx, groups)
if err != nil {
return nil, err
}
//nolint:prealloc
var notifications []domain.EnnoblementNotification
for _, g := range groups {
for _, r := range res {
if g.ServerKey != r.serverKey || g.VersionCode != r.versionCode {
continue
}
ns, err := m.executeGroup(ctx, g, r.ennoblements)
if err != nil {
m.logger.Warn(
"something went wrong while executing group",
zap.String("group", g.ID),
zap.Error(err),
)
continue
}
notifications = append(notifications, ns...)
}
}
return notifications, nil
}
func (m *Monitor) executeGroup(
ctx context.Context,
g domain.Group,
ennoblements []twhelp.Ennoblement,
) ([]domain.EnnoblementNotification, error) {
monitors, err := m.repo.List(ctx, g.ID)
if err != nil {
return nil, fmt.Errorf("MonitorRepository.List: %w", err)
}
//nolint:prealloc
var notifications []domain.EnnoblementNotification
for _, e := range ennoblements {
if canSendEnnoblementNotificationTypeGain(g, e, monitors) {
notifications = append(notifications, domain.EnnoblementNotification{
Type: domain.EnnoblementNotificationTypeGain,
ServerID: g.ServerID,
ChannelID: g.ChannelGains,
Ennoblement: ennoblementToDomainModel(e),
})
}
if canSendEnnoblementNotificationTypeLoss(g, e, monitors) {
notifications = append(notifications, domain.EnnoblementNotification{
Type: domain.EnnoblementNotificationTypeLoss,
ServerID: g.ServerID,
ChannelID: g.ChannelLosses,
Ennoblement: ennoblementToDomainModel(e),
})
}
}
return notifications, nil
}
type listEnnoblementsResult struct {
versionCode string
serverKey string
ennoblements []twhelp.Ennoblement
err error
}
type fetchEnnoblementsResult struct {
versionCode string
serverKey string
ennoblements []twhelp.Ennoblement
}
func (m *Monitor) fetchEnnoblements(ctx context.Context, groups []domain.Group) ([]fetchEnnoblementsResult, error) {
m.ennoblementsMu.Lock()
defer m.ennoblementsMu.Unlock()
var wg sync.WaitGroup
ch := make(chan listEnnoblementsResult)
ennoblementsSince := m.ennoblementsSince
skip := make(map[string]struct{}, len(ennoblementsSince))
for _, g := range groups {
key := g.VersionCode + ":" + g.ServerKey
if _, ok := skip[key]; ok {
continue
}
skip[key] = struct{}{}
since := ennoblementsSince[key]
if since.IsZero() {
since = time.Now().Add(-1 * time.Minute)
}
wg.Add(1)
go func(g domain.Group, since time.Time) {
res := listEnnoblementsResult{
versionCode: g.VersionCode,
serverKey: g.ServerKey,
}
res.ennoblements, res.err = m.client.ListEnnoblements(
ctx,
g.VersionCode,
g.ServerKey,
twhelp.ListEnnoblementsQueryParams{
Since: since,
Sort: []twhelp.ListEnnoblementsSort{twhelp.ListEnnoblementsSortCreatedAtASC},
},
)
ch <- res
}(g, since)
}
go func() {
wg.Wait()
close(ch)
}()
// reinitialize ennoblementsSince
m.ennoblementsSince = make(map[string]time.Time)
//nolint:prealloc
var results []fetchEnnoblementsResult
for res := range ch {
wg.Done()
key := res.versionCode + ":" + res.serverKey
if l := len(res.ennoblements); l > 0 {
m.ennoblementsSince[key] = res.ennoblements[l-1].CreatedAt.Add(time.Second)
} else {
m.ennoblementsSince[key] = ennoblementsSince[key]
}
if res.err != nil {
m.logger.Warn(
"failed to fetch ennoblements",
zap.String("versionCode", res.versionCode),
zap.String("serverKey", res.serverKey),
zap.Error(res.err),
)
continue
}
results = append(results, fetchEnnoblementsResult{
versionCode: res.versionCode,
serverKey: res.serverKey,
ennoblements: res.ennoblements,
})
}
return results, nil
}
func canSendEnnoblementNotificationTypeGain(g domain.Group, e twhelp.Ennoblement, monitors []domain.Monitor) bool {
if g.ChannelGains == "" {
return false
}
if !g.Barbarians && isBarbarian(e) {
return false
}
if !g.Internals && isInternal(e, monitors) {
return false
}
return isGain(e, monitors)
}
func canSendEnnoblementNotificationTypeLoss(g domain.Group, e twhelp.Ennoblement, monitors []domain.Monitor) bool {
if g.ChannelLosses == "" {
return false
}
if isInternal(e, monitors) {
return false
}
return isLoss(e, monitors)
}
func isInternal(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
var n, o bool
for _, m := range monitors {
if m.TribeID == e.NewOwner.Player.Tribe.Tribe.ID {
n = true
}
if m.TribeID == e.Village.Player.Player.Tribe.Tribe.ID {
o = true
}
}
return n && o
}
func isBarbarian(e twhelp.Ennoblement) bool {
return !e.Village.Player.Valid
}
func isGain(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
var n bool
for _, m := range monitors {
if m.TribeID == e.NewOwner.Player.Tribe.Tribe.ID {
n = true
break
}
}
return n && e.NewOwner.Player.ID != e.Village.Player.Player.ID
}
func isLoss(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
var o bool
for _, m := range monitors {
if m.TribeID == e.Village.Player.Player.Tribe.Tribe.ID {
o = true
break
}
}
return o && e.NewOwner.Player.ID != e.Village.Player.Player.ID
}
func ennoblementToDomainModel(e twhelp.Ennoblement) domain.Ennoblement {
return domain.Ennoblement{
ID: e.ID,
Village: domain.VillageMeta{
ID: e.Village.ID,
FullName: e.Village.FullName,
ProfileURL: e.Village.ProfileURL,
Player: domain.NullPlayerMeta{
Player: domain.PlayerMeta{
ID: e.Village.Player.Player.ID,
Name: e.Village.Player.Player.Name,
ProfileURL: e.Village.Player.Player.ProfileURL,
Tribe: domain.NullTribeMeta{
Tribe: domain.TribeMeta(e.Village.Player.Player.Tribe.Tribe),
Valid: e.Village.Player.Player.Tribe.Valid,
},
},
Valid: e.Village.Player.Valid,
},
},
NewOwner: domain.NullPlayerMeta{
Player: domain.PlayerMeta{
ID: e.NewOwner.Player.ID,
Name: e.NewOwner.Player.Name,
ProfileURL: e.NewOwner.Player.ProfileURL,
Tribe: domain.NullTribeMeta{
Tribe: domain.TribeMeta(e.NewOwner.Player.Tribe.Tribe),
Valid: e.NewOwner.Player.Tribe.Valid,
},
},
Valid: e.NewOwner.Valid,
},
CreatedAt: e.CreatedAt,
}
}

View File

@ -1,811 +0,0 @@
package service_test
import (
"context"
"errors"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"gitea.dwysokinski.me/twhelp/dcbot/internal/service"
"gitea.dwysokinski.me/twhelp/dcbot/internal/service/internal/mock"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)
func TestMonitor_Create(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
repo := &mock.FakeMonitorRepository{}
repo.ListReturns(nil, nil)
repo.CreateCalls(func(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) {
return domain.Monitor{
ID: uuid.NewString(),
TribeID: params.TribeID(),
GroupID: params.GroupID(),
CreatedAt: time.Now(),
}, nil
})
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
ServerID: serverID,
ChannelGains: "",
ChannelLosses: "",
ServerKey: "pl151",
VersionCode: "pl",
CreatedAt: time.Now(),
}, nil
})
client := &mock.FakeTWHelpClient{}
tribe := twhelp.Tribe{
ID: 150,
Tag: "*TAG*",
Name: uuid.NewString(),
ProfileURL: "https://pl151.plemiona.pl/game.php?screen=info_player&id=150",
DeletedAt: time.Time{},
}
client.ListTribesReturns([]twhelp.Tribe{tribe}, nil)
groupID := uuid.NewString()
monitor, err := service.NewMonitor(repo, groupSvc, client, zap.NewNop(), 10).
Create(context.Background(), groupID, uuid.NewString(), tribe.Tag)
assert.NoError(t, err)
assert.NotEmpty(t, monitor.ID)
assert.Equal(t, groupID, monitor.GroupID)
assert.Equal(t, tribe.ID, monitor.TribeID)
assert.NotEmpty(t, monitor.CreatedAt)
})
t.Run("ERR: group doesn't exist", func(t *testing.T) {
t.Parallel()
groupID := uuid.NewString()
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetReturns(domain.Group{}, domain.GroupNotFoundError{ID: groupID})
monitor, err := service.NewMonitor(nil, groupSvc, nil, zap.NewNop(), 10).
Create(context.Background(), groupID, uuid.NewString(), "tag")
assert.ErrorIs(t, err, domain.GroupDoesNotExistError{
ID: groupID,
})
assert.Zero(t, monitor)
})
t.Run("ERR: monitor limit has been reached", func(t *testing.T) {
t.Parallel()
const maxMonitorsPerGroup = 10
repo := &mock.FakeMonitorRepository{}
repo.ListReturns(make([]domain.Monitor, maxMonitorsPerGroup), nil)
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
ServerID: serverID,
ChannelGains: "",
ChannelLosses: "",
ServerKey: "pl151",
VersionCode: "pl",
CreatedAt: time.Now(),
}, nil
})
monitor, err := service.NewMonitor(repo, groupSvc, nil, zap.NewNop(), maxMonitorsPerGroup).
Create(context.Background(), uuid.NewString(), uuid.NewString(), "TAG")
assert.ErrorIs(t, err, domain.MonitorLimitReachedError{
Current: maxMonitorsPerGroup,
Limit: maxMonitorsPerGroup,
})
assert.Zero(t, monitor)
})
t.Run("ERR: tribe doesn't exist", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
}{
{
name: "err=null",
},
{
name: "err!=null",
err: twhelp.APIError{
Code: twhelp.ErrorCodeInternalServerError,
Message: "internal server error",
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repo := &mock.FakeMonitorRepository{}
repo.ListReturns(nil, nil)
repo.CreateCalls(func(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) {
return domain.Monitor{
ID: uuid.NewString(),
TribeID: params.TribeID(),
GroupID: params.GroupID(),
CreatedAt: time.Now(),
}, nil
})
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
ServerID: serverID,
ChannelGains: "",
ChannelLosses: "",
ServerKey: "pl151",
VersionCode: "pl",
CreatedAt: time.Now(),
}, nil
})
client := &mock.FakeTWHelpClient{}
client.ListTribesReturns(nil, tt.err)
tag := "TAG"
monitor, err := service.NewMonitor(repo, groupSvc, client, zap.NewNop(), 10).
Create(context.Background(), uuid.NewString(), uuid.NewString(), tag)
assert.ErrorIs(t, err, domain.TribeDoesNotExistError{
Tag: tag,
})
assert.Zero(t, monitor)
})
}
})
}
func TestMonitor_List(t *testing.T) {
t.Parallel()
groupID := uuid.NewString()
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
ServerID: serverID,
ChannelGains: "",
ChannelLosses: "",
ServerKey: "pl151",
VersionCode: "pl",
CreatedAt: time.Now(),
}, nil
})
repo := &mock.FakeMonitorRepository{}
monitors := []domain.Monitor{
{
ID: uuid.NewString(),
TribeID: 124,
GroupID: groupID,
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
TribeID: 125,
GroupID: groupID,
CreatedAt: time.Now(),
},
}
repo.ListReturns(monitors, nil)
client := &mock.FakeTWHelpClient{}
client.GetTribeByIDCalls(func(ctx context.Context, _ string, _ string, id int64) (twhelp.Tribe, error) {
return twhelp.Tribe{
ID: id,
Tag: uuid.NewString(),
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
DeletedAt: time.Time{},
}, nil
})
res, err := service.NewMonitor(repo, groupSvc, client, zap.NewNop(), 10).
List(context.Background(), groupID, uuid.NewString())
assert.NoError(t, err)
assert.Len(t, res, len(monitors))
for i, m := range monitors {
assert.Equal(t, m, res[i].Monitor)
assert.Equal(t, m.TribeID, res[i].Tribe.ID)
}
}
func TestMonitor_Execute(t *testing.T) {
t.Parallel()
client := &mock.FakeTWHelpClient{}
tribes := map[string][]twhelp.TribeMeta{
"pl:pl181": {
{
ID: 1,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 2,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 3,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
},
"en:en130": {
{
ID: 100,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 101,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
},
"pl:pl180": {
{
ID: 200,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
},
"de:de200": {
{
ID: 300,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 301,
Name: uuid.NewString(),
Tag: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
},
}
players := map[string][]twhelp.PlayerMeta{
"pl:pl181": {
{
ID: 1,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["pl:pl181"][0],
Valid: true,
},
},
{
ID: 2,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["pl:pl181"][1],
Valid: true,
},
},
{
ID: 3,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["pl:pl181"][2],
Valid: true,
},
},
{
ID: 4,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{},
},
},
"en:en130": {
{
ID: 100,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["en:en130"][0],
Valid: true,
},
},
{
ID: 101,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["en:en130"][1],
Valid: true,
},
},
{
ID: 102,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{},
},
},
"pl:pl180": {
{
ID: 200,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["pl:pl180"][0],
Valid: true,
},
},
},
"de:de200": {
{
ID: 300,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["de:de200"][0],
Valid: true,
},
},
{
ID: 301,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["de:de200"][0],
Valid: true,
},
},
{
ID: 302,
Name: uuid.NewString(),
ProfileURL: uuid.NewString(),
Tribe: twhelp.NullTribeMeta{
Tribe: tribes["de:de200"][1],
Valid: true,
},
},
},
}
villages := map[string][]twhelp.VillageMeta{
"pl:pl181": {
{
ID: 1,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][1],
Valid: true,
},
},
{
ID: 2,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][0],
Valid: true,
},
},
{
ID: 3,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][2],
Valid: true,
},
},
{
ID: 4,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 5,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][0],
Valid: true,
},
},
},
"en:en130": {
{
ID: 100,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 101,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["en:en130"][0],
Valid: true,
},
},
{
ID: 102,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["en:en130"][2],
Valid: true,
},
},
},
"pl:pl180": {
{
ID: 200,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
},
"de:de200": {
{
ID: 300,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
},
{
ID: 301,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["de:de200"][1],
Valid: true,
},
},
{
ID: 302,
FullName: uuid.NewString(),
ProfileURL: uuid.NewString(),
Player: twhelp.NullPlayerMeta{
Player: players["de:de200"][2],
Valid: true,
},
},
},
}
ennoblements := map[string][]twhelp.Ennoblement{
"pl:pl181": {
{
ID: 1,
Village: villages["pl:pl181"][0],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 2, // self conquer, should be skipped
Village: villages["pl:pl181"][1],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-4 * time.Minute),
},
{
ID: 3, // internal, should be skipped (internals disabled)
Village: villages["pl:pl181"][2],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-3 * time.Minute),
},
{
ID: 4, // barbarian, shouldn't be skipped (barbarians enabled)
Village: villages["pl:pl181"][3],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 5, // disabled notifications about gains, should be skipped
Village: villages["pl:pl181"][4],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][1],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 6, // disabled notifications about losses, should be skipped
Village: villages["pl:pl181"][4],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl181"][3],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
},
"en:en130": {
{
ID: 100, // no monitor for these tribes, should be skipped
Village: villages["en:en130"][0],
NewOwner: twhelp.NullPlayerMeta{
Player: players["en:en130"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 101, // no monitor for these tribes, should be skipped
Village: villages["en:en130"][1],
NewOwner: twhelp.NullPlayerMeta{
Player: players["en:en130"][2],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 102,
Village: villages["en:en130"][2],
NewOwner: twhelp.NullPlayerMeta{
Player: players["en:en130"][1],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
},
"pl:pl180": {
{
ID: 200, // api error, should be skipped
Village: villages["pl:pl180"][0],
NewOwner: twhelp.NullPlayerMeta{
Player: players["pl:pl180"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
},
"de:de200": {
{
ID: 300, // barbarian, should be skipped (barbarians disabled)
Village: villages["de:de200"][0],
NewOwner: twhelp.NullPlayerMeta{
Player: players["de:de200"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 301, // internal, shouldn't be skipped (internals enabled)
Village: villages["de:de200"][1],
NewOwner: twhelp.NullPlayerMeta{
Player: players["de:de200"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
{
ID: 302, // internal, shouldn't be skipped (internals enabled)
Village: villages["de:de200"][2],
NewOwner: twhelp.NullPlayerMeta{
Player: players["de:de200"][0],
Valid: true,
},
CreatedAt: time.Now().Add(-5 * time.Minute),
},
},
}
client.ListEnnoblementsCalls(
func(
ctx context.Context,
version string,
server string,
_ twhelp.ListEnnoblementsQueryParams,
) ([]twhelp.Ennoblement, error) {
if version == "pl" && server == "pl180" {
return nil, errors.New("random error")
}
return ennoblements[version+":"+server], nil
},
)
groupSvc := &mock.FakeGroupReader{}
groups := []domain.Group{
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: "",
Barbarians: true,
ServerKey: "pl181",
VersionCode: "pl",
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: "",
ChannelLosses: uuid.NewString(),
ServerKey: "pl181",
VersionCode: "pl",
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: uuid.NewString(),
ServerKey: "en130",
VersionCode: "en",
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: uuid.NewString(),
Barbarians: true,
ServerKey: "pl180",
VersionCode: "pl",
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
ChannelGains: uuid.NewString(),
ChannelLosses: uuid.NewString(),
Barbarians: false,
Internals: true,
ServerKey: "de200",
VersionCode: "de",
CreatedAt: time.Now(),
},
}
groupSvc.ListReturns(groups, nil)
repo := &mock.FakeMonitorRepository{}
monitors := map[string][]domain.Monitor{
groups[0].ID: {
{
ID: uuid.NewString(),
TribeID: tribes["pl:pl181"][0].ID,
GroupID: groups[0].ID,
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
TribeID: tribes["pl:pl181"][2].ID,
GroupID: groups[0].ID,
CreatedAt: time.Now(),
},
},
groups[1].ID: {
{
ID: uuid.NewString(),
TribeID: tribes["pl:pl181"][1].ID,
GroupID: groups[1].ID,
CreatedAt: time.Now(),
},
},
groups[2].ID: {
{
ID: uuid.NewString(),
TribeID: tribes["en:en130"][1].ID,
GroupID: groups[2].ID,
CreatedAt: time.Now(),
},
},
groups[3].ID: {
{
ID: uuid.NewString(),
TribeID: tribes["pl:pl180"][0].ID,
GroupID: groups[3].ID,
CreatedAt: time.Now(),
},
},
groups[4].ID: {
{
ID: uuid.NewString(),
TribeID: tribes["de:de200"][0].ID,
GroupID: groups[4].ID,
CreatedAt: time.Now(),
},
{
ID: uuid.NewString(),
TribeID: tribes["de:de200"][1].ID,
GroupID: groups[4].ID,
CreatedAt: time.Now(),
},
},
}
repo.ListCalls(func(ctx context.Context, groupID string) ([]domain.Monitor, error) {
return monitors[groupID], nil
})
notifications, err := service.NewMonitor(repo, groupSvc, client, zap.NewNop(), 10).
Execute(context.Background())
assert.NoError(t, err)
expectedNotifications := []domain.EnnoblementNotification{
newNotification(domain.EnnoblementNotificationTypeGain, groups[0].ServerID, groups[0].ChannelGains, ennoblements["pl:pl181"][0]),
newNotification(domain.EnnoblementNotificationTypeGain, groups[0].ServerID, groups[0].ChannelGains, ennoblements["pl:pl181"][3]),
newNotification(domain.EnnoblementNotificationTypeLoss, groups[1].ServerID, groups[1].ChannelLosses, ennoblements["pl:pl181"][0]),
newNotification(domain.EnnoblementNotificationTypeGain, groups[2].ServerID, groups[2].ChannelGains, ennoblements["en:en130"][2]),
newNotification(domain.EnnoblementNotificationTypeGain, groups[4].ServerID, groups[4].ChannelGains, ennoblements["de:de200"][1]),
newNotification(domain.EnnoblementNotificationTypeGain, groups[4].ServerID, groups[4].ChannelGains, ennoblements["de:de200"][2]),
}
assert.Len(t, notifications, len(expectedNotifications))
for _, n := range expectedNotifications {
assert.Contains(t, notifications, n)
}
}
func newNotification(typ domain.EnnoblementNotificationType, serverID, channelID string, e twhelp.Ennoblement) domain.EnnoblementNotification {
return domain.EnnoblementNotification{
Type: typ,
ServerID: serverID,
ChannelID: channelID,
Ennoblement: domain.Ennoblement{
ID: e.ID,
Village: domain.VillageMeta{
ID: e.Village.ID,
FullName: e.Village.FullName,
ProfileURL: e.Village.ProfileURL,
Player: domain.NullPlayerMeta{
Player: domain.PlayerMeta{
ID: e.Village.Player.Player.ID,
Name: e.Village.Player.Player.Name,
ProfileURL: e.Village.Player.Player.ProfileURL,
Tribe: domain.NullTribeMeta{
Tribe: domain.TribeMeta(e.Village.Player.Player.Tribe.Tribe),
Valid: e.Village.Player.Player.Tribe.Valid,
},
},
Valid: e.Village.Player.Valid,
},
},
NewOwner: domain.NullPlayerMeta{
Player: domain.PlayerMeta{
ID: e.NewOwner.Player.ID,
Name: e.NewOwner.Player.Name,
ProfileURL: e.NewOwner.Player.ProfileURL,
Tribe: domain.NullTribeMeta{
Tribe: domain.TribeMeta(e.NewOwner.Player.Tribe.Tribe),
Valid: e.NewOwner.Player.Tribe.Valid,
},
},
Valid: e.NewOwner.Valid,
},
CreatedAt: e.CreatedAt,
},
}
}

View File

@ -9,6 +9,8 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"time" "time"
"golang.org/x/time/rate"
) )
const ( const (
@ -27,6 +29,7 @@ type Client struct {
userAgent string userAgent string
client *http.Client client *http.Client
baseURL *url.URL baseURL *url.URL
rateLimiter *rate.Limiter
} }
type ClientOption func(c *Client) type ClientOption func(c *Client)
@ -43,6 +46,12 @@ func WithUserAgent(ua string) ClientOption {
} }
} }
func WithRateLimiter(rl *rate.Limiter) ClientOption {
return func(c *Client) {
c.rateLimiter = rl
}
}
func NewClient(baseURL *url.URL, opts ...ClientOption) *Client { func NewClient(baseURL *url.URL, opts ...ClientOption) *Client {
c := &Client{ c := &Client{
baseURL: baseURL, baseURL: baseURL,
@ -216,6 +225,13 @@ func (c *Client) getJSON(ctx context.Context, urlStr string, v any) error {
// headers // headers
req.Header.Set("User-Agent", c.userAgent) 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) resp, err := c.client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("client.Do: %w", err) return fmt.Errorf("client.Do: %w", err)

View File

@ -34,8 +34,10 @@ spec:
key: token key: token
- name: TWHELP_URL - name: TWHELP_URL
value: "https://tribalwarshelp.com" value: "https://tribalwarshelp.com"
- name: TWHELP_RATE_LIMITER_ENABLED
value: "false"
- name: BOT_MAX_GROUPS_PER_SERVER - name: BOT_MAX_GROUPS_PER_SERVER
value: "10" value: "5"
- name: BOT_MAX_MONITORS_PER_GROUP - name: BOT_MAX_MONITORS_PER_GROUP
value: "10" value: "10"
livenessProbe: livenessProbe:

39
k8s/overlays/dev/bot.yml Normal file
View File

@ -0,0 +1,39 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: twhelp-dcbot-deployment
spec:
template:
spec:
containers:
- name: twhelp-dcbot
image: dcbot
env:
- name: APP_MODE
value: development
- name: DB_DSN
valueFrom:
secretKeyRef:
name: twhelp-dcbot-secret
key: db-dsn
- name: DB_MAX_OPEN_CONNECTIONS
value: "10"
- name: DB_MAX_IDLE_CONNECTIONS
value: "3"
- name: BOT_TOKEN
valueFrom:
secretKeyRef:
name: twhelp-dcbot-secret
key: token
- name: TWHELP_URL
value: "https://tribalwarshelp.com"
- name: TWHELP_RATE_LIMITER_ENABLED
value: "true"
- name: TWHELP_RATE_LIMITER_MAX_REQUESTS_PER_SECOND
value: "10"
- name: TWHELP_RATE_LIMITER_REQUEST_BURST
value: "5"
- name: BOT_MAX_GROUPS_PER_SERVER
value: "5"
- name: BOT_MAX_MONITORS_PER_GROUP
value: "10"

View File

@ -4,3 +4,5 @@ nameSuffix: -dev
resources: resources:
- secret.yml - secret.yml
- ../../base - ../../base
patchesStrategicMerge:
- bot.yml