From fb3b207a90ac6ba504e512023c41cc9be7d26c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 23 Oct 2022 08:11:50 +0200 Subject: [PATCH] feat: add a new command - /monitor create --- cmd/dcbot/internal/run/run.go | 4 +- internal/discord/bot.go | 30 +++- internal/discord/command_group.go | 4 +- internal/discord/command_monitor.go | 118 ++++++++++++++ internal/domain/tw.go | 16 ++ internal/service/monitor.go | 25 ++- internal/service/monitor_test.go | 195 ++++++++++++++++++++++++ internal/service/service.go | 1 + internal/twhelp/client.go | 13 +- internal/twhelp/{models.go => types.go} | 14 ++ 10 files changed, 407 insertions(+), 13 deletions(-) create mode 100644 internal/discord/command_monitor.go create mode 100644 internal/service/monitor_test.go rename internal/twhelp/{models.go => types.go} (78%) diff --git a/cmd/dcbot/internal/run/run.go b/cmd/dcbot/internal/run/run.go index 1e4d12f..b0d91f1 100644 --- a/cmd/dcbot/internal/run/run.go +++ b/cmd/dcbot/internal/run/run.go @@ -39,15 +39,17 @@ func New() *cli.Command { } groupRepo := bundb.NewGroup(db) + monitorRepo := bundb.NewMonitor(db) groupSvc := service.NewGroup(groupRepo, client) + monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client) cfg, err := newBotConfig() if err != nil { return fmt.Errorf("newBotConfig: %w", err) } - bot, err := discord.NewBot(cfg.Token, groupSvc, client) + bot, err := discord.NewBot(cfg.Token, groupSvc, monitorSvc, client) if err != nil { return fmt.Errorf("discord.NewBot: %w", err) } diff --git a/internal/discord/bot.go b/internal/discord/bot.go index 64c009b..39573e9 100644 --- a/internal/discord/bot.go +++ b/internal/discord/bot.go @@ -17,36 +17,52 @@ type GroupService interface { Delete(ctx context.Context, id, serverID string) error } +type MonitorService interface { + Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) +} + type TWHelpClient interface { ListVersions(ctx context.Context) ([]twhelp.Version, error) } type Bot struct { - s *discordgo.Session - groupSvc GroupService - client TWHelpClient + s *discordgo.Session + groupSvc GroupService + monitorSvc MonitorService + client TWHelpClient } -func NewBot(token string, groupSvc GroupService, client TWHelpClient) (*Bot, error) { +func NewBot(token string, groupSvc GroupService, monitorSvc MonitorService, client TWHelpClient) (*Bot, error) { s, err := discordgo.New("Bot " + token) if err != nil { return nil, fmt.Errorf("discordgo.New: %w", err) } + if err = s.Open(); err != nil { return nil, fmt.Errorf("s.Open: %w", err) } - b := &Bot{s: s, groupSvc: groupSvc, client: client} + + b := &Bot{s: s, groupSvc: groupSvc, monitorSvc: monitorSvc, client: client} if err = b.registerCommands(); err != nil { _ = s.Close() return nil, fmt.Errorf("couldn't register commands: %w", err) } + return b, nil } func (b *Bot) registerCommands() error { - if err := b.registerCommand(&groupCommand{svc: b.groupSvc, client: b.client}); err != nil { - return err + commands := []command{ + &groupCommand{svc: b.groupSvc, client: b.client}, + &monitorCommand{svc: b.monitorSvc}, } + + for _, c := range commands { + if err := b.registerCommand(c); err != nil { + return err + } + } + return nil } diff --git a/internal/discord/command_group.go b/internal/discord/command_group.go index 8be129f..b7e820b 100644 --- a/internal/discord/command_group.go +++ b/internal/discord/command_group.go @@ -35,9 +35,10 @@ func (c *groupCommand) create(s *discordgo.Session) error { } var perm int64 = discordgo.PermissionAdministrator + _, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ Name: c.name(), - Description: "Manages monitor groups on this server", + Description: "Manages groups on this server", DefaultMemberPermissions: &perm, Options: []*discordgo.ApplicationCommandOption{ { @@ -185,6 +186,7 @@ func (c *groupCommand) create(s *discordgo.Session) error { if err != nil { return fmt.Errorf("s.ApplicationCommandCreate: %w", err) } + return nil } diff --git a/internal/discord/command_monitor.go b/internal/discord/command_monitor.go new file mode 100644 index 0000000..7df2ca8 --- /dev/null +++ b/internal/discord/command_monitor.go @@ -0,0 +1,118 @@ +package discord + +import ( + "context" + "fmt" + + "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 + + _, err := s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ + Name: c.name(), + Description: "Manages monitors", + DefaultMemberPermissions: &perm, + 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, + }, + }, + }, + }, + }) + 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 + 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 + ")", + }, + }) +} diff --git a/internal/domain/tw.go b/internal/domain/tw.go index 4a67a14..749389d 100644 --- a/internal/domain/tw.go +++ b/internal/domain/tw.go @@ -35,3 +35,19 @@ func (e ServerIsClosedError) UserError() string { func (e ServerIsClosedError) Code() ErrorCode { return ErrorCodeValidationError } + +type TribeDoesNotExistError struct { + Tag string +} + +func (e TribeDoesNotExistError) Error() string { + return fmt.Sprintf("tribe (Tag=%s) doens't exist", e.Tag) +} + +func (e TribeDoesNotExistError) UserError() string { + return e.Error() +} + +func (e TribeDoesNotExistError) Code() ErrorCode { + return ErrorCodeValidationError +} diff --git a/internal/service/monitor.go b/internal/service/monitor.go index 3a8cca6..0b93813 100644 --- a/internal/service/monitor.go +++ b/internal/service/monitor.go @@ -6,6 +6,7 @@ import ( "fmt" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" ) const ( @@ -25,9 +26,14 @@ type GroupGetter interface { type Monitor struct { repo MonitorRepository + client TWHelpClient groupSvc GroupGetter } +func NewMonitor(repo MonitorRepository, groupSvc GroupGetter, client TWHelpClient) *Monitor { + return &Monitor{repo: repo, client: client, groupSvc: groupSvc} +} + 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) @@ -52,9 +58,24 @@ func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string } // check if tribe exists + tribe, err := m.client.GetTribeByTag(ctx, group.VersionCode, group.ServerKey, tribeTag) + 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, + } + } - const temp = 123 - params, err := domain.NewCreateMonitorParams(group.ID, temp) + if !tribe.DeletedAt.IsZero() { + return domain.Monitor{}, domain.TribeDoesNotExistError{ + Tag: tribeTag, + } + } + + params, err := domain.NewCreateMonitorParams(group.ID, tribe.ID) if err != nil { return domain.Monitor{}, fmt.Errorf("domain.NewCreateMonitorParams: %w", err) } diff --git a/internal/service/monitor_test.go b/internal/service/monitor_test.go new file mode 100644 index 0000000..7a18247 --- /dev/null +++ b/internal/service/monitor_test.go @@ -0,0 +1,195 @@ +package service_test + +import ( + "context" + "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" +) + +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.FakeGroupGetter{} + 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.GetTribeByTagReturns(tribe, nil) + + groupID := uuid.NewString() + + monitor, err := service.NewMonitor(repo, groupSvc, client). + 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.FakeGroupGetter{} + groupSvc.GetReturns(domain.Group{}, domain.GroupNotFoundError{ID: groupID}) + + monitor, err := service.NewMonitor(nil, groupSvc, nil). + 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.FakeGroupGetter{} + 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). + 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() + + t.Run("API error", 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.FakeGroupGetter{} + 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.GetTribeByTagReturns(twhelp.Tribe{}, twhelp.APIError{ + Code: twhelp.ErrorCodeEntityNotFound, + Message: "tribe not found", + }) + + tag := "TAG" + + monitor, err := service.NewMonitor(repo, groupSvc, client). + Create(context.Background(), uuid.NewString(), uuid.NewString(), tag) + assert.ErrorIs(t, err, domain.TribeDoesNotExistError{ + Tag: tag, + }) + assert.Zero(t, monitor) + }) + + t.Run("tribe is deleted", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeMonitorRepository{} + repo.ListReturns(nil, nil) + + groupSvc := &mock.FakeGroupGetter{} + 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.Now(), + } + client.GetTribeByTagReturns(tribe, nil) + + monitor, err := service.NewMonitor(repo, groupSvc, client). + Create(context.Background(), uuid.NewString(), uuid.NewString(), tribe.Tag) + assert.ErrorIs(t, err, domain.TribeDoesNotExistError{ + Tag: tribe.Tag, + }) + assert.Zero(t, monitor) + }) + }) +} diff --git a/internal/service/service.go b/internal/service/service.go index 3555d28..8abd446 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -11,4 +11,5 @@ import ( //counterfeiter:generate -o internal/mock/twhelp_client.gen.go . TWHelpClient type TWHelpClient interface { GetServer(ctx context.Context, version, server string) (twhelp.Server, error) + GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error) } diff --git a/internal/twhelp/client.go b/internal/twhelp/client.go index 4d54b79..04b5b43 100644 --- a/internal/twhelp/client.go +++ b/internal/twhelp/client.go @@ -14,8 +14,9 @@ const ( defaultUserAgent = "TWHelpDCBot/development" defaultTimeout = 10 * time.Second - endpointListVersions = "/api/v1/versions" - endpointGetServer = "/api/v1/versions/%s/servers/%s" + endpointListVersions = "/api/v1/versions" + endpointGetServer = "/api/v1/versions/%s/servers/%s" + endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s" ) type Client struct { @@ -68,6 +69,14 @@ func (c *Client) GetServer(ctx context.Context, version, server string) (Server, return resp.Data, nil } +func (c *Client) GetTribeByTag(ctx context.Context, version, server, tag string) (Tribe, error) { + var resp getTribeResp + if err := c.getJSON(ctx, fmt.Sprintf(endpointGetTribeByTag, version, server, tag), &resp); err != nil { + return Tribe{}, err + } + return resp.Data, nil +} + func (c *Client) getJSON(ctx context.Context, urlStr string, v any) error { u, err := c.baseURL.Parse(urlStr) if err != nil { diff --git a/internal/twhelp/models.go b/internal/twhelp/types.go similarity index 78% rename from internal/twhelp/models.go rename to internal/twhelp/types.go index 6505e19..f9b2615 100644 --- a/internal/twhelp/models.go +++ b/internal/twhelp/types.go @@ -1,5 +1,7 @@ package twhelp +import "time" + type ErrorCode string const ( @@ -47,3 +49,15 @@ type Server struct { type getServerResp struct { Data Server `json:"data"` } + +type Tribe struct { + ID int64 `json:"id"` + Tag string `json:"tag"` + Name string `json:"name"` + ProfileURL string `json:"profileUrl"` + DeletedAt time.Time `json:"deletedAt"` +} + +type getTribeResp struct { + Data Tribe `json:"data"` +}