feat: notifications
This commit is contained in:
parent
db2026c6e4
commit
e76cb17caf
|
@ -28,6 +28,11 @@ func New() *cli.Command {
|
|||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
cfg, err := newBotConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("newBotConfig: %w", err)
|
||||
}
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
|
@ -43,22 +48,18 @@ func New() *cli.Command {
|
|||
|
||||
choiceSvc := service.NewChoice(client)
|
||||
groupSvc := service.NewGroup(groupRepo, client)
|
||||
monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client)
|
||||
|
||||
cfg, err := newBotConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("newBotConfig: %w", err)
|
||||
}
|
||||
monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client, cfg.EnnoblementsReqLimit)
|
||||
|
||||
bot, err := discord.NewBot(cfg.Token, groupSvc, monitorSvc, choiceSvc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord.NewBot: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = bot.Close()
|
||||
|
||||
go func() {
|
||||
_ = bot.Run()
|
||||
}()
|
||||
|
||||
if err := os.WriteFile(healthyFilePath, []byte("healthy"), healthyFilePerm); err != nil {
|
||||
if err = os.WriteFile(healthyFilePath, []byte("healthy"), healthyFilePerm); err != nil {
|
||||
return fmt.Errorf("couldn't create file (path=%s): %w", healthyFilePath, err)
|
||||
}
|
||||
|
||||
|
@ -66,13 +67,18 @@ func New() *cli.Command {
|
|||
|
||||
waitForSignal(c.Context)
|
||||
|
||||
if err = bot.Close(); err != nil {
|
||||
return fmt.Errorf("bot.Close: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type botConfig struct {
|
||||
Token string `envconfig:"TOKEN" required:"true"`
|
||||
Token string `envconfig:"TOKEN" required:"true"`
|
||||
EnnoblementsReqLimit int `envconfig:"ENNOBLEMENTS_REQ_LIMIT" default:"4"`
|
||||
}
|
||||
|
||||
func newBotConfig() (botConfig, error) {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -43,6 +43,7 @@ require (
|
|||
github.com/opencontainers/runc v1.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -89,6 +89,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
|
|
@ -3,9 +3,11 @@ package discord
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type GroupService interface {
|
||||
|
@ -18,6 +20,7 @@ type GroupService interface {
|
|||
|
||||
type MonitorService interface {
|
||||
Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error)
|
||||
Execute(ctx context.Context) error
|
||||
Delete(ctx context.Context, id, serverID string) error
|
||||
}
|
||||
|
||||
|
@ -27,6 +30,7 @@ type ChoiceService interface {
|
|||
|
||||
type Bot struct {
|
||||
s *discordgo.Session
|
||||
c *cron.Cron
|
||||
groupSvc GroupService
|
||||
monitorSvc MonitorService
|
||||
choiceSvc ChoiceService
|
||||
|
@ -38,19 +42,46 @@ func NewBot(token string, groupSvc GroupService, monitorSvc MonitorService, clie
|
|||
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, monitorSvc: monitorSvc, choiceSvc: client}
|
||||
if err = b.registerCommands(); err != nil {
|
||||
_ = s.Close()
|
||||
return nil, fmt.Errorf("couldn't register commands: %w", err)
|
||||
b := &Bot{
|
||||
s: s,
|
||||
c: cron.New(
|
||||
cron.WithLocation(time.UTC),
|
||||
cron.WithChain(
|
||||
cron.SkipIfStillRunning(cron.DiscardLogger),
|
||||
),
|
||||
),
|
||||
groupSvc: groupSvc,
|
||||
monitorSvc: monitorSvc,
|
||||
choiceSvc: client,
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Bot) Run() error {
|
||||
if err := b.initCron(); err != nil {
|
||||
return fmt.Errorf("initCron: %w", err)
|
||||
}
|
||||
|
||||
if err := b.s.Open(); err != nil {
|
||||
return fmt.Errorf("s.Open: %w", err)
|
||||
}
|
||||
|
||||
if err := b.registerCommands(); err != nil {
|
||||
_ = b.s.Close()
|
||||
return fmt.Errorf("couldn't register commands: %w", err)
|
||||
}
|
||||
|
||||
b.c.Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type command interface {
|
||||
name() string
|
||||
register(s *discordgo.Session) error
|
||||
}
|
||||
|
||||
func (b *Bot) registerCommands() error {
|
||||
commands := []command{
|
||||
&groupCommand{groupSvc: b.groupSvc, choiceSvc: b.choiceSvc},
|
||||
|
@ -66,11 +97,6 @@ func (b *Bot) registerCommands() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type command interface {
|
||||
name() string
|
||||
register(s *discordgo.Session) error
|
||||
}
|
||||
|
||||
func (b *Bot) registerCommand(cmd command) error {
|
||||
if err := cmd.register(b.s); err != nil {
|
||||
return fmt.Errorf("couldn't register command '%s': %w", cmd.name(), err)
|
||||
|
@ -78,6 +104,15 @@ func (b *Bot) registerCommand(cmd command) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) initCron() error {
|
||||
_, err := b.c.AddJob("@every 1m", &executeMonitorsJob{svc: b.monitorSvc})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) Close() error {
|
||||
<-b.c.Stop().Done()
|
||||
return b.s.Close()
|
||||
}
|
||||
|
|
22
internal/discord/job_execute_monitors.go
Normal file
22
internal/discord/job_execute_monitors.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
executeMonitorsJobTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
type executeMonitorsJob struct {
|
||||
svc MonitorService
|
||||
}
|
||||
|
||||
func (e *executeMonitorsJob) Run() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), executeMonitorsJobTimeout)
|
||||
defer cancel()
|
||||
fmt.Println("execute")
|
||||
_ = e.svc.Execute(ctx)
|
||||
}
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
|
||||
|
@ -21,19 +23,33 @@ type MonitorRepository interface {
|
|||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate -o internal/mock/group_getter.gen.go . GroupGetter
|
||||
type GroupGetter interface {
|
||||
//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 GroupGetter
|
||||
repo MonitorRepository
|
||||
client TWHelpClient
|
||||
groupSvc GroupReader
|
||||
lastExecutionMu sync.RWMutex
|
||||
lastExecution time.Time
|
||||
ennoblementsReqLimit int
|
||||
}
|
||||
|
||||
func NewMonitor(repo MonitorRepository, groupSvc GroupGetter, client TWHelpClient) *Monitor {
|
||||
return &Monitor{repo: repo, client: client, groupSvc: groupSvc}
|
||||
func NewMonitor(
|
||||
repo MonitorRepository,
|
||||
groupSvc GroupReader,
|
||||
client TWHelpClient,
|
||||
ennoblementsReqLimit int,
|
||||
) *Monitor {
|
||||
return &Monitor{
|
||||
repo: repo,
|
||||
client: client,
|
||||
groupSvc: groupSvc,
|
||||
ennoblementsReqLimit: ennoblementsReqLimit,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) {
|
||||
|
@ -107,3 +123,110 @@ func (m *Monitor) Delete(ctx context.Context, id, serverID string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Monitor) Execute(ctx context.Context) error {
|
||||
groups, err := m.groupSvc.List(ctx, domain.ListGroupsParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GroupService.List: %w", err)
|
||||
}
|
||||
|
||||
ennoblements, err := m.preloadEnnoblements(ctx, groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, e := range ennoblements {
|
||||
fmt.Println(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listEnnoblementsResult struct {
|
||||
versionCode string
|
||||
serverKey string
|
||||
ennoblements []twhelp.Ennoblement
|
||||
err error
|
||||
}
|
||||
|
||||
type preloadEnnoblementsResult struct {
|
||||
VersionCode string
|
||||
ServerKey string
|
||||
Ennoblements []twhelp.Ennoblement
|
||||
}
|
||||
|
||||
func (m *Monitor) preloadEnnoblements(ctx context.Context, groups []domain.Group) ([]preloadEnnoblementsResult, error) {
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan listEnnoblementsResult)
|
||||
reqLimitCh := make(chan struct{}, m.ennoblementsReqLimit)
|
||||
skip := make(map[string]struct{})
|
||||
|
||||
m.lastExecutionMu.RLock()
|
||||
since := m.lastExecution
|
||||
m.lastExecutionMu.RUnlock()
|
||||
if since.IsZero() {
|
||||
since = time.Now().Add(-1 * time.Minute)
|
||||
}
|
||||
since = since.Add(1 * time.Millisecond)
|
||||
|
||||
for _, g := range groups {
|
||||
if g.ChannelGains == "" && g.ChannelLosses == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := g.VersionCode + ":" + g.ServerKey
|
||||
if _, ok := skip[key]; ok {
|
||||
continue
|
||||
}
|
||||
skip[key] = struct{}{}
|
||||
|
||||
wg.Add(1)
|
||||
go func(g domain.Group, since time.Time) {
|
||||
reqLimitCh <- struct{}{}
|
||||
defer func() {
|
||||
<-reqLimitCh
|
||||
}()
|
||||
|
||||
res := listEnnoblementsResult{
|
||||
versionCode: g.VersionCode,
|
||||
serverKey: g.ServerKey,
|
||||
}
|
||||
res.ennoblements, res.err = m.client.ListEnnoblements(
|
||||
ctx,
|
||||
g.VersionCode,
|
||||
g.ServerKey,
|
||||
twhelp.ListEnnoblementsQueryParams{
|
||||
Since: since,
|
||||
},
|
||||
)
|
||||
ch <- res
|
||||
}(g, since)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
//nolint:prealloc
|
||||
var results []preloadEnnoblementsResult
|
||||
for res := range ch {
|
||||
wg.Done()
|
||||
|
||||
if res.err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, preloadEnnoblementsResult{
|
||||
VersionCode: res.versionCode,
|
||||
ServerKey: res.serverKey,
|
||||
Ennoblements: res.ennoblements,
|
||||
})
|
||||
}
|
||||
|
||||
m.lastExecutionMu.Lock()
|
||||
m.lastExecution = time.Now()
|
||||
m.lastExecutionMu.Unlock()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
}, nil
|
||||
})
|
||||
|
||||
groupSvc := &mock.FakeGroupGetter{}
|
||||
groupSvc := &mock.FakeGroupReader{}
|
||||
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
|
||||
return domain.Group{
|
||||
ID: groupID,
|
||||
|
@ -55,7 +55,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
|
||||
groupID := uuid.NewString()
|
||||
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, client).
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, client, 10).
|
||||
Create(context.Background(), groupID, uuid.NewString(), tribe.Tag)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, monitor.ID)
|
||||
|
@ -68,10 +68,10 @@ func TestMonitor_Create(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
groupID := uuid.NewString()
|
||||
groupSvc := &mock.FakeGroupGetter{}
|
||||
groupSvc := &mock.FakeGroupReader{}
|
||||
groupSvc.GetReturns(domain.Group{}, domain.GroupNotFoundError{ID: groupID})
|
||||
|
||||
monitor, err := service.NewMonitor(nil, groupSvc, nil).
|
||||
monitor, err := service.NewMonitor(nil, groupSvc, nil, 10).
|
||||
Create(context.Background(), groupID, uuid.NewString(), "tag")
|
||||
assert.ErrorIs(t, err, domain.GroupDoesNotExistError{
|
||||
ID: groupID,
|
||||
|
@ -87,7 +87,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
repo := &mock.FakeMonitorRepository{}
|
||||
repo.ListReturns(make([]domain.Monitor, maxMonitorsPerGroup), nil)
|
||||
|
||||
groupSvc := &mock.FakeGroupGetter{}
|
||||
groupSvc := &mock.FakeGroupReader{}
|
||||
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
|
||||
return domain.Group{
|
||||
ID: groupID,
|
||||
|
@ -100,7 +100,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
}, nil
|
||||
})
|
||||
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, nil).
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, nil, 10).
|
||||
Create(context.Background(), uuid.NewString(), uuid.NewString(), "TAG")
|
||||
assert.ErrorIs(t, err, domain.MonitorLimitReachedError{
|
||||
Current: maxMonitorsPerGroup,
|
||||
|
@ -126,7 +126,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
}, nil
|
||||
})
|
||||
|
||||
groupSvc := &mock.FakeGroupGetter{}
|
||||
groupSvc := &mock.FakeGroupReader{}
|
||||
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
|
||||
return domain.Group{
|
||||
ID: groupID,
|
||||
|
@ -147,7 +147,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
|
||||
tag := "TAG"
|
||||
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, client).
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, client, 10).
|
||||
Create(context.Background(), uuid.NewString(), uuid.NewString(), tag)
|
||||
assert.ErrorIs(t, err, domain.TribeDoesNotExistError{
|
||||
Tag: tag,
|
||||
|
@ -161,7 +161,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
repo := &mock.FakeMonitorRepository{}
|
||||
repo.ListReturns(nil, nil)
|
||||
|
||||
groupSvc := &mock.FakeGroupGetter{}
|
||||
groupSvc := &mock.FakeGroupReader{}
|
||||
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
|
||||
return domain.Group{
|
||||
ID: groupID,
|
||||
|
@ -184,7 +184,7 @@ func TestMonitor_Create(t *testing.T) {
|
|||
}
|
||||
client.GetTribeByTagReturns(tribe, nil)
|
||||
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, client).
|
||||
monitor, err := service.NewMonitor(repo, groupSvc, client, 10).
|
||||
Create(context.Background(), uuid.NewString(), uuid.NewString(), tribe.Tag)
|
||||
assert.ErrorIs(t, err, domain.TribeDoesNotExistError{
|
||||
Tag: tribe.Tag,
|
||||
|
|
|
@ -13,4 +13,9 @@ type TWHelpClient interface {
|
|||
ListVersions(ctx context.Context) ([]twhelp.Version, error)
|
||||
GetServer(ctx context.Context, version, server string) (twhelp.Server, error)
|
||||
GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error)
|
||||
ListEnnoblements(
|
||||
ctx context.Context,
|
||||
version, server string,
|
||||
queryParams twhelp.ListEnnoblementsQueryParams,
|
||||
) ([]twhelp.Ennoblement, error)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -14,9 +15,10 @@ const (
|
|||
defaultUserAgent = "TWHelpDCBot/development"
|
||||
defaultTimeout = 10 * time.Second
|
||||
|
||||
endpointListVersions = "/api/v1/versions"
|
||||
endpointGetServer = "/api/v1/versions/%s/servers/%s"
|
||||
endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s"
|
||||
endpointListVersions = "/api/v1/versions"
|
||||
endpointGetServer = "/api/v1/versions/%s/servers/%s"
|
||||
endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s"
|
||||
endpointListEnnoblements = "/api/v1/versions/%s/servers/%s/ennoblements"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
@ -77,6 +79,40 @@ func (c *Client) GetTribeByTag(ctx context.Context, version, server, tag string)
|
|||
return resp.Data, nil
|
||||
}
|
||||
|
||||
type ListEnnoblementsQueryParams struct {
|
||||
Limit int32
|
||||
Offset int32
|
||||
Since time.Time
|
||||
}
|
||||
|
||||
func (c *Client) ListEnnoblements(
|
||||
ctx context.Context,
|
||||
version, server string,
|
||||
queryParams ListEnnoblementsQueryParams,
|
||||
) ([]Ennoblement, error) {
|
||||
q := url.Values{}
|
||||
|
||||
if queryParams.Limit >= 0 {
|
||||
q.Set("limit", strconv.Itoa(int(queryParams.Limit)))
|
||||
}
|
||||
|
||||
if queryParams.Offset >= 0 {
|
||||
q.Set("offset", strconv.Itoa(int(queryParams.Offset)))
|
||||
}
|
||||
|
||||
if !queryParams.Since.IsZero() {
|
||||
q.Set("since", queryParams.Since.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
var resp listEnnoblementsResp
|
||||
|
||||
if err := c.getJSON(ctx, fmt.Sprintf(endpointListEnnoblements, version, server)+"?"+q.Encode(), &resp); err != nil {
|
||||
return nil, 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 {
|
||||
|
|
154
internal/twhelp/twhelp.go
Normal file
154
internal/twhelp/twhelp.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package twhelp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrorCodeInternalServerError ErrorCode = "internal-server-error"
|
||||
ErrorCodeEntityNotFound ErrorCode = "entity-not-found"
|
||||
ErrorCodeValidationError ErrorCode = "validation-error"
|
||||
ErrorCodeRouteNotFound ErrorCode = "route-not-found"
|
||||
ErrorCodeMethodNotAllowed ErrorCode = "method-not-allowed"
|
||||
)
|
||||
|
||||
func (c ErrorCode) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
return e.Message + " (code=" + e.Code.String() + ")"
|
||||
}
|
||||
|
||||
type errorResp struct {
|
||||
Error APIError `json:"error"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Code string `json:"code"`
|
||||
Host string `json:"host"`
|
||||
Name string `json:"name"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type listVersionsResp struct {
|
||||
Data []Version `json:"data"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Key string `json:"key"`
|
||||
URL string `json:"url"`
|
||||
Open bool `json:"open"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type TribeMeta struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag"`
|
||||
ProfileURL string `json:"profileUrl"`
|
||||
}
|
||||
|
||||
type NullTribeMeta struct {
|
||||
Tribe TribeMeta
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func (t NullTribeMeta) MarshalJSON() ([]byte, error) {
|
||||
if !t.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
return json.Marshal(t.Tribe)
|
||||
}
|
||||
|
||||
func (t *NullTribeMeta) UnmarshalJSON(data []byte) error {
|
||||
// Ignore null, like in the main JSON package.
|
||||
if string(data) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &t.Tribe); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Valid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PlayerMeta struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProfileURL string `json:"profileUrl"`
|
||||
Tribe NullTribeMeta `json:"tribe"`
|
||||
}
|
||||
|
||||
type NullPlayerMeta struct {
|
||||
Player PlayerMeta
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func (p NullPlayerMeta) MarshalJSON() ([]byte, error) {
|
||||
if !p.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
return json.Marshal(p.Player)
|
||||
}
|
||||
|
||||
func (p *NullPlayerMeta) UnmarshalJSON(data []byte) error {
|
||||
// Ignore null, like in the main JSON package.
|
||||
if string(data) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &p.Player); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Valid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type VillageMeta struct {
|
||||
ID int64 `json:"id"`
|
||||
FullName string `json:"fullName"`
|
||||
ProfileURL string `json:"profileUrl"`
|
||||
}
|
||||
|
||||
type Ennoblement struct {
|
||||
ID int64 `json:"id"`
|
||||
Village VillageMeta `json:"village"`
|
||||
NewOwner NullPlayerMeta `json:"newOwner"`
|
||||
OldOwner NullPlayerMeta `json:"oldOwner"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type listEnnoblementsResp struct {
|
||||
Data []Ennoblement `json:"data"`
|
||||
}
|
258
internal/twhelp/twhelp_test.go
Normal file
258
internal/twhelp/twhelp_test.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
package twhelp_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNullTribeMeta_MarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tribe twhelp.NullTribeMeta
|
||||
expectedJSON string
|
||||
}{
|
||||
{
|
||||
name: "OK: null 1",
|
||||
tribe: twhelp.NullTribeMeta{
|
||||
Tribe: twhelp.TribeMeta{},
|
||||
Valid: false,
|
||||
},
|
||||
expectedJSON: "null",
|
||||
},
|
||||
{
|
||||
name: "OK: null 2",
|
||||
tribe: twhelp.NullTribeMeta{
|
||||
Tribe: twhelp.TribeMeta{ID: 1234},
|
||||
Valid: false,
|
||||
},
|
||||
expectedJSON: "null",
|
||||
},
|
||||
{
|
||||
name: "OK: valid struct",
|
||||
tribe: twhelp.NullTribeMeta{
|
||||
Tribe: twhelp.TribeMeta{
|
||||
ID: 997,
|
||||
Name: "name 997",
|
||||
Tag: "tag 997",
|
||||
ProfileURL: "profile-997",
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
expectedJSON: `{"id":997,"name":"name 997","tag":"tag 997","profileUrl":"profile-997"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b, err := json.Marshal(tt.tribe)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, tt.expectedJSON, string(b))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullTribeMeta_UnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expectedTribe twhelp.NullTribeMeta
|
||||
expectedJSONSyntaxError bool
|
||||
}{
|
||||
{
|
||||
name: "OK: null",
|
||||
json: "null",
|
||||
expectedTribe: twhelp.NullTribeMeta{
|
||||
Tribe: twhelp.TribeMeta{},
|
||||
Valid: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: valid struct",
|
||||
//nolint:lll
|
||||
json: `{"id":997,"name":"name 997","tag":"tag 997","profileUrl":"profile-997"}`,
|
||||
expectedTribe: twhelp.NullTribeMeta{
|
||||
Tribe: twhelp.TribeMeta{
|
||||
ID: 997,
|
||||
Name: "name 997",
|
||||
Tag: "tag 997",
|
||||
ProfileURL: "profile-997",
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: invalid tribe 1",
|
||||
json: "2022-07-30T14:13:12.0000005Z",
|
||||
expectedTribe: twhelp.NullTribeMeta{},
|
||||
expectedJSONSyntaxError: true,
|
||||
},
|
||||
{
|
||||
name: "ERR: invalid tribe 2",
|
||||
json: "hello world",
|
||||
expectedTribe: twhelp.NullTribeMeta{},
|
||||
expectedJSONSyntaxError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var target twhelp.NullTribeMeta
|
||||
err := json.Unmarshal([]byte(tt.json), &target)
|
||||
if tt.expectedJSONSyntaxError {
|
||||
var syntaxError *json.SyntaxError
|
||||
assert.ErrorAs(t, err, &syntaxError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.expectedTribe, target)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullPlayerMeta_MarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
player twhelp.NullPlayerMeta
|
||||
expectedJSON string
|
||||
}{
|
||||
{
|
||||
name: "OK: null 1",
|
||||
player: twhelp.NullPlayerMeta{
|
||||
Player: twhelp.PlayerMeta{},
|
||||
Valid: false,
|
||||
},
|
||||
expectedJSON: "null",
|
||||
},
|
||||
{
|
||||
name: "OK: null 2",
|
||||
player: twhelp.NullPlayerMeta{
|
||||
Player: twhelp.PlayerMeta{ID: 1234},
|
||||
Valid: false,
|
||||
},
|
||||
expectedJSON: "null",
|
||||
},
|
||||
{
|
||||
name: "OK: valid struct",
|
||||
player: twhelp.NullPlayerMeta{
|
||||
Player: twhelp.PlayerMeta{
|
||||
ID: 997,
|
||||
Name: "name 997",
|
||||
ProfileURL: "profile-997",
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Valid: true,
|
||||
Tribe: twhelp.TribeMeta{
|
||||
ID: 1234,
|
||||
Name: "name 997",
|
||||
ProfileURL: "profile-997",
|
||||
Tag: "tag 997",
|
||||
},
|
||||
},
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
//nolint:lll
|
||||
expectedJSON: `{"id":997,"name":"name 997","profileUrl":"profile-997","tribe":{"id":1234,"name":"name 997","profileUrl":"profile-997","tag":"tag 997"}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b, err := json.Marshal(tt.player)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, tt.expectedJSON, string(b))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullPlayerMeta_UnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expectedPlayer twhelp.NullPlayerMeta
|
||||
expectedJSONSyntaxError bool
|
||||
}{
|
||||
{
|
||||
name: "OK: null",
|
||||
json: "null",
|
||||
expectedPlayer: twhelp.NullPlayerMeta{
|
||||
Player: twhelp.PlayerMeta{},
|
||||
Valid: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: valid struct",
|
||||
//nolint:lll
|
||||
json: `{"id":997,"name":"name 997","profileUrl":"profile-997","tribe":{"id":1234,"name":"name 997","profileUrl":"profile-997","tag":"tag 997"}}`,
|
||||
expectedPlayer: twhelp.NullPlayerMeta{
|
||||
Player: twhelp.PlayerMeta{
|
||||
ID: 997,
|
||||
Name: "name 997",
|
||||
ProfileURL: "profile-997",
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Valid: true,
|
||||
Tribe: twhelp.TribeMeta{
|
||||
ID: 1234,
|
||||
Name: "name 997",
|
||||
ProfileURL: "profile-997",
|
||||
Tag: "tag 997",
|
||||
},
|
||||
},
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: invalid tribe 1",
|
||||
json: "2022-07-30T14:13:12.0000005Z",
|
||||
expectedPlayer: twhelp.NullPlayerMeta{},
|
||||
expectedJSONSyntaxError: true,
|
||||
},
|
||||
{
|
||||
name: "ERR: invalid tribe 2",
|
||||
json: "hello world",
|
||||
expectedPlayer: twhelp.NullPlayerMeta{},
|
||||
expectedJSONSyntaxError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var target twhelp.NullPlayerMeta
|
||||
err := json.Unmarshal([]byte(tt.json), &target)
|
||||
if tt.expectedJSONSyntaxError {
|
||||
var syntaxError *json.SyntaxError
|
||||
assert.ErrorAs(t, err, &syntaxError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.expectedPlayer, target)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package twhelp
|
||||
|
||||
import "time"
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrorCodeInternalServerError ErrorCode = "internal-server-error"
|
||||
ErrorCodeEntityNotFound ErrorCode = "entity-not-found"
|
||||
ErrorCodeValidationError ErrorCode = "validation-error"
|
||||
ErrorCodeRouteNotFound ErrorCode = "route-not-found"
|
||||
ErrorCodeMethodNotAllowed ErrorCode = "method-not-allowed"
|
||||
)
|
||||
|
||||
func (c ErrorCode) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
return e.Message + " (code=" + e.Code.String() + ")"
|
||||
}
|
||||
|
||||
type errorResp struct {
|
||||
Error APIError `json:"error"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Code string `json:"code"`
|
||||
Host string `json:"host"`
|
||||
Name string `json:"name"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type listVersionsResp struct {
|
||||
Data []Version `json:"data"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Key string `json:"key"`
|
||||
URL string `json:"url"`
|
||||
Open bool `json:"open"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
|
@ -34,6 +34,8 @@ spec:
|
|||
key: token
|
||||
- name: TWHELP_URL
|
||||
value: "https://tribalwarshelp.com"
|
||||
- name: BOT_ENNOBLEMENTS_REQ_LIMIT
|
||||
value: "4"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: [ "cat", "/tmp/healthy" ]
|
||||
|
|
Loading…
Reference in New Issue
Block a user