feat: add a new command - /monitor create (#21)
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #21
This commit is contained in:
Dawid Wysokiński 2022-10-23 06:20:48 +00:00
parent 13a8da3325
commit 58f73e9ca8
25 changed files with 1234 additions and 92 deletions

View File

@ -39,15 +39,17 @@ func New() *cli.Command {
} }
groupRepo := bundb.NewGroup(db) groupRepo := bundb.NewGroup(db)
monitorRepo := bundb.NewMonitor(db)
groupSvc := service.NewGroup(groupRepo, client) groupSvc := service.NewGroup(groupRepo, client)
monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client)
cfg, err := newBotConfig() cfg, err := newBotConfig()
if err != nil { if err != nil {
return fmt.Errorf("newBotConfig: %w", err) 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 { if err != nil {
return fmt.Errorf("discord.NewBot: %w", err) return fmt.Errorf("discord.NewBot: %w", err)
} }

View File

@ -79,12 +79,35 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom
} }
result := make([]domain.Group, 0, len(groups)) result := make([]domain.Group, 0, len(groups))
for _, version := range groups { for _, group := range groups {
result = append(result, version.ToDomain()) result = append(result, group.ToDomain())
} }
return result, nil return result, nil
} }
func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) {
if _, err := uuid.Parse(id); err != nil {
return domain.Group{}, domain.GroupNotFoundError{ID: id}
}
var group model.Group
err := 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)
}
return group.ToDomain(), nil
}
func (g *Group) Delete(ctx context.Context, id, serverID string) error { func (g *Group) Delete(ctx context.Context, id, serverID string) error {
if _, err := uuid.Parse(id); err != nil { if _, err := uuid.Parse(id); err != nil {
return domain.GroupNotFoundError{ID: id} return domain.GroupNotFoundError{ID: id}

View File

@ -50,7 +50,7 @@ func TestGroup_Update(t *testing.T) {
db := newDB(t) db := newDB(t)
fixture := loadFixtures(t, db) fixture := loadFixtures(t, db)
repo := bundb.NewGroup(db) repo := bundb.NewGroup(db)
group := getGroupFromFixture(t, fixture, "group-server-1-1") group := getGroupFromFixture(t, fixture, "group-1-server-1")
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
@ -80,45 +80,47 @@ func TestGroup_Update(t *testing.T) {
assert.Zero(t, updatedGroup) assert.Zero(t, updatedGroup)
}) })
t.Run("ERR: invalid UUID", func(t *testing.T) { t.Run("ERR: group not found", func(t *testing.T) {
t.Parallel() t.Parallel()
id := "12345" tests := []struct {
updatedGroup, err := repo.Update(context.Background(), id, "", domain.UpdateGroupParams{ name string
ChannelGains: domain.NullString{ id string
String: "update", serverID string
Valid: true, }{
{
name: "ID - not UUID",
id: "test",
serverID: group.ServerID,
}, },
}) {
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: id}) name: "ID - random UUID",
assert.Zero(t, updatedGroup) id: uuid.NewString(),
}) serverID: group.ServerID,
t.Run("ERR: group not found (unknown ID)", func(t *testing.T) {
t.Parallel()
id := uuid.NewString()
updatedGroup, err := repo.Update(context.Background(), id, group.ServerID, domain.UpdateGroupParams{
ChannelGains: domain.NullString{
String: "update",
Valid: true,
}, },
}) {
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: id}) name: "ServerID - random UUID",
assert.Zero(t, updatedGroup) id: group.ID.String(),
}) serverID: uuid.NewString(),
t.Run("ERR: group not found (unknown ServerID)", func(t *testing.T) {
t.Parallel()
updatedGroup, err := repo.Update(context.Background(), group.ID.String(), uuid.NewString(), domain.UpdateGroupParams{
ChannelGains: domain.NullString{
String: "update",
Valid: true,
}, },
}) }
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: group.ID.String()})
assert.Zero(t, updatedGroup) for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
updatedGroup, err := repo.Update(context.Background(), tt.id, tt.serverID, domain.UpdateGroupParams{
ChannelGains: domain.NullString{
String: "update",
Valid: true,
},
})
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id})
assert.Zero(t, updatedGroup)
})
}
}) })
} }
@ -180,13 +182,65 @@ func TestGroup_List(t *testing.T) {
} }
} }
func TestGroup_Get(t *testing.T) {
t.Parallel()
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewGroup(db)
group := getGroupFromFixture(t, fixture, "group-1-server-1")
t.Run("OK", func(t *testing.T) {
t.Parallel()
})
t.Run("ERR: group not found (unknown ID)", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
serverID string
}{
{
name: "ID - not UUID",
id: "test",
serverID: group.ServerID,
},
{
name: "ID - random UUID",
id: uuid.NewString(),
serverID: group.ServerID,
},
{
name: "ServerID - random UUID",
id: group.ID.String(),
serverID: uuid.NewString(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g, err := repo.Get(context.Background(), tt.id, tt.serverID)
assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id})
assert.Zero(t, g)
})
}
})
}
func TestGroup_Delete(t *testing.T) { func TestGroup_Delete(t *testing.T) {
t.Parallel() t.Parallel()
db := newDB(t) db := newDB(t)
fixture := loadFixtures(t, db) fixture := loadFixtures(t, db)
repo := bundb.NewGroup(db) repo := bundb.NewGroup(db)
group := getGroupFromFixture(t, fixture, "group-server-1-1") group := getGroupFromFixture(t, fixture, "group-1-server-1")
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
@ -200,32 +254,44 @@ func TestGroup_Delete(t *testing.T) {
assert.Len(t, groups, 0) assert.Len(t, groups, 0)
}) })
t.Run("ERR: invalid UUID", func(t *testing.T) { t.Run("ERR: group not found", func(t *testing.T) {
t.Parallel() t.Parallel()
id := "12345" tests := []struct {
assert.ErrorIs(t, repo.Delete(context.Background(), id, ""), domain.GroupNotFoundError{ID: id}) name string
}) id string
serverID string
}{
{
name: "ID - not UUID",
id: "test",
serverID: group.ServerID,
},
{
name: "ID - random UUID",
id: uuid.NewString(),
serverID: group.ServerID,
},
{
name: "ServerID - random UUID",
id: group.ID.String(),
serverID: uuid.NewString(),
},
}
t.Run("ERR: group not found (unknown ID)", func(t *testing.T) { for _, tt := range tests {
t.Parallel() tt := tt
id := uuid.NewString() t.Run(tt.name, func(t *testing.T) {
assert.ErrorIs( t.Parallel()
t,
repo.Delete(context.Background(), id, group.ServerID),
domain.GroupNotFoundError{ID: id},
)
})
t.Run("ERR: group not found (unknown ServerID)", func(t *testing.T) { assert.ErrorIs(
t.Parallel() t,
repo.Delete(context.Background(), tt.id, tt.serverID),
assert.ErrorIs( domain.GroupNotFoundError{ID: tt.id},
t, )
repo.Delete(context.Background(), group.ID.String(), uuid.NewString()), })
domain.GroupNotFoundError{ID: group.ID.String()}, }
)
}) })
} }
@ -233,7 +299,7 @@ func getAllGroupsFromFixture(tb testing.TB, fixture *dbfixture.Fixture) []model.
tb.Helper() tb.Helper()
//nolint:lll //nolint:lll
ids := []string{"group-server-1-1", "group-server-1-2", "group-server-2-1", "group-server-2-2"} ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2"}
groups := make([]model.Group, 0, len(ids)) groups := make([]model.Group, 0, len(ids))
for _, id := range ids { for _, id := range ids {

View File

@ -0,0 +1,27 @@
package model
import (
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid"
"github.com/uptrace/bun"
)
type Monitor struct {
bun.BaseModel `bun:"base_model,table:monitors,alias:monitor"`
ID uuid.UUID `bun:"id,type:uuid,pk,default:gen_random_uuid()"`
GroupID uuid.UUID `bun:"group_id,type:uuid,notnull,unique:monitors_group_id_tribe_id_key"`
TribeID int64 `bun:"tribe_id,notnull,unique:monitors_group_id_tribe_id_key"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
}
func (m Monitor) ToDomain() domain.Monitor {
return domain.Monitor{
ID: m.ID.String(),
GroupID: m.GroupID.String(),
TribeID: m.TribeID,
CreatedAt: m.CreatedAt,
}
}

View File

@ -0,0 +1,31 @@
package migrations
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
if _, err := db.NewCreateTable().
Model(&model.Monitor{}).
Varchar(defaultVarcharLength).
ForeignKey(`(group_id) REFERENCES groups (id) ON DELETE CASCADE`).
Exec(ctx); err != nil {
return fmt.Errorf("couldn't create the 'monitors' table: %w", err)
}
return nil
}, func(ctx context.Context, db *bun.DB) error {
if _, err := db.NewDropTable().
Model(&model.Group{}).
IfExists().
Cascade().
Exec(ctx); err != nil {
return fmt.Errorf("couldn't drop the 'monitors' table: %w", err)
}
return nil
})
}

95
internal/bundb/monitor.go Normal file
View File

@ -0,0 +1,95 @@
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/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 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 == "23503" && constraint == "monitors_group_id_fkey":
return domain.GroupDoesNotExistError{
ID: params.GroupID(),
}
case code == "23505" && constraint == "monitors_group_id_tribe_id_key":
return domain.MonitorAlreadyExistsError{
TribeID: params.TribeID(),
GroupID: params.GroupID(),
}
default:
return err
}
}

View File

@ -0,0 +1,140 @@
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()
db := newDB(t)
fixture := loadFixtures(t, db)
repo := bundb.NewMonitor(db)
t.Run("OK", func(t *testing.T) {
t.Parallel()
group := getGroupFromFixture(t, fixture, "group-1-server-1")
params, err := domain.NewCreateMonitorParams(group.ID.String(), 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()
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: getGroupFromFixture(t, fixture, "group-1-server-1").ID.String(),
expectedMonitors: []string{
"e3017ba8-4fba-4bb1-ac8d-e4e9477a04d2",
"89719460-58ab-46b8-8682-46f161546949",
},
},
{
name: "group-2-server-2",
groupID: getGroupFromFixture(t, fixture, "group-2-server-2").ID.String(),
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)
}
})
}
}

View File

@ -1,26 +1,43 @@
- model: Group - model: Group
rows: rows:
- _id: group-server-1-1 - _id: group-1-server-1
id: d56ad37f-2637-48ea-98f8-79627f3fcc96 id: d56ad37f-2637-48ea-98f8-79627f3fcc96
server_id: server-1 server_id: server-1
server_key: pl181 server_key: pl181
version_code: pl version_code: pl
created_at: 2022-03-15T15:00:10.000Z created_at: 2022-03-15T15:00:10.000Z
- _id: group-server-1-2 - _id: group-2-server-1
id: 429b790e-7186-4106-b531-4cc4931ce2ba id: 429b790e-7186-4106-b531-4cc4931ce2ba
server_id: server-1 server_id: server-1
server_key: pl181 server_key: pl181
version_code: pl version_code: pl
created_at: 2022-03-15T15:03:10.000Z created_at: 2022-03-15T15:03:10.000Z
- _id: group-server-2-1 - _id: group-1-server-2
id: abeb6c8e-70b6-445c-989f-890cd2a1f87a id: abeb6c8e-70b6-445c-989f-890cd2a1f87a
server_id: server-2 server_id: server-2
server_key: pl181 server_key: pl181
version_code: pl version_code: pl
created_at: 2022-03-18T15:03:10.000Z created_at: 2022-03-18T15:03:10.000Z
- _id: group-server-2-2 - _id: group-2-server-2
id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7 id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7
server_id: server-2 server_id: server-2
server_key: pl181 server_key: pl181
version_code: pl version_code: pl
created_at: 2022-03-19T15:03:10.000Z created_at: 2022-03-19T15:03:10.000Z
- model: Monitor
rows:
- _id: monitor-1-group-1-server-1
id: e3017ba8-4fba-4bb1-ac8d-e4e9477a04d2
group_id: d56ad37f-2637-48ea-98f8-79627f3fcc96
tribe_id: 123
created_at: 2022-03-15T15:01:10.000Z
- _id: monitor-2-group-1-server-1
id: 89719460-58ab-46b8-8682-46f161546949
group_id: d56ad37f-2637-48ea-98f8-79627f3fcc96
tribe_id: 15
created_at: 2022-03-15T15:02:10.000Z
- _id: monitor-1-group-2-server-2
id: c7d63c3d-55f6-432e-b9d8-006f8c5ab407
group_id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7
tribe_id: 121
created_at: 2022-03-19T15:10:10.000Z

View File

@ -17,36 +17,52 @@ type GroupService interface {
Delete(ctx context.Context, id, serverID string) error 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 { type TWHelpClient interface {
ListVersions(ctx context.Context) ([]twhelp.Version, error) ListVersions(ctx context.Context) ([]twhelp.Version, error)
} }
type Bot struct { type Bot struct {
s *discordgo.Session s *discordgo.Session
groupSvc GroupService groupSvc GroupService
client TWHelpClient 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) s, err := discordgo.New("Bot " + token)
if err != nil { if err != nil {
return nil, fmt.Errorf("discordgo.New: %w", err) return nil, fmt.Errorf("discordgo.New: %w", err)
} }
if err = s.Open(); err != nil { if err = s.Open(); err != nil {
return nil, fmt.Errorf("s.Open: %w", err) 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 { if err = b.registerCommands(); err != nil {
_ = s.Close() _ = s.Close()
return nil, fmt.Errorf("couldn't register commands: %w", err) return nil, fmt.Errorf("couldn't register commands: %w", err)
} }
return b, nil return b, nil
} }
func (b *Bot) registerCommands() error { func (b *Bot) registerCommands() error {
if err := b.registerCommand(&groupCommand{svc: b.groupSvc, client: b.client}); err != nil { commands := []command{
return err &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 return nil
} }

View File

@ -35,9 +35,10 @@ func (c *groupCommand) create(s *discordgo.Session) error {
} }
var perm int64 = discordgo.PermissionAdministrator var perm int64 = discordgo.PermissionAdministrator
_, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ _, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{
Name: c.name(), Name: c.name(),
Description: "Manages monitor groups on this server", Description: "Manages groups on this server",
DefaultMemberPermissions: &perm, DefaultMemberPermissions: &perm,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{ {
@ -185,6 +186,7 @@ func (c *groupCommand) create(s *discordgo.Session) error {
if err != nil { if err != nil {
return fmt.Errorf("s.ApplicationCommandCreate: %w", err) return fmt.Errorf("s.ApplicationCommandCreate: %w", err)
} }
return nil 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

@ -1,9 +1,13 @@
package domain package domain
import "errors" import (
"errors"
"fmt"
)
var ( var (
ErrNothingToUpdate = errors.New("nothing to update") ErrNothingToUpdate = errors.New("nothing to update")
ErrRequired = errors.New("cannot be blank")
) )
type ErrorCode uint8 type ErrorCode uint8
@ -12,6 +16,7 @@ const (
ErrorCodeUnknown ErrorCode = iota ErrorCodeUnknown ErrorCode = iota
ErrorCodeEntityNotFound ErrorCodeEntityNotFound
ErrorCodeValidationError ErrorCodeValidationError
ErrorCodeAlreadyExists
) )
type UserError interface { type UserError interface {
@ -20,18 +25,39 @@ type UserError interface {
Code() ErrorCode Code() ErrorCode
} }
type RequiredError struct { type ValidationError struct {
Field string Field string
Err error
} }
func (e RequiredError) Error() string { func (e ValidationError) Error() string {
return e.Field + ": cannot be blank" return fmt.Sprintf("%s: %s", e.Field, e.Err)
} }
func (e RequiredError) UserError() string { func (e ValidationError) UserError() string {
return e.Error() return e.Error()
} }
func (e RequiredError) Code() ErrorCode { func (e ValidationError) Code() ErrorCode {
return ErrorCodeValidationError
}
func (e ValidationError) Unwrap() error {
return e.Err
}
type MinError struct {
Min int
}
func (e MinError) Error() string {
return fmt.Sprintf("must be no less than %d", e.Min)
}
func (e MinError) UserError() string {
return e.Error()
}
func (e MinError) Code() ErrorCode {
return ErrorCodeValidationError return ErrorCodeValidationError
} }

View File

@ -1,20 +1,37 @@
package domain_test package domain_test
import ( import (
"fmt"
"testing" "testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestRequiredError(t *testing.T) { func TestValidationError(t *testing.T) {
t.Parallel() t.Parallel()
err := domain.RequiredError{ err := domain.ValidationError{
Field: "123", Field: "test",
Err: domain.MinError{
Min: 25,
},
} }
var _ domain.UserError = err var _ domain.UserError = err
assert.Equal(t, err.Field+": cannot be blank", err.Error()) assert.Equal(t, fmt.Sprintf("%s: %s", err.Field, err.Err.Error()), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.ErrorIs(t, err, err.Err)
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}
func TestMinError(t *testing.T) {
t.Parallel()
err := domain.MinError{
Min: 25,
}
var _ domain.UserError = err
assert.Equal(t, "must be no less than 25", err.Error())
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
} }

View File

@ -25,20 +25,23 @@ type CreateGroupParams struct {
func NewCreateGroupParams(serverID, versionCode, serverKey, channelGains, channelLosses string) (CreateGroupParams, error) { func NewCreateGroupParams(serverID, versionCode, serverKey, channelGains, channelLosses string) (CreateGroupParams, error) {
if serverID == "" { if serverID == "" {
return CreateGroupParams{}, RequiredError{ return CreateGroupParams{}, ValidationError{
Field: "ServerID", Field: "ServerID",
Err: ErrRequired,
} }
} }
if versionCode == "" { if versionCode == "" {
return CreateGroupParams{}, RequiredError{ return CreateGroupParams{}, ValidationError{
Field: "VersionCode", Field: "VersionCode",
Err: ErrRequired,
} }
} }
if serverKey == "" { if serverKey == "" {
return CreateGroupParams{}, RequiredError{ return CreateGroupParams{}, ValidationError{
Field: "ServerKey", Field: "ServerKey",
Err: ErrRequired,
} }
} }
@ -117,3 +120,19 @@ func (e GroupNotFoundError) UserError() string {
func (e GroupNotFoundError) Code() ErrorCode { func (e GroupNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound return ErrorCodeEntityNotFound
} }
type GroupDoesNotExistError struct {
ID string
}
func (e GroupDoesNotExistError) Error() string {
return fmt.Sprintf("group (ID=%s) doesn't exist", e.ID)
}
func (e GroupDoesNotExistError) UserError() string {
return e.Error()
}
func (e GroupDoesNotExistError) Code() ErrorCode {
return ErrorCodeValidationError
}

View File

@ -34,20 +34,29 @@ func TestNewCreateGroupParams(t *testing.T) {
name: "ERR: ServerID cannot be blank", name: "ERR: ServerID cannot be blank",
versionCode: "", versionCode: "",
serverID: "", serverID: "",
err: domain.RequiredError{Field: "ServerID"}, err: domain.ValidationError{
Field: "ServerID",
Err: domain.ErrRequired,
},
}, },
{ {
name: "ERR: VersionCode cannot be blank", name: "ERR: VersionCode cannot be blank",
serverID: "1234", serverID: "1234",
versionCode: "", versionCode: "",
err: domain.RequiredError{Field: "VersionCode"}, err: domain.ValidationError{
Field: "VersionCode",
Err: domain.ErrRequired,
},
}, },
{ {
name: "ERR: ServerKey cannot be blank", name: "ERR: ServerKey cannot be blank",
serverID: "1234", serverID: "1234",
versionCode: "en", versionCode: "en",
serverKey: "", serverKey: "",
err: domain.RequiredError{Field: "ServerKey"}, err: domain.ValidationError{
Field: "ServerKey",
Err: domain.ErrRequired,
},
}, },
} }
@ -163,3 +172,15 @@ func TestGroupNotFoundError(t *testing.T) {
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code()) assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
} }
func TestGroupDoesNotExistError(t *testing.T) {
t.Parallel()
err := domain.GroupDoesNotExistError{
ID: uuid.NewString(),
}
var _ domain.UserError = err
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())
}

View File

@ -0,0 +1,84 @@
package domain
import (
"fmt"
"time"
)
const (
tribeIDMin = 1
)
type Monitor struct {
ID string
TribeID int64
GroupID string
CreatedAt time.Time
}
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)
}
func (e MonitorAlreadyExistsError) UserError() string {
return e.Error()
}
func (e MonitorAlreadyExistsError) Code() ErrorCode {
return ErrorCodeAlreadyExists
}
type MonitorLimitReachedError struct {
Current int // current number of groups
Limit int // maximum number of groups
}
func (e MonitorLimitReachedError) Error() string {
return fmt.Sprintf("monitor limit has been reached (%d/%d)", e.Current, e.Limit)
}
func (e MonitorLimitReachedError) UserError() string {
return e.Error()
}
func (e MonitorLimitReachedError) Code() ErrorCode {
return ErrorCodeValidationError
}

View File

@ -0,0 +1,93 @@
package domain_test
import (
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid"
"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()
err := domain.MonitorAlreadyExistsError{
GroupID: uuid.NewString(),
TribeID: 1234,
}
var _ domain.UserError = err
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())
}
func TestMonitorLimitReachedError(t *testing.T) {
t.Parallel()
err := domain.MonitorLimitReachedError{
Current: 10,
Limit: 10,
}
var _ domain.UserError = err
assert.Equal(t, fmt.Sprintf("monitor limit has been reached (%d/%d)", err.Current, err.Limit), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}

View File

@ -35,3 +35,19 @@ func (e ServerIsClosedError) UserError() string {
func (e ServerIsClosedError) Code() ErrorCode { func (e ServerIsClosedError) Code() ErrorCode {
return ErrorCodeValidationError return ErrorCodeValidationError
} }
type TribeDoesNotExistError struct {
Tag string
}
func (e TribeDoesNotExistError) Error() string {
return fmt.Sprintf("tribe (Tag=%s) doesn't exist", e.Tag)
}
func (e TribeDoesNotExistError) UserError() string {
return e.Error()
}
func (e TribeDoesNotExistError) Code() ErrorCode {
return ErrorCodeValidationError
}

View File

@ -33,3 +33,15 @@ func TestServerIsClosedError(t *testing.T) {
assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
} }
func TestTribeDoesNotExistError(t *testing.T) {
t.Parallel()
err := domain.TribeDoesNotExistError{
Tag: "*TAG*",
}
var _ domain.UserError = err
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())
}

View File

@ -18,6 +18,7 @@ type GroupRepository interface {
Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error)
Update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (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) 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 Delete(ctx context.Context, id, serverID string) error
} }
@ -91,6 +92,14 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom
return groups, nil return groups, nil
} }
func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) {
group, err := g.repo.Get(ctx, id, serverID)
if err != nil {
return domain.Group{}, fmt.Errorf("GroupRepository.Get: %w", err)
}
return group, nil
}
func (g *Group) Delete(ctx context.Context, id, serverID string) error { func (g *Group) Delete(ctx context.Context, id, serverID string) error {
if err := g.repo.Delete(ctx, id, serverID); err != nil { if err := g.repo.Delete(ctx, id, serverID); err != nil {
return fmt.Errorf("GroupRepository.Delete: %w", err) return fmt.Errorf("GroupRepository.Delete: %w", err)

View File

@ -0,0 +1,89 @@
package service
import (
"context"
"errors"
"fmt"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
)
const (
maxMonitorsPerGroup = 10
)
//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)
}
//counterfeiter:generate -o internal/mock/group_getter.gen.go . GroupGetter
type GroupGetter interface {
Get(ctx context.Context, id, serverID string) (domain.Group, error)
}
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)
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) >= maxMonitorsPerGroup {
return domain.Monitor{}, domain.MonitorLimitReachedError{
Current: len(monitors),
Limit: maxMonitorsPerGroup,
}
}
// 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,
}
}
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)
}
monitor, err := m.repo.Create(ctx, params)
if err != nil {
return domain.Monitor{}, fmt.Errorf("MonitorRepository.Create: %w", err)
}
return monitor, nil
}

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 //counterfeiter:generate -o internal/mock/twhelp_client.gen.go . TWHelpClient
type TWHelpClient interface { type TWHelpClient interface {
GetServer(ctx context.Context, version, server string) (twhelp.Server, error) 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" defaultUserAgent = "TWHelpDCBot/development"
defaultTimeout = 10 * time.Second defaultTimeout = 10 * time.Second
endpointListVersions = "/api/v1/versions" endpointListVersions = "/api/v1/versions"
endpointGetServer = "/api/v1/versions/%s/servers/%s" endpointGetServer = "/api/v1/versions/%s/servers/%s"
endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s"
) )
type Client struct { type Client struct {
@ -68,6 +69,14 @@ func (c *Client) GetServer(ctx context.Context, version, server string) (Server,
return resp.Data, nil 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 { func (c *Client) getJSON(ctx context.Context, urlStr string, v any) error {
u, err := c.baseURL.Parse(urlStr) u, err := c.baseURL.Parse(urlStr)
if err != nil { if err != nil {

View File

@ -1,5 +1,7 @@
package twhelp package twhelp
import "time"
type ErrorCode string type ErrorCode string
const ( const (
@ -47,3 +49,15 @@ type Server struct {
type getServerResp struct { type getServerResp struct {
Data Server `json:"data"` 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"`
}