refactor: group & monitor refactor (#107)
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #107
This commit is contained in:
parent
e6d15ac950
commit
e110807619
|
@ -47,36 +47,23 @@ func New() *cli.Command {
|
|||
}
|
||||
|
||||
groupRepo := bundb.NewGroup(db)
|
||||
monitorRepo := bundb.NewMonitor(db)
|
||||
|
||||
choiceSvc := service.NewChoice(client)
|
||||
groupSvc := service.NewGroup(
|
||||
groupRepo,
|
||||
client,
|
||||
logger,
|
||||
cfg.MaxGroupsPerServer,
|
||||
)
|
||||
monitorSvc := service.NewMonitor(
|
||||
monitorRepo,
|
||||
groupRepo,
|
||||
client,
|
||||
logger,
|
||||
cfg.MaxMonitorsPerGroup,
|
||||
)
|
||||
groupSvc := service.NewGroup(groupRepo, client, logger, cfg.MaxGroupsPerServer, cfg.MaxMonitorsPerGroup)
|
||||
|
||||
bot, err := discord.NewBot(cfg.Token, groupSvc, monitorSvc, choiceSvc, logger)
|
||||
bot, err := discord.NewBot(cfg.Token, groupSvc, choiceSvc, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord.NewBot: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = bot.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if runErr := bot.Run(); runErr != nil {
|
||||
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 {
|
||||
return fmt.Errorf("couldn't create file (path=%s): %w", healthyFilePath, err)
|
||||
|
@ -93,7 +80,7 @@ func New() *cli.Command {
|
|||
|
||||
type botConfig struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,15 @@ import (
|
|||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type twhelpClientConfig struct {
|
||||
URL *url.URL `envconfig:"URL" required:"true"`
|
||||
Timeout time.Duration `envconfig:"TIMEOUT" default:"10s"`
|
||||
URL *url.URL `envconfig:"URL" required:"true"`
|
||||
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) {
|
||||
|
@ -21,11 +25,16 @@ func NewTWHelpClient(version string) (*twhelp.Client, error) {
|
|||
return nil, fmt.Errorf("envconfig.Process: %w", err)
|
||||
}
|
||||
|
||||
return twhelp.NewClient(
|
||||
cfg.URL,
|
||||
opts := []twhelp.ClientOption{
|
||||
twhelp.WithHTTPClient(&http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
}),
|
||||
twhelp.WithUserAgent("TWHelpDCBot/"+version),
|
||||
), nil
|
||||
twhelp.WithUserAgent("TWHelpDCBot/" + version),
|
||||
}
|
||||
|
||||
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
1
go.mod
|
@ -18,6 +18,7 @@ require (
|
|||
github.com/uptrace/bun/driver/pgdriver v1.1.14
|
||||
github.com/urfave/cli/v2 v2.25.5
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/time v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
2
go.sum
2
go.sum
|
@ -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/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/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
|
|
@ -149,7 +149,7 @@ func loadFixtures(tb testing.TB, bunDB *bun.DB) *bunfixture {
|
|||
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()
|
||||
|
||||
row, err := f.Row("Group." + id)
|
||||
|
@ -159,13 +159,13 @@ func (f *bunfixture) group(tb testing.TB, id string) domain.Group {
|
|||
return g.ToDomain()
|
||||
}
|
||||
|
||||
func (f *bunfixture) groups(tb testing.TB) []domain.Group {
|
||||
func (f *bunfixture) groups(tb testing.TB) []domain.GroupWithMonitors {
|
||||
tb.Helper()
|
||||
|
||||
//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"}
|
||||
|
||||
groups := make([]domain.Group, 0, len(ids))
|
||||
groups := make([]domain.GroupWithMonitors, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
groups = append(groups, f.group(tb, id))
|
||||
}
|
||||
|
@ -173,6 +173,16 @@ func (f *bunfixture) groups(tb testing.TB) []domain.Group {
|
|||
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 {
|
||||
return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ import (
|
|||
"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 Group struct {
|
||||
|
@ -20,7 +22,7 @@ func NewGroup(db *bun.DB) *Group {
|
|||
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{
|
||||
ServerID: params.ServerID(),
|
||||
VersionCode: params.VersionCode(),
|
||||
|
@ -35,52 +37,103 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do
|
|||
Model(&group).
|
||||
Returning("*").
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
return domain.Group{}, domain.ErrNothingToUpdate
|
||||
return domain.GroupWithMonitors{}, domain.ErrNothingToUpdate
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
res, err := g.db.NewUpdate().
|
||||
Model(&group).
|
||||
Returning("*").
|
||||
Returning("NULL").
|
||||
Where("id = ?", id).
|
||||
Where("server_id = ?", serverID).
|
||||
Apply(updateGroupsParamsApplier{params}.apply).
|
||||
Exec(ctx)
|
||||
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 {
|
||||
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
|
||||
|
||||
if err := g.db.NewSelect().
|
||||
Model(&groups).
|
||||
Order("created_at ASC").
|
||||
Relation("Monitors", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.Order("created_at ASC")
|
||||
}).
|
||||
Apply(listGroupsParamsApplier{params}.apply).
|
||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
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 {
|
||||
result = append(result, group.ToDomain())
|
||||
}
|
||||
|
@ -88,29 +141,41 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom
|
|||
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 {
|
||||
return domain.Group{}, domain.GroupNotFoundError{ID: id}
|
||||
return model.Group{}, domain.GroupNotFoundError{ID: id}
|
||||
}
|
||||
|
||||
var group model.Group
|
||||
|
||||
err := g.db.NewSelect().
|
||||
q := g.db.NewSelect().
|
||||
Model(&group).
|
||||
Where("id = ?", id).
|
||||
Where("server_id = ?", serverID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
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)
|
||||
Where("id = ?", id)
|
||||
if withMonitors {
|
||||
q = q.Relation("Monitors", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.Order("created_at ASC")
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
return domain.GroupNotFoundError{ID: id}
|
||||
}
|
||||
|
@ -119,14 +184,14 @@ func (g *Group) Delete(ctx context.Context, id, serverID string) error {
|
|||
Model(&model.Group{}).
|
||||
Returning("NULL").
|
||||
Where("id = ?", id).
|
||||
Where("server_id = ?", serverID).
|
||||
Exec(ctx)
|
||||
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 {
|
||||
return domain.GroupNotFoundError{ID: id}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -183,7 +248,7 @@ type listGroupsParamsApplier struct {
|
|||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -202,7 +267,7 @@ func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
|||
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))
|
||||
}
|
||||
|
||||
|
@ -212,3 +277,26 @@ func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,7 @@ func TestGroup_Create(t *testing.T) {
|
|||
|
||||
group, err := repo.Create(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
_, err = uuid.Parse(group.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, group.ID)
|
||||
assert.Equal(t, params.ServerID(), group.ServerID)
|
||||
assert.Equal(t, params.ServerKey(), group.ServerKey)
|
||||
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.Equal(t, params.ChannelGains.String, updatedGroup.ChannelGains)
|
||||
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.Parallel()
|
||||
|
||||
updatedGroup, err := repo.Update(context.Background(), "", "", domain.UpdateGroupParams{})
|
||||
updatedGroup, err := repo.Update(context.Background(), "", domain.UpdateGroupParams{})
|
||||
assert.ErrorIs(t, err, domain.ErrNothingToUpdate)
|
||||
assert.Zero(t, updatedGroup)
|
||||
})
|
||||
|
@ -104,24 +103,16 @@ func TestGroup_Update(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
serverID string
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
name: "ID - not UUID",
|
||||
id: "test",
|
||||
serverID: group.ServerID,
|
||||
name: "ID - not UUID",
|
||||
id: "test",
|
||||
},
|
||||
{
|
||||
name: "ID - random UUID",
|
||||
id: uuid.NewString(),
|
||||
serverID: group.ServerID,
|
||||
},
|
||||
{
|
||||
name: "ServerID - random UUID",
|
||||
id: group.ID,
|
||||
serverID: uuid.NewString(),
|
||||
name: "ID - random UUID",
|
||||
id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -131,7 +122,7 @@ func TestGroup_Update(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
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{
|
||||
String: "update",
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -299,33 +452,25 @@ func TestGroup_Get(t *testing.T) {
|
|||
t.Run("OK", func(t *testing.T) {
|
||||
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.Equal(t, group, g)
|
||||
assert.Equal(t, group.ID, g.ID)
|
||||
})
|
||||
|
||||
t.Run("ERR: group not found (unknown ID)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
serverID string
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
name: "ID - not UUID",
|
||||
id: "test",
|
||||
serverID: group.ServerID,
|
||||
name: "ID - not UUID",
|
||||
id: "test",
|
||||
},
|
||||
{
|
||||
name: "ID - random UUID",
|
||||
id: uuid.NewString(),
|
||||
serverID: group.ServerID,
|
||||
},
|
||||
{
|
||||
name: "ServerID - random UUID",
|
||||
id: group.ID,
|
||||
serverID: uuid.NewString(),
|
||||
name: "ID - random UUID",
|
||||
id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -335,7 +480,7 @@ func TestGroup_Get(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
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.Zero(t, g)
|
||||
})
|
||||
|
@ -353,7 +498,6 @@ func TestGroup_Delete(t *testing.T) {
|
|||
db := newDB(t)
|
||||
fixture := loadFixtures(t, db)
|
||||
groupRepo := bundb.NewGroup(db)
|
||||
monitorRepo := bundb.NewMonitor(db)
|
||||
group := fixture.group(t, "group-1-server-1")
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
|
@ -364,42 +508,29 @@ func TestGroup_Delete(t *testing.T) {
|
|||
})
|
||||
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{
|
||||
ServerIDs: []string{group.ServerID},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
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.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
serverID string
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
name: "ID - not UUID",
|
||||
id: "test",
|
||||
serverID: group.ServerID,
|
||||
name: "ID - not UUID",
|
||||
id: "test",
|
||||
},
|
||||
{
|
||||
name: "ID - random UUID",
|
||||
id: uuid.NewString(),
|
||||
serverID: group.ServerID,
|
||||
},
|
||||
{
|
||||
name: "ServerID - random UUID",
|
||||
id: group.ID,
|
||||
serverID: uuid.NewString(),
|
||||
name: "ID - random UUID",
|
||||
id: uuid.NewString(),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -409,11 +540,7 @@ func TestGroup_Delete(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.ErrorIs(
|
||||
t,
|
||||
groupRepo.Delete(context.Background(), tt.id, tt.serverID),
|
||||
domain.GroupNotFoundError{ID: tt.id},
|
||||
)
|
||||
assert.ErrorIs(t, groupRepo.Delete(context.Background(), tt.id), domain.GroupNotFoundError{ID: tt.id})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -429,7 +556,6 @@ func TestGroup_DeleteMany(t *testing.T) {
|
|||
db := newDB(t)
|
||||
fixture := loadFixtures(t, db)
|
||||
groupRepo := bundb.NewGroup(db)
|
||||
monitorRepo := bundb.NewMonitor(db)
|
||||
group1 := fixture.group(t, "group-1-server-1")
|
||||
group2 := fixture.group(t, "group-2-server-1")
|
||||
ids := []string{group1.ID, group2.ID}
|
||||
|
@ -450,13 +576,6 @@ func TestGroup_DeleteMany(t *testing.T) {
|
|||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, afterDelete, len(beforeDelete)-len(ids))
|
||||
|
||||
// monitors should also be deleted
|
||||
for _, id := range ids {
|
||||
monitors, err := monitorRepo.List(context.Background(), id)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, monitors, 0)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OK: 0 ids", func(t *testing.T) {
|
||||
|
|
|
@ -20,18 +20,26 @@ type Group struct {
|
|||
ServerKey string `bun:"server_key,nullzero"`
|
||||
VersionCode string `bun:"version_code,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 {
|
||||
return domain.Group{
|
||||
ID: g.ID.String(),
|
||||
ServerID: g.ServerID,
|
||||
ChannelGains: g.ChannelGains,
|
||||
ChannelLosses: g.ChannelLosses,
|
||||
Internals: g.Internals,
|
||||
Barbarians: g.Barbarians,
|
||||
ServerKey: g.ServerKey,
|
||||
VersionCode: g.VersionCode,
|
||||
CreatedAt: g.CreatedAt,
|
||||
func (g Group) ToDomain() domain.GroupWithMonitors {
|
||||
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(),
|
||||
ServerID: g.ServerID,
|
||||
ChannelGains: g.ChannelGains,
|
||||
ChannelLosses: g.ChannelLosses,
|
||||
Internals: g.Internals,
|
||||
Barbarians: g.Barbarians,
|
||||
ServerKey: g.ServerKey,
|
||||
VersionCode: g.VersionCode,
|
||||
CreatedAt: g.CreatedAt,
|
||||
},
|
||||
Monitors: monitors,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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},
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -12,20 +12,17 @@ import (
|
|||
)
|
||||
|
||||
type GroupService interface {
|
||||
Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error)
|
||||
SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error)
|
||||
SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error)
|
||||
SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error)
|
||||
SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error)
|
||||
CleanUp(ctx context.Context) error
|
||||
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error)
|
||||
Delete(ctx context.Context, id, serverID string) error
|
||||
}
|
||||
|
||||
type MonitorService interface {
|
||||
Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error)
|
||||
List(ctx context.Context, groupID, serverID string) ([]domain.MonitorWithTribe, error)
|
||||
Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error)
|
||||
AddMonitor(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error)
|
||||
DeleteMonitor(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error)
|
||||
SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error)
|
||||
SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error)
|
||||
SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.GroupWithMonitors, error)
|
||||
SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.GroupWithMonitors, 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
|
||||
}
|
||||
|
||||
|
@ -34,18 +31,16 @@ type ChoiceService interface {
|
|||
}
|
||||
|
||||
type Bot struct {
|
||||
s *discordgo.Session
|
||||
c *cron.Cron
|
||||
groupSvc GroupService
|
||||
monitorSvc MonitorService
|
||||
choiceSvc ChoiceService
|
||||
logger *zap.Logger
|
||||
s *discordgo.Session
|
||||
c *cron.Cron
|
||||
groupSvc GroupService
|
||||
choiceSvc ChoiceService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewBot(
|
||||
token string,
|
||||
groupSvc GroupService,
|
||||
monitorSvc MonitorService,
|
||||
client ChoiceService,
|
||||
logger *zap.Logger,
|
||||
) (*Bot, error) {
|
||||
|
@ -64,10 +59,9 @@ func NewBot(
|
|||
cron.SkipIfStillRunning(cron.DiscardLogger),
|
||||
),
|
||||
),
|
||||
groupSvc: groupSvc,
|
||||
monitorSvc: monitorSvc,
|
||||
choiceSvc: client,
|
||||
logger: logger,
|
||||
groupSvc: groupSvc,
|
||||
choiceSvc: client,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
b.s.AddHandler(b.handleSessionReady)
|
||||
|
@ -103,7 +97,6 @@ type command interface {
|
|||
func (b *Bot) registerCommands() error {
|
||||
commands := []command{
|
||||
&groupCommand{groupSvc: b.groupSvc, choiceSvc: b.choiceSvc},
|
||||
&monitorCommand{svc: b.monitorSvc},
|
||||
}
|
||||
|
||||
for _, c := range commands {
|
||||
|
@ -129,7 +122,7 @@ func (b *Bot) initCron() error {
|
|||
}{
|
||||
{
|
||||
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 * * *",
|
||||
|
@ -159,7 +152,8 @@ func (b *Bot) logCommands(_ *discordgo.Session, i *discordgo.InteractionCreate)
|
|||
cmdData := i.ApplicationCommandData()
|
||||
cmd := cmdData.Name
|
||||
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
|
||||
options = options[0].Options
|
||||
}
|
||||
|
|
|
@ -3,12 +3,17 @@ package discord
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
const (
|
||||
tribeTagMaxLength = 10
|
||||
)
|
||||
|
||||
type groupCommand struct {
|
||||
groupSvc GroupService
|
||||
choiceSvc ChoiceService
|
||||
|
@ -98,9 +103,69 @@ func (c *groupCommand) create(s *discordgo.Session) error {
|
|||
Description: "Lists all created groups",
|
||||
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",
|
||||
Description: "Sets various properties in a group configuration",
|
||||
Description: "Sets various properties in group configuration",
|
||||
Type: discordgo.ApplicationCommandOptionSubCommandGroup,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
|
@ -266,29 +331,27 @@ func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCrea
|
|||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch cmdData.Options[0].Name {
|
||||
case "create":
|
||||
c.handleCreate(s, i)
|
||||
return
|
||||
c.handleCreate(ctx, s, i)
|
||||
case "list":
|
||||
c.handleList(s, i)
|
||||
return
|
||||
c.handleList(ctx, s, i)
|
||||
case "details":
|
||||
c.handleDetails(ctx, s, i)
|
||||
case "set":
|
||||
c.handleSet(s, i)
|
||||
return
|
||||
c.handleSet(ctx, s, i)
|
||||
case "unset":
|
||||
c.handleUnset(s, i)
|
||||
return
|
||||
c.handleUnset(ctx, s, i)
|
||||
case "monitor":
|
||||
c.handleMonitor(ctx, s, i)
|
||||
case "delete":
|
||||
c.handleDelete(s, i)
|
||||
return
|
||||
default:
|
||||
c.handleDelete(ctx, s, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *groupCommand) handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleCreate(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
version := ""
|
||||
server := ""
|
||||
channelGains := ""
|
||||
|
@ -345,17 +408,13 @@ func (c *groupCommand) handleCreate(s *discordgo.Session, i *discordgo.Interacti
|
|||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
groups, err := c.groupSvc.List(ctx, domain.ListGroupsParams{
|
||||
ServerIDs: []string{i.GuildID},
|
||||
})
|
||||
func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
groups, err := c.groupSvc.ListServer(ctx, i.GuildID)
|
||||
if err != nil {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
|
@ -379,12 +438,13 @@ func (c *groupCommand) handleList(s *discordgo.Session, i *discordgo.Interaction
|
|||
fields = append(fields, &discordgo.MessageEmbedField{
|
||||
Name: g.ID,
|
||||
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,
|
||||
channelGains,
|
||||
channelLosses,
|
||||
boolToEmoji(g.Internals),
|
||||
boolToEmoji(g.Barbarians),
|
||||
len(g.Monitors),
|
||||
),
|
||||
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 {
|
||||
case "channel-gains":
|
||||
c.handleSetChannelGains(s, i)
|
||||
return
|
||||
c.handleSetChannelGains(ctx, s, i)
|
||||
case "channel-losses":
|
||||
c.handleSetChannelLosses(s, i)
|
||||
return
|
||||
c.handleSetChannelLosses(ctx, s, i)
|
||||
case "internals":
|
||||
c.handleSetInternals(s, i)
|
||||
return
|
||||
c.handleSetInternals(ctx, s, i)
|
||||
case "barbarians":
|
||||
c.handleSetBarbarians(s, i)
|
||||
return
|
||||
default:
|
||||
c.handleSetBarbarians(ctx, s, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *groupCommand) handleSetChannelGains(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
group := ""
|
||||
channel := ""
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleSetChannelLosses(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
group := ""
|
||||
channel := ""
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleSetInternals(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
group := ""
|
||||
internals := false
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleSetBarbarians(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
group := ""
|
||||
barbarians := false
|
||||
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 {
|
||||
case "channel-gains":
|
||||
c.handleUnsetChannelGains(s, i)
|
||||
return
|
||||
c.handleUnsetChannelGains(ctx, s, i)
|
||||
case "channel-losses":
|
||||
c.handleUnsetChannelLosses(s, i)
|
||||
return
|
||||
default:
|
||||
c.handleUnsetChannelLosses(ctx, s, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *groupCommand) handleUnsetChannelGains(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleUnsetChannelGains(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
group := i.ApplicationCommandData().Options[0].Options[0].Options[0].StringValue()
|
||||
_, err := c.groupSvc.SetChannelGains(ctx, group, i.GuildID, "")
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
func (c *groupCommand) handleUnsetChannelLosses(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
group := i.ApplicationCommandData().Options[0].Options[0].Options[0].StringValue()
|
||||
_, err := c.groupSvc.SetChannelLosses(ctx, group, i.GuildID, "")
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
func (c *groupCommand) handleMonitor(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
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()
|
||||
if err := c.groupSvc.Delete(ctx, group, i.GuildID); err != nil {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -19,7 +19,7 @@ const (
|
|||
|
||||
type executeMonitorsJob struct {
|
||||
s *discordgo.Session
|
||||
svc MonitorService
|
||||
svc GroupService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package domain
|
|||
type EnnoblementNotificationType uint8
|
||||
|
||||
const (
|
||||
EnnoblementNotificationTypeGain = iota
|
||||
EnnoblementNotificationTypeGain EnnoblementNotificationType = iota
|
||||
EnnoblementNotificationTypeLoss
|
||||
)
|
||||
|
||||
|
|
|
@ -17,6 +17,18 @@ type Group struct {
|
|||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type GroupWithMonitors struct {
|
||||
Group
|
||||
|
||||
Monitors []Monitor
|
||||
}
|
||||
|
||||
type GroupWithMonitorsAndTribes struct {
|
||||
Group
|
||||
|
||||
Monitors []MonitorWithTribe
|
||||
}
|
||||
|
||||
type CreateGroupParams struct {
|
||||
serverID string
|
||||
serverKey string
|
||||
|
@ -135,7 +147,7 @@ type GroupNotFoundError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -151,7 +163,7 @@ type GroupDoesNotExistError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -204,7 +204,7 @@ func TestGroupNotFoundError(t *testing.T) {
|
|||
ID: uuid.NewString(),
|
||||
}
|
||||
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, domain.ErrorCodeEntityNotFound, err.Code())
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ func TestGroupDoesNotExistError(t *testing.T) {
|
|||
ID: uuid.NewString(),
|
||||
}
|
||||
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, domain.ErrorCodeValidationError, err.Code())
|
||||
}
|
||||
|
|
|
@ -5,10 +5,6 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tribeIDMin = 1
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
ID string
|
||||
TribeID int64
|
||||
|
@ -21,46 +17,13 @@ type MonitorWithTribe struct {
|
|||
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 {
|
||||
TribeID int64
|
||||
GroupID 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 {
|
||||
|
@ -93,7 +56,7 @@ type MonitorNotFoundError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -9,63 +9,6 @@ import (
|
|||
"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) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -74,7 +17,7 @@ func TestMonitorAlreadyExistsError(t *testing.T) {
|
|||
TribeID: 1234,
|
||||
}
|
||||
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, domain.ErrorCodeAlreadyExists, err.Code())
|
||||
}
|
||||
|
@ -99,7 +42,7 @@ func TestMonitorNotFoundError(t *testing.T) {
|
|||
ID: uuid.NewString(),
|
||||
}
|
||||
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, domain.ErrorCodeEntityNotFound, err.Code())
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ type ServerDoesNotExistError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -66,7 +66,7 @@ type ServerIsClosedError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -82,7 +82,7 @@ type TribeDoesNotExistError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -92,3 +92,19 @@ func (e TribeDoesNotExistError) UserError() string {
|
|||
func (e TribeDoesNotExistError) Code() ErrorCode {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ func TestServerDoesNotExistError(t *testing.T) {
|
|||
Key: "pl151",
|
||||
}
|
||||
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, domain.ErrorCodeValidationError, err.Code())
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func TestServerIsClosedError(t *testing.T) {
|
|||
Key: "pl151",
|
||||
}
|
||||
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, domain.ErrorCodeValidationError, err.Code())
|
||||
}
|
||||
|
@ -41,7 +41,19 @@ func TestTribeDoesNotExistError(t *testing.T) {
|
|||
Tag: "*TAG*",
|
||||
}
|
||||
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, 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())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
|
@ -13,52 +14,58 @@ import (
|
|||
|
||||
//counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository
|
||||
type GroupRepository interface {
|
||||
Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error)
|
||||
Update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.Group, error)
|
||||
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error)
|
||||
Get(ctx context.Context, id, serverID string) (domain.Group, error)
|
||||
Delete(ctx context.Context, id, serverID string) error
|
||||
Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error)
|
||||
Update(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error)
|
||||
AddMonitor(ctx context.Context, id string, tribeID int64) (domain.GroupWithMonitors, error)
|
||||
DeleteMonitors(ctx context.Context, id string, monitorID ...string) (domain.GroupWithMonitors, 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
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
repo GroupRepository
|
||||
client TWHelpClient
|
||||
logger *zap.Logger
|
||||
maxGroupsPerServer int
|
||||
repo GroupRepository
|
||||
client TWHelpClient
|
||||
logger *zap.Logger
|
||||
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{
|
||||
repo: repo,
|
||||
client: client,
|
||||
logger: logger,
|
||||
maxGroupsPerServer: maxGroupsPerServer,
|
||||
repo: repo,
|
||||
client: client,
|
||||
logger: logger,
|
||||
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{
|
||||
ServerIDs: []string{params.ServerID()},
|
||||
})
|
||||
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 {
|
||||
return domain.Group{}, domain.GroupLimitReachedError{
|
||||
return domain.GroupWithMonitors{}, domain.GroupLimitReachedError{
|
||||
Current: len(groups),
|
||||
Limit: g.maxGroupsPerServer,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return domain.Group{}, fmt.Errorf("GroupRepository.Create: %w", err)
|
||||
return domain.GroupWithMonitors{}, fmt.Errorf("GroupRepository.Create: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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 domain.ServerDoesNotExistError{
|
||||
|
@ -87,7 +94,86 @@ func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string
|
|||
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{
|
||||
ChannelGains: domain.NullString{
|
||||
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{
|
||||
ChannelLosses: domain.NullString{
|
||||
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{
|
||||
Internals: domain.NullBool{
|
||||
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{
|
||||
Barbarians: domain.NullBool{
|
||||
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) {
|
||||
group, err := g.repo.Update(ctx, id, serverID, params)
|
||||
if err != nil {
|
||||
return domain.Group{}, fmt.Errorf("GroupRepository.Update: %w", err)
|
||||
func (g *Group) update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) {
|
||||
// check if group exists
|
||||
if _, err := g.Get(ctx, id, serverID); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) {
|
||||
groups, err := g.repo.List(ctx, params)
|
||||
func (g *Group) ListServer(ctx context.Context, serverID string) ([]domain.GroupWithMonitors, error) {
|
||||
groups, err := g.repo.List(ctx, domain.ListGroupsParams{
|
||||
ServerIDs: []string{serverID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GroupRepository.List: %w", err)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) {
|
||||
group, err := g.repo.Get(ctx, id, serverID)
|
||||
func (g *Group) Get(ctx context.Context, id, serverID string) (domain.GroupWithMonitors, error) {
|
||||
group, err := g.repo.Get(ctx, id)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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 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 {
|
||||
if err := g.deleteAllWithDisabledNotifications(ctx); err != nil {
|
||||
return err
|
||||
|
@ -175,7 +473,7 @@ func (g *Group) deleteAllWithDisabledNotifications(ctx context.Context) error {
|
|||
CreatedAtLTE: time.Now().Add(-24 * time.Hour),
|
||||
})
|
||||
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))
|
||||
|
@ -184,7 +482,7 @@ func (g *Group) deleteAllWithDisabledNotifications(ctx context.Context) error {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -204,7 +502,7 @@ func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
|
|||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -225,7 +523,7 @@ func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
|
|||
|
||||
groups, err := g.repo.List(ctx, params)
|
||||
if err != nil {
|
||||
g.logger.Warn("failed to list groups", zap.Error(err), zap.String("versionCode", v.Code))
|
||||
g.logger.Warn("couldn't list groups", zap.Error(err), zap.String("versionCode", v.Code))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -239,11 +537,7 @@ func (g *Group) deleteAllWithClosedTWServers(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if err = g.repo.DeleteMany(ctx, ids...); err != nil {
|
||||
g.logger.Warn(
|
||||
"failed to delete groups",
|
||||
zap.Error(err),
|
||||
zap.String("versionCode", v.Code),
|
||||
)
|
||||
g.logger.Warn("couldn't delete groups", zap.Error(err), zap.String("versionCode", v.Code))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ import (
|
|||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -24,9 +26,10 @@ const (
|
|||
)
|
||||
|
||||
type Client struct {
|
||||
userAgent string
|
||||
client *http.Client
|
||||
baseURL *url.URL
|
||||
userAgent string
|
||||
client *http.Client
|
||||
baseURL *url.URL
|
||||
rateLimiter *rate.Limiter
|
||||
}
|
||||
|
||||
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 {
|
||||
c := &Client{
|
||||
baseURL: baseURL,
|
||||
|
@ -216,6 +225,13 @@ func (c *Client) getJSON(ctx context.Context, urlStr string, v any) error {
|
|||
// headers
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
|
||||
// rate limiter
|
||||
if c.rateLimiter != nil {
|
||||
if err = c.rateLimiter.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client.Do: %w", err)
|
||||
|
|
|
@ -34,8 +34,10 @@ spec:
|
|||
key: token
|
||||
- name: TWHELP_URL
|
||||
value: "https://tribalwarshelp.com"
|
||||
- name: TWHELP_RATE_LIMITER_ENABLED
|
||||
value: "false"
|
||||
- name: BOT_MAX_GROUPS_PER_SERVER
|
||||
value: "10"
|
||||
value: "5"
|
||||
- name: BOT_MAX_MONITORS_PER_GROUP
|
||||
value: "10"
|
||||
livenessProbe:
|
||||
|
@ -49,4 +51,4 @@ spec:
|
|||
memory: 100Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 300Mi
|
||||
memory: 300Mi
|
||||
|
|
|
@ -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"
|
|
@ -3,4 +3,6 @@ kind: Kustomization
|
|||
nameSuffix: -dev
|
||||
resources:
|
||||
- secret.yml
|
||||
- ../../base
|
||||
- ../../base
|
||||
patchesStrategicMerge:
|
||||
- bot.yml
|
||||
|
|
Loading…
Reference in New Issue