feat: add a new command - /monitor create
All checks were successful
continuous-integration/drone/pr Build is passing
All checks were successful
continuous-integration/drone/pr Build is passing
This commit is contained in:
parent
68226b748c
commit
fb3b207a90
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
118
internal/discord/command_monitor.go
Normal file
118
internal/discord/command_monitor.go
Normal 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 + ")",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
195
internal/service/monitor_test.go
Normal file
195
internal/service/monitor_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
}
|
Loading…
Reference in New Issue
Block a user