feat: add a new command - /monitor create
All checks were successful
continuous-integration/drone/pr Build is passing

This commit is contained in:
Dawid Wysokiński 2022-10-23 08:11:50 +02:00
parent 68226b748c
commit fb3b207a90
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
10 changed files with 407 additions and 13 deletions

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 + ")",
},
})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
})
})
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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"`
}