feat: notifications #25
|
@ -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.EnnoblementsMaxConcurrentRequests)
|
||||
|
||||
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"`
|
||||
EnnoblementsMaxConcurrentRequests int `envconfig:"ENNOBLEMENTS_MAX_CONCURRENT_REQUESTS" default:"4"`
|
||||
}
|
||||
|
||||
func newBotConfig() (botConfig, error) {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
|||
github.com/google/uuid v1.3.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/uptrace/bun v1.1.8
|
||||
github.com/uptrace/bun/dbfixture v1.1.8
|
||||
|
|
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=
|
||||
|
|
|
@ -182,11 +182,9 @@ func TestMonitor_Get(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.ErrorIs(
|
||||
t,
|
||||
repo.Delete(context.Background(), tt.id),
|
||||
domain.MonitorNotFoundError{ID: tt.id},
|
||||
)
|
||||
monitor, err := repo.Get(context.Background(), tt.id)
|
||||
assert.ErrorIs(t, err, domain.MonitorNotFoundError{ID: tt.id})
|
||||
assert.Zero(t, monitor)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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) ([]domain.EnnoblementNotification, 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, s: b.s})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) Close() error {
|
||||
<-b.c.Stop().Done()
|
||||
return b.s.Close()
|
||||
}
|
||||
|
|
|
@ -317,7 +317,7 @@ func (c *groupCommand) handleList(s *discordgo.Session, i *discordgo.Interaction
|
|||
Type: discordgo.EmbedTypeRich,
|
||||
Title: "Group list",
|
||||
Description: buildGroupListDescription(groups),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Timestamp: formatTimestamp(time.Now()),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,10 +2,16 @@ package discord
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
embedDescriptionCharLimit = 4096
|
||||
)
|
||||
|
||||
func messageFromError(err error) string {
|
||||
var userErr domain.UserError
|
||||
if !errors.As(err, &userErr) {
|
||||
|
@ -13,3 +19,14 @@ func messageFromError(err error) string {
|
|||
}
|
||||
return userErr.UserError()
|
||||
}
|
||||
|
||||
func formatTimestamp(t time.Time) string {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func buildLink(text string, url string) string {
|
||||
if url == "" {
|
||||
return text
|
||||
}
|
||||
return fmt.Sprintf("[`%s`](%s)", text, url)
|
||||
}
|
||||
|
|
126
internal/discord/job_execute_monitors.go
Normal file
126
internal/discord/job_execute_monitors.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
const (
|
||||
executeMonitorsJobTimeout = 2 * time.Minute
|
||||
colorGreen = 0x00ff00
|
||||
colorRed = 0xff0000
|
||||
)
|
||||
|
||||
type executeMonitorsJob struct {
|
||||
s *discordgo.Session
|
||||
svc MonitorService
|
||||
}
|
||||
|
||||
func (e *executeMonitorsJob) Run() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), executeMonitorsJobTimeout)
|
||||
defer cancel()
|
||||
|
||||
notifications, err := e.svc.Execute(ctx)
|
||||
if err != nil {
|
||||
// TODO: log this error
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for ch, embeds := range ennoblementNotificationsToEmbeds(notifications) {
|
||||
wg.Add(1)
|
||||
|
||||
go func(ch string, embeds []*discordgo.MessageEmbed) {
|
||||
defer wg.Done()
|
||||
for _, embed := range embeds {
|
||||
_, _ = e.s.ChannelMessageSendEmbed(ch, embed)
|
||||
}
|
||||
}(ch, embeds)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func ennoblementNotificationsToEmbeds(ns []domain.EnnoblementNotification) map[string][]*discordgo.MessageEmbed {
|
||||
m := make(map[string]map[domain.EnnoblementNotificationType][]*discordgo.MessageEmbed)
|
||||
timestamp := formatTimestamp(time.Now())
|
||||
for _, n := range ns {
|
||||
if _, ok := m[n.ChannelID]; !ok {
|
||||
m[n.ChannelID] = make(map[domain.EnnoblementNotificationType][]*discordgo.MessageEmbed)
|
||||
}
|
||||
|
||||
embeds := m[n.ChannelID][n.Type]
|
||||
|
||||
str := buildEnnoblementNotificationDescription(n)
|
||||
if l := len(embeds); l == 0 || len(embeds[l-1].Description)+len(str) > embedDescriptionCharLimit {
|
||||
embeds = append(embeds, &discordgo.MessageEmbed{
|
||||
Color: ennoblementNotificationTypeToColor(n.Type),
|
||||
Type: discordgo.EmbedTypeRich,
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
if len(embeds[len(embeds)-1].Description) > 0 {
|
||||
str = "\n" + str
|
||||
}
|
||||
embeds[len(embeds)-1].Description += str
|
||||
|
||||
m[n.ChannelID][n.Type] = embeds
|
||||
}
|
||||
|
||||
res := make(map[string][]*discordgo.MessageEmbed, len(m))
|
||||
for ch, innerMap := range m {
|
||||
cnt := 0
|
||||
for _, embeds := range innerMap {
|
||||
cnt += len(embeds)
|
||||
}
|
||||
|
||||
res[ch] = make([]*discordgo.MessageEmbed, 0, cnt)
|
||||
for _, embeds := range innerMap {
|
||||
res[ch] = append(res[ch], embeds...)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func buildEnnoblementNotificationDescription(n domain.EnnoblementNotification) string {
|
||||
return fmt.Sprintf(
|
||||
"%s has taken %s (Old owner: %s)",
|
||||
nullPlayerMetaToMarkdown(n.Ennoblement.NewOwner),
|
||||
buildLink(n.Ennoblement.Village.FullName, n.Ennoblement.Village.ProfileURL),
|
||||
nullPlayerMetaToMarkdown(n.Ennoblement.OldOwner),
|
||||
)
|
||||
}
|
||||
|
||||
func nullPlayerMetaToMarkdown(p domain.NullPlayerMeta) string {
|
||||
if !p.Valid {
|
||||
return "Barbarians"
|
||||
}
|
||||
|
||||
tag := p.Player.Tribe.Tribe.Tag
|
||||
if tag == "" {
|
||||
tag = "-"
|
||||
}
|
||||
|
||||
return buildLink(p.Player.Name, p.Player.ProfileURL) +
|
||||
" [" +
|
||||
buildLink(tag, p.Player.Tribe.Tribe.ProfileURL) +
|
||||
"]"
|
||||
}
|
||||
|
||||
func ennoblementNotificationTypeToColor(t domain.EnnoblementNotificationType) int {
|
||||
switch t {
|
||||
case domain.EnnoblementNotificationTypeGain:
|
||||
return colorGreen
|
||||
case domain.EnnoblementNotificationTypeLoss:
|
||||
fallthrough
|
||||
default:
|
||||
return colorRed
|
||||
}
|
||||
}
|
15
internal/domain/ennoblement_notification.go
Normal file
15
internal/domain/ennoblement_notification.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package domain
|
||||
|
||||
type EnnoblementNotificationType uint8
|
||||
|
||||
const (
|
||||
EnnoblementNotificationTypeGain = iota
|
||||
EnnoblementNotificationTypeLoss
|
||||
)
|
||||
|
||||
type EnnoblementNotification struct {
|
||||
Type EnnoblementNotificationType
|
||||
ServerID string
|
||||
ChannelID string
|
||||
Ennoblement Ennoblement
|
||||
}
|
|
@ -1,6 +1,47 @@
|
|||
package domain
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TribeMeta struct {
|
||||
ID int64
|
||||
Name string
|
||||
Tag string
|
||||
ProfileURL string
|
||||
}
|
||||
|
||||
type NullTribeMeta struct {
|
||||
Tribe TribeMeta
|
||||
Valid bool
|
||||
}
|
||||
|
||||
type PlayerMeta struct {
|
||||
ID int64
|
||||
Name string
|
||||
ProfileURL string
|
||||
Tribe NullTribeMeta
|
||||
}
|
||||
|
||||
type NullPlayerMeta struct {
|
||||
Player PlayerMeta
|
||||
Valid bool
|
||||
}
|
||||
|
||||
type VillageMeta struct {
|
||||
ID int64
|
||||
FullName string
|
||||
ProfileURL string
|
||||
}
|
||||
|
||||
type Ennoblement struct {
|
||||
ID int64
|
||||
Village VillageMeta
|
||||
NewOwner NullPlayerMeta
|
||||
OldOwner NullPlayerMeta
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ServerDoesNotExistError struct {
|
||||
VersionCode string
|
||||
|
|
|
@ -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,34 @@ 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
|
||||
ennoblementsMu sync.Mutex // ennoblementsMu is used by Monitor.fetchEnnoblements
|
||||
ennoblementsSince map[string]time.Time // ennoblementsSince is used by Monitor.fetchEnnoblements
|
||||
ennoblementsMaxConcurrentRequests int // ennoblementsMaxConcurrentRequests is used by Monitor.fetchEnnoblements
|
||||
}
|
||||
|
||||
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,
|
||||
ennoblementsMaxConcurrentRequests int,
|
||||
) *Monitor {
|
||||
return &Monitor{
|
||||
repo: repo,
|
||||
client: client,
|
||||
groupSvc: groupSvc,
|
||||
ennoblementsMaxConcurrentRequests: ennoblementsMaxConcurrentRequests,
|
||||
ennoblementsSince: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) {
|
||||
|
@ -107,3 +124,244 @@ func (m *Monitor) Delete(ctx context.Context, id, serverID string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Monitor) Execute(ctx context.Context) ([]domain.EnnoblementNotification, error) {
|
||||
groups, err := m.groupSvc.List(ctx, domain.ListGroupsParams{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GroupService.List: %w", err)
|
||||
}
|
||||
|
||||
res, err := m.fetchEnnoblements(ctx, groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:prealloc
|
||||
var notifications []domain.EnnoblementNotification
|
||||
for _, r := range res {
|
||||
for _, g := range groups {
|
||||
if g.ServerKey != r.serverKey || g.VersionCode != r.versionCode {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.ChannelGains == "" && g.ChannelLosses == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ns, err := m.executeGroup(ctx, g, r.ennoblements)
|
||||
if err != nil {
|
||||
// TODO: log this error
|
||||
continue
|
||||
}
|
||||
notifications = append(notifications, ns...)
|
||||
}
|
||||
}
|
||||
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
func (m *Monitor) executeGroup(
|
||||
ctx context.Context,
|
||||
g domain.Group,
|
||||
ennoblements []twhelp.Ennoblement,
|
||||
) ([]domain.EnnoblementNotification, error) {
|
||||
monitors, err := m.repo.List(ctx, g.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("MonitorRepository.List: %w", err)
|
||||
}
|
||||
|
||||
//nolint:prealloc
|
||||
var notifications []domain.EnnoblementNotification
|
||||
for _, e := range ennoblements {
|
||||
if isInternal(e, monitors) {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.ChannelGains != "" && isGain(e, monitors) {
|
||||
notifications = append(notifications, domain.EnnoblementNotification{
|
||||
Type: domain.EnnoblementNotificationTypeGain,
|
||||
ServerID: g.ServerID,
|
||||
ChannelID: g.ChannelGains,
|
||||
Ennoblement: ennoblementToDomainModel(e),
|
||||
})
|
||||
}
|
||||
|
||||
if g.ChannelLosses != "" && isLoss(e, monitors) {
|
||||
notifications = append(notifications, domain.EnnoblementNotification{
|
||||
Type: domain.EnnoblementNotificationTypeLoss,
|
||||
ServerID: g.ServerID,
|
||||
ChannelID: g.ChannelLosses,
|
||||
Ennoblement: ennoblementToDomainModel(e),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
type listEnnoblementsResult struct {
|
||||
versionCode string
|
||||
serverKey string
|
||||
ennoblements []twhelp.Ennoblement
|
||||
err error
|
||||
}
|
||||
|
||||
type fetchEnnoblementsResult struct {
|
||||
versionCode string
|
||||
serverKey string
|
||||
ennoblements []twhelp.Ennoblement
|
||||
}
|
||||
|
||||
func (m *Monitor) fetchEnnoblements(ctx context.Context, groups []domain.Group) ([]fetchEnnoblementsResult, error) {
|
||||
m.ennoblementsMu.Lock()
|
||||
defer m.ennoblementsMu.Unlock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan listEnnoblementsResult)
|
||||
reqLimiter := make(chan struct{}, m.ennoblementsMaxConcurrentRequests)
|
||||
defer close(reqLimiter)
|
||||
ennoblementsSince := m.ennoblementsSince
|
||||
skip := make(map[string]struct{}, len(ennoblementsSince))
|
||||
|
||||
for _, g := range groups {
|
||||
if g.ChannelGains == "" && g.ChannelLosses == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := g.VersionCode + ":" + g.ServerKey
|
||||
|
||||
if _, ok := skip[key]; ok {
|
||||
continue
|
||||
}
|
||||
skip[key] = struct{}{}
|
||||
|
||||
since := ennoblementsSince[key]
|
||||
if since.IsZero() {
|
||||
since = time.Now().Add(-1 * time.Minute)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(g domain.Group, since time.Time) {
|
||||
reqLimiter <- struct{}{}
|
||||
defer func() {
|
||||
<-reqLimiter
|
||||
}()
|
||||
|
||||
res := listEnnoblementsResult{
|
||||
versionCode: g.VersionCode,
|
||||
serverKey: g.ServerKey,
|
||||
}
|
||||
res.ennoblements, res.err = m.client.ListEnnoblements(
|
||||
ctx,
|
||||
g.VersionCode,
|
||||
g.ServerKey,
|
||||
twhelp.ListEnnoblementsQueryParams{
|
||||
Since: since,
|
||||
Sort: []twhelp.ListEnnoblementsSort{twhelp.ListEnnoblementsSortCreatedAtASC},
|
||||
},
|
||||
)
|
||||
ch <- res
|
||||
}(g, since)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// reinitialize ennoblementsSince
|
||||
m.ennoblementsSince = make(map[string]time.Time)
|
||||
|
||||
//nolint:prealloc
|
||||
var results []fetchEnnoblementsResult
|
||||
for res := range ch {
|
||||
wg.Done()
|
||||
|
||||
key := res.versionCode + ":" + res.serverKey
|
||||
if l := len(res.ennoblements); l > 0 {
|
||||
m.ennoblementsSince[key] = res.ennoblements[l-1].CreatedAt.Add(time.Second)
|
||||
} else {
|
||||
m.ennoblementsSince[key] = ennoblementsSince[key]
|
||||
}
|
||||
|
||||
if res.err != nil {
|
||||
// TODO: log this error
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, fetchEnnoblementsResult{
|
||||
versionCode: res.versionCode,
|
||||
serverKey: res.serverKey,
|
||||
ennoblements: res.ennoblements,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func isInternal(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
|
||||
var n, o bool
|
||||
for _, m := range monitors {
|
||||
if m.TribeID == e.NewOwner.Player.Tribe.Tribe.ID {
|
||||
n = true
|
||||
}
|
||||
if m.TribeID == e.OldOwner.Player.Tribe.Tribe.ID {
|
||||
o = true
|
||||
}
|
||||
}
|
||||
return n && o
|
||||
}
|
||||
|
||||
func isGain(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
|
||||
var n bool
|
||||
for _, m := range monitors {
|
||||
if m.TribeID == e.NewOwner.Player.Tribe.Tribe.ID {
|
||||
n = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return n && e.NewOwner.Player.Tribe.Tribe.ID != e.OldOwner.Player.Tribe.Tribe.ID
|
||||
}
|
||||
|
||||
func isLoss(e twhelp.Ennoblement, monitors []domain.Monitor) bool {
|
||||
var o bool
|
||||
for _, m := range monitors {
|
||||
if m.TribeID == e.OldOwner.Player.Tribe.Tribe.ID {
|
||||
o = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return o && e.NewOwner.Player.Tribe.Tribe.ID != e.OldOwner.Player.Tribe.Tribe.ID
|
||||
}
|
||||
|
||||
func ennoblementToDomainModel(e twhelp.Ennoblement) domain.Ennoblement {
|
||||
return domain.Ennoblement{
|
||||
ID: e.ID,
|
||||
Village: domain.VillageMeta(e.Village),
|
||||
NewOwner: domain.NullPlayerMeta{
|
||||
Player: domain.PlayerMeta{
|
||||
ID: e.NewOwner.Player.ID,
|
||||
Name: e.NewOwner.Player.Name,
|
||||
ProfileURL: e.NewOwner.Player.ProfileURL,
|
||||
Tribe: domain.NullTribeMeta{
|
||||
Tribe: domain.TribeMeta(e.NewOwner.Player.Tribe.Tribe),
|
||||
Valid: e.NewOwner.Player.Tribe.Valid,
|
||||
},
|
||||
},
|
||||
Valid: e.NewOwner.Valid,
|
||||
},
|
||||
OldOwner: domain.NullPlayerMeta{
|
||||
Player: domain.PlayerMeta{
|
||||
ID: e.OldOwner.Player.ID,
|
||||
Name: e.OldOwner.Player.Name,
|
||||
ProfileURL: e.OldOwner.Player.ProfileURL,
|
||||
Tribe: domain.NullTribeMeta{
|
||||
Tribe: domain.TribeMeta(e.OldOwner.Player.Tribe.Tribe),
|
||||
Valid: e.OldOwner.Player.Tribe.Valid,
|
||||
},
|
||||
},
|
||||
Valid: e.OldOwner.Valid,
|
||||
},
|
||||
CreatedAt: e.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package service_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -30,7 +31,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 +56,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 +69,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 +88,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 +101,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 +127,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 +148,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 +162,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 +185,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,
|
||||
|
@ -193,3 +194,474 @@ func TestMonitor_Create(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonitor_Execute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &mock.FakeTWHelpClient{}
|
||||
villages := map[string][]twhelp.VillageMeta{
|
||||
"pl:pl181": {
|
||||
{
|
||||
ID: 1,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
},
|
||||
"en:en130": {
|
||||
{
|
||||
ID: 100,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 101,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
},
|
||||
"pl:pl180": {
|
||||
{
|
||||
ID: 200,
|
||||
FullName: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
tribes := map[string][]twhelp.TribeMeta{
|
||||
"pl:pl181": {
|
||||
{
|
||||
ID: 1,
|
||||
Name: uuid.NewString(),
|
||||
Tag: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: uuid.NewString(),
|
||||
Tag: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: uuid.NewString(),
|
||||
Tag: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
},
|
||||
"en:en130": {
|
||||
{
|
||||
ID: 100,
|
||||
Name: uuid.NewString(),
|
||||
Tag: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
{
|
||||
ID: 101,
|
||||
Name: uuid.NewString(),
|
||||
Tag: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
},
|
||||
"pl:pl180": {
|
||||
{
|
||||
ID: 200,
|
||||
Name: uuid.NewString(),
|
||||
Tag: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
players := map[string][]twhelp.PlayerMeta{
|
||||
"pl:pl181": {
|
||||
{
|
||||
ID: 1,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Tribe: tribes["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Tribe: tribes["pl:pl181"][1],
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Tribe: tribes["pl:pl181"][2],
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{},
|
||||
},
|
||||
},
|
||||
"en:en130": {
|
||||
{
|
||||
ID: 100,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Tribe: tribes["en:en130"][0],
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 101,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Tribe: tribes["en:en130"][1],
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 102,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{},
|
||||
},
|
||||
},
|
||||
"pl:pl180": {
|
||||
{
|
||||
ID: 200,
|
||||
Name: uuid.NewString(),
|
||||
ProfileURL: uuid.NewString(),
|
||||
Tribe: twhelp.NullTribeMeta{
|
||||
Tribe: tribes["pl:pl180"][0],
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ennoblements := map[string][]twhelp.Ennoblement{
|
||||
"pl:pl181": {
|
||||
{
|
||||
ID: 1,
|
||||
Village: villages["pl:pl181"][0],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][1],
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 2, // self conquer, should be skipped
|
||||
Village: villages["pl:pl181"][0],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: time.Now().Add(-4 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 3, // internal conquer, should be skipped
|
||||
Village: villages["pl:pl181"][1],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][2],
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: time.Now().Add(-3 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 4, // barbarian
|
||||
Village: villages["pl:pl181"][2],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 5, // disabled notifications about gains, should be skipped
|
||||
Village: villages["pl:pl181"][3],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][1],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 6, // disabled notifications about losses, should be skipped
|
||||
Village: villages["pl:pl181"][3],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][3],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl181"][0],
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
"en:en130": {
|
||||
{
|
||||
ID: 100, // no monitor for these tribes, should be skipped
|
||||
Village: villages["en:en130"][0],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["en:en130"][0],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 101, // no monitor for these tribes, should be skipped
|
||||
Village: villages["en:en130"][0],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["en:en130"][2],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["en:en130"][0],
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 102,
|
||||
Village: villages["en:en130"][1],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["en:en130"][1],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["en:en130"][2],
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
"pl:pl180": {
|
||||
{
|
||||
ID: 200, // api error, should be skipped
|
||||
Village: villages["pl:pl180"][0],
|
||||
NewOwner: twhelp.NullPlayerMeta{
|
||||
Player: players["pl:pl180"][0],
|
||||
Valid: true,
|
||||
},
|
||||
OldOwner: twhelp.NullPlayerMeta{},
|
||||
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
client.ListEnnoblementsCalls(
|
||||
func(
|
||||
ctx context.Context,
|
||||
version string,
|
||||
server string,
|
||||
_ twhelp.ListEnnoblementsQueryParams,
|
||||
) ([]twhelp.Ennoblement, error) {
|
||||
if version == "pl" && server == "pl180" {
|
||||
return nil, errors.New("random error")
|
||||
}
|
||||
return ennoblements[version+":"+server], nil
|
||||
},
|
||||
)
|
||||
|
||||
groupSvc := &mock.FakeGroupReader{}
|
||||
groups := []domain.Group{
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
ServerID: uuid.NewString(),
|
||||
ChannelGains: "", // should be skipped - ChannelGains and ChannelLosses are empty strings
|
||||
ChannelLosses: "",
|
||||
ServerKey: "pl181",
|
||||
VersionCode: "pl",
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
ServerID: uuid.NewString(),
|
||||
ChannelGains: uuid.NewString(),
|
||||
ChannelLosses: "",
|
||||
ServerKey: "pl181",
|
||||
VersionCode: "pl",
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
ServerID: uuid.NewString(),
|
||||
ChannelGains: "",
|
||||
ChannelLosses: uuid.NewString(),
|
||||
ServerKey: "pl181",
|
||||
VersionCode: "pl",
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
ServerID: uuid.NewString(),
|
||||
ChannelGains: uuid.NewString(),
|
||||
ChannelLosses: uuid.NewString(),
|
||||
ServerKey: "en130",
|
||||
VersionCode: "en",
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
ServerID: uuid.NewString(),
|
||||
ChannelGains: uuid.NewString(),
|
||||
ChannelLosses: uuid.NewString(),
|
||||
ServerKey: "pl180",
|
||||
VersionCode: "pl",
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
groupSvc.ListReturns(groups, nil)
|
||||
|
||||
repo := &mock.FakeMonitorRepository{}
|
||||
monitors := map[string][]domain.Monitor{
|
||||
groups[1].ID: {
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
TribeID: tribes["pl:pl181"][0].ID,
|
||||
GroupID: groups[1].ID,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
TribeID: tribes["pl:pl181"][2].ID,
|
||||
GroupID: groups[1].ID,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
groups[2].ID: {
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
TribeID: tribes["pl:pl181"][1].ID,
|
||||
GroupID: groups[2].ID,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
groups[3].ID: {
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
TribeID: tribes["en:en130"][1].ID,
|
||||
GroupID: groups[3].ID,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
groups[4].ID: {
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
TribeID: tribes["pl:pl180"][0].ID,
|
||||
GroupID: groups[4].ID,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
repo.ListCalls(func(ctx context.Context, groupID string) ([]domain.Monitor, error) {
|
||||
return monitors[groupID], nil
|
||||
})
|
||||
|
||||
notifications, err := service.NewMonitor(repo, groupSvc, client, 10).
|
||||
Execute(context.Background())
|
||||
assert.NoError(t, err)
|
||||
expectedNotifications := []domain.EnnoblementNotification{
|
||||
{
|
||||
Type: domain.EnnoblementNotificationTypeGain,
|
||||
ServerID: groups[1].ServerID,
|
||||
ChannelID: groups[1].ChannelGains,
|
||||
Ennoblement: ennoblementToDomainModel(ennoblements["pl:pl181"][0]),
|
||||
},
|
||||
{
|
||||
Type: domain.EnnoblementNotificationTypeGain,
|
||||
ServerID: groups[1].ServerID,
|
||||
ChannelID: groups[1].ChannelGains,
|
||||
Ennoblement: ennoblementToDomainModel(ennoblements["pl:pl181"][3]),
|
||||
},
|
||||
{
|
||||
Type: domain.EnnoblementNotificationTypeLoss,
|
||||
ServerID: groups[2].ServerID,
|
||||
ChannelID: groups[2].ChannelLosses,
|
||||
Ennoblement: ennoblementToDomainModel(ennoblements["pl:pl181"][0]),
|
||||
},
|
||||
{
|
||||
Type: domain.EnnoblementNotificationTypeGain,
|
||||
ServerID: groups[3].ServerID,
|
||||
ChannelID: groups[3].ChannelGains,
|
||||
Ennoblement: ennoblementToDomainModel(ennoblements["en:en130"][2]),
|
||||
},
|
||||
}
|
||||
assert.Len(t, notifications, len(expectedNotifications))
|
||||
for _, n := range expectedNotifications {
|
||||
assert.Contains(t, notifications, n)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ennoblementToDomainModel(e twhelp.Ennoblement) domain.Ennoblement {
|
||||
return domain.Ennoblement{
|
||||
ID: e.ID,
|
||||
Village: domain.VillageMeta(e.Village),
|
||||
NewOwner: domain.NullPlayerMeta{
|
||||
Player: domain.PlayerMeta{
|
||||
ID: e.NewOwner.Player.ID,
|
||||
Name: e.NewOwner.Player.Name,
|
||||
ProfileURL: e.NewOwner.Player.ProfileURL,
|
||||
Tribe: domain.NullTribeMeta{
|
||||
Tribe: domain.TribeMeta(e.NewOwner.Player.Tribe.Tribe),
|
||||
Valid: e.NewOwner.Player.Tribe.Valid,
|
||||
},
|
||||
},
|
||||
Valid: e.NewOwner.Valid,
|
||||
},
|
||||
OldOwner: domain.NullPlayerMeta{
|
||||
Player: domain.PlayerMeta{
|
||||
ID: e.OldOwner.Player.ID,
|
||||
Name: e.OldOwner.Player.Name,
|
||||
ProfileURL: e.OldOwner.Player.ProfileURL,
|
||||
Tribe: domain.NullTribeMeta{
|
||||
Tribe: domain.TribeMeta(e.OldOwner.Player.Tribe.Tribe),
|
||||
Valid: e.OldOwner.Player.Tribe.Valid,
|
||||
},
|
||||
},
|
||||
Valid: e.OldOwner.Valid,
|
||||
},
|
||||
CreatedAt: e.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,50 @@ func (c *Client) GetTribeByTag(ctx context.Context, version, server, tag string)
|
|||
return resp.Data, nil
|
||||
}
|
||||
|
||||
type ListEnnoblementsSort string
|
||||
|
||||
const (
|
||||
ListEnnoblementsSortCreatedAtASC ListEnnoblementsSort = "createdAt:ASC"
|
||||
)
|
||||
|
||||
func (s ListEnnoblementsSort) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
type ListEnnoblementsQueryParams struct {
|
||||
Limit int32
|
||||
Offset int32
|
||||
Since time.Time
|
||||
Sort []ListEnnoblementsSort
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
for _, s := range queryParams.Sort {
|
||||
q.Add("sort", s.String())
|
||||
}
|
||||
|
||||
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_MAX_CONCURRENT_REQUESTS
|
||||
value: "4"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: [ "cat", "/tmp/healthy" ]
|
||||
|
|
Loading…
Reference in New Issue
Block a user