Reviewed-on: #116
This commit is contained in:
parent
71a61533db
commit
d1ba315bd2
|
@ -9,6 +9,7 @@ steps:
|
|||
pull: always
|
||||
environment:
|
||||
TESTS_DB_DSN: postgres://postgres:dcbot@database:5432/dcbot?sslmode=disable
|
||||
TESTS_REDIS_CONNECTION_STRING: redis://:@redis:6379/0
|
||||
commands:
|
||||
- make generate
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
@ -19,6 +20,8 @@ services:
|
|||
environment:
|
||||
POSTGRES_DB: dcbot
|
||||
POSTGRES_PASSWORD: dcbot
|
||||
- name: redis
|
||||
image: redis:7.0.12
|
||||
|
||||
trigger:
|
||||
event:
|
||||
|
@ -392,6 +395,6 @@ depends_on:
|
|||
- migrations-manifest
|
||||
---
|
||||
kind: signature
|
||||
hmac: 4fa2583c281aacc0e4d0dc87296a2ce6ff7facf71d1ce2e9f5d4157b35635d4f
|
||||
hmac: ed0db55292798e0b63ba43ae50c749730c9a72423f6387b670e3fd7f68d2f5ad
|
||||
|
||||
...
|
||||
|
|
|
@ -35,6 +35,7 @@ func NewBunDB() (*bun.DB, error) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), dbPingTimeout)
|
||||
defer cancel()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("db.PingContext: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
redisPingTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type redisConfig struct {
|
||||
ConnectionString string `envconfig:"CONNECTION_STRING" required:"true"`
|
||||
}
|
||||
|
||||
func NewRedisClient() (*redis.Client, error) {
|
||||
var cfg redisConfig
|
||||
if err := envconfig.Process("REDIS", &cfg); err != nil {
|
||||
return nil, fmt.Errorf("envconfig.Process: %w", err)
|
||||
}
|
||||
|
||||
opts, err := redis.ParseURL(cfg.ConnectionString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse REDIS_CONNECTION_STRING: %w", err)
|
||||
}
|
||||
|
||||
c := redis.NewClient(opts)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), redisPingTimeout)
|
||||
defer cancel()
|
||||
if err = c.Ping(ctx).Err(); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, fmt.Errorf("couldn't ping redis: %w", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
|
@ -41,17 +41,24 @@ func New() *cli.Command {
|
|||
_ = db.Close()
|
||||
}()
|
||||
|
||||
redisClient, err := internal.NewRedisClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
twhelpService, err := internal.NewTWHelpService(c.App.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewTWHelpService: %w", err)
|
||||
}
|
||||
|
||||
groupRepo := adapter.NewGroupBun(db)
|
||||
villageRepo := adapter.NewVillageRedis(redisClient)
|
||||
|
||||
choiceSvc := service.NewChoice(twhelpService)
|
||||
groupSvc := service.NewGroup(groupRepo, twhelpService, logger, cfg.MaxGroupsPerServer, cfg.MaxMonitorsPerGroup)
|
||||
villageSvc := service.NewVillage(villageRepo, twhelpService)
|
||||
|
||||
bot, err := discord.NewBot(cfg.Token, groupSvc, choiceSvc, logger)
|
||||
bot, err := discord.NewBot(cfg.Token, groupSvc, choiceSvc, villageSvc, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord.NewBot: %w", err)
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -12,6 +12,7 @@ require (
|
|||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/ory/dockertest/v3 v3.10.0
|
||||
github.com/pressly/goose/v3 v3.11.2
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/uptrace/bun v1.1.14
|
||||
|
@ -28,9 +29,11 @@ require (
|
|||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v23.0.6+incompatible // indirect
|
||||
github.com/docker/docker v23.0.6+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -7,10 +7,14 @@ github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5
|
|||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
|
||||
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
|
@ -19,6 +23,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v23.0.6+incompatible h1:CScadyCJ2ZKUDpAMZta6vK8I+6/m60VIjGIV7Wg/Eu4=
|
||||
github.com/docker/cli v23.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v23.0.6+incompatible h1:aBD4np894vatVX99UTx/GyOUOK4uEcROwA3+bQhEcoU=
|
||||
|
@ -76,6 +82,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.11.2 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs=
|
||||
github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
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=
|
||||
|
|
|
@ -14,16 +14,20 @@ import (
|
|||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
const envDBDSN = "TESTS_DB_DSN"
|
||||
const (
|
||||
envDBDSN = "TESTS_DB_DSN"
|
||||
envRedisConnectionString = "TESTS_REDIS_CONNECTION_STRING"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goose.SetBaseFS(os.DirFS("../../migrations"))
|
||||
goose.SetLogger(goose.NopLogger())
|
||||
|
||||
_, isDBDSNPresent := os.LookupEnv(envDBDSN)
|
||||
_, isRedisConnectionStringPresent := os.LookupEnv(envRedisConnectionString)
|
||||
|
||||
// all required envs are set, there is no need to use dockertest
|
||||
if isDBDSNPresent {
|
||||
if isDBDSNPresent && isRedisConnectionStringPresent {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
|
@ -45,6 +49,12 @@ func TestMain(m *testing.M) {
|
|||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
if !isRedisConnectionStringPresent {
|
||||
resource, connString := newRedis(pool)
|
||||
_ = os.Setenv(envRedisConnectionString, connString.String())
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
for _, r := range resources {
|
||||
|
@ -66,7 +76,7 @@ func newPostgres(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
|||
|
||||
pw, _ := dsn.User.Password()
|
||||
|
||||
resource, runErr := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "14.8",
|
||||
Env: []string{
|
||||
|
@ -80,8 +90,8 @@ func newPostgres(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
|||
Name: "no",
|
||||
}
|
||||
})
|
||||
if runErr != nil {
|
||||
log.Fatalf("couldn't run postgres: %s", runErr)
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't run postgres: %s", err)
|
||||
}
|
||||
|
||||
_ = resource.Expire(120)
|
||||
|
@ -91,6 +101,32 @@ func newPostgres(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
|||
return resource, dsn
|
||||
}
|
||||
|
||||
func newRedis(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
||||
connectionString := &url.URL{
|
||||
Scheme: "redis",
|
||||
Path: "0",
|
||||
}
|
||||
|
||||
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "redis",
|
||||
Tag: "7.0.11",
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{
|
||||
Name: "no",
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't run redis: %s", err)
|
||||
}
|
||||
|
||||
_ = resource.Expire(120)
|
||||
|
||||
connectionString.Host = getHostPort(resource, "6379/tcp")
|
||||
|
||||
return resource, connectionString
|
||||
}
|
||||
|
||||
func getHostPort(resource *dockertest.Resource, id string) string {
|
||||
dockerURL := os.Getenv("DOCKER_HOST")
|
||||
if dockerURL == "" {
|
||||
|
|
|
@ -29,7 +29,7 @@ func newBunDB(tb testing.TB) *bun.DB {
|
|||
sqldb := sql.OpenDB(
|
||||
pgdriver.NewConnector(
|
||||
pgdriver.WithDSN(os.Getenv(envDBDSN)),
|
||||
pgdriver.WithConnParams(map[string]interface{}{
|
||||
pgdriver.WithConnParams(map[string]any{
|
||||
"search_path": schema,
|
||||
}),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package adapter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newRedisClient(tb testing.TB) *redis.Client {
|
||||
tb.Helper()
|
||||
|
||||
opts, err := redis.ParseURL(os.Getenv(envRedisConnectionString))
|
||||
require.NoError(tb, err, "couldn't parse redis connection string")
|
||||
|
||||
client := redis.NewClient(opts)
|
||||
tb.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
require.NoError(tb, retry(func() error {
|
||||
return client.Ping(context.Background()).Err()
|
||||
}), "couldn't ping DB")
|
||||
|
||||
return client
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type VillageRedis struct {
|
||||
client redis.UniversalClient
|
||||
}
|
||||
|
||||
func NewVillageRedis(client redis.UniversalClient) *VillageRedis {
|
||||
return &VillageRedis{client: client}
|
||||
}
|
||||
|
||||
const translateVillageCoordsParamsExp = 72 * time.Hour
|
||||
|
||||
type translateVillageCoordsParamsRedis struct {
|
||||
VersionCode string
|
||||
ServerKey string
|
||||
Coords []string
|
||||
PerPage int32
|
||||
MaxPage int32
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
func (v *VillageRedis) SaveTranslateCoordsParams(ctx context.Context, params domain.TranslateVillageCoordsParams) error {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
if err := gob.NewEncoder(buf).Encode(translateVillageCoordsParamsRedis{
|
||||
VersionCode: params.VersionCode(),
|
||||
ServerKey: params.ServerKey(),
|
||||
Coords: params.Coords(),
|
||||
PerPage: params.PerPage(),
|
||||
MaxPage: params.MaxPage(),
|
||||
SHA256: params.SHA256(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.client.Set(
|
||||
ctx,
|
||||
v.buildTranslateCoordsParamsKey(params.SHA256()),
|
||||
buf.Bytes(),
|
||||
translateVillageCoordsParamsExp,
|
||||
).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VillageRedis) GetTranslateCoordsParams(ctx context.Context, sha256Hash string) (domain.TranslateVillageCoordsParams, error) {
|
||||
b, err := v.client.Get(ctx, v.buildTranslateCoordsParamsKey(sha256Hash)).Bytes()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return domain.TranslateVillageCoordsParams{}, domain.TranslateVillageCoordsParamsNotFoundError{
|
||||
SHA256: sha256Hash,
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return domain.TranslateVillageCoordsParams{}, err
|
||||
}
|
||||
|
||||
var params translateVillageCoordsParamsRedis
|
||||
|
||||
if err = gob.NewDecoder(bytes.NewReader(b)).Decode(¶ms); err != nil {
|
||||
return domain.TranslateVillageCoordsParams{}, err
|
||||
}
|
||||
|
||||
res, err := domain.UnmarshalTranslateVillageCoordsParamsFromDatabase(
|
||||
params.VersionCode,
|
||||
params.ServerKey,
|
||||
params.Coords,
|
||||
params.PerPage,
|
||||
params.MaxPage,
|
||||
params.SHA256,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.TranslateVillageCoordsParams{}, fmt.Errorf("couldn't unmarshal TranslateVillageCoordsParams from database: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (v *VillageRedis) buildTranslateCoordsParamsKey(sha256Hash string) string {
|
||||
return "translate_village_coords_params_" + sha256Hash
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package adapter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/adapter"
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVillageRedis_SaveTranslateCoordsParams_GetTranslateCoordsParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repo := adapter.NewVillageRedis(newRedisClient(t))
|
||||
|
||||
params, err := domain.NewTranslateVillageCoordsParams("pl", "pl181", "123|123 898|123", 20)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoError(t, repo.SaveTranslateCoordsParams(context.Background(), params))
|
||||
|
||||
paramsRedis, err := repo.GetTranslateCoordsParams(context.Background(), params.SHA256())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, params, paramsRedis)
|
||||
|
||||
sha256Hash := params.SHA256() + "123"
|
||||
paramsRedis, err = repo.GetTranslateCoordsParams(context.Background(), sha256Hash)
|
||||
assert.ErrorIs(t, err, domain.TranslateVillageCoordsParamsNotFoundError{
|
||||
SHA256: sha256Hash,
|
||||
})
|
||||
assert.Zero(t, paramsRedis)
|
||||
}
|
|
@ -33,19 +33,30 @@ type ChoiceService interface {
|
|||
Versions(ctx context.Context) ([]domain.Choice, error)
|
||||
}
|
||||
|
||||
type VillageService interface {
|
||||
TranslateCoords(
|
||||
ctx context.Context,
|
||||
params domain.TranslateVillageCoordsParams,
|
||||
page int32,
|
||||
) (domain.TranslateVillageCoordsResult, error)
|
||||
TranslateCoordsFromHash(ctx context.Context, paramsSHA256Hash string, page int32) (domain.TranslateVillageCoordsResult, error)
|
||||
}
|
||||
|
||||
type Bot struct {
|
||||
session *discordgo.Session
|
||||
cron *cron.Cron
|
||||
groupSvc GroupService
|
||||
choiceSvc ChoiceService
|
||||
logger *zap.Logger
|
||||
localizer *discordi18n.Localizer
|
||||
session *discordgo.Session
|
||||
cron *cron.Cron
|
||||
groupSvc GroupService
|
||||
choiceSvc *choiceService
|
||||
villageSvc VillageService
|
||||
logger *zap.Logger
|
||||
localizer *discordi18n.Localizer
|
||||
}
|
||||
|
||||
func NewBot(
|
||||
token string,
|
||||
groupSvc GroupService,
|
||||
client ChoiceService,
|
||||
choiceSvc ChoiceService,
|
||||
villageSvc VillageService,
|
||||
logger *zap.Logger,
|
||||
) (*Bot, error) {
|
||||
localizer, err := discordi18n.NewLocalizer(language.English)
|
||||
|
@ -67,10 +78,14 @@ func NewBot(
|
|||
cron.SkipIfStillRunning(cron.DiscardLogger),
|
||||
),
|
||||
),
|
||||
groupSvc: groupSvc,
|
||||
choiceSvc: client,
|
||||
logger: logger,
|
||||
localizer: localizer,
|
||||
groupSvc: groupSvc,
|
||||
choiceSvc: &choiceService{
|
||||
choiceSvc: choiceSvc,
|
||||
localizer: localizer,
|
||||
},
|
||||
villageSvc: villageSvc,
|
||||
logger: logger,
|
||||
localizer: localizer,
|
||||
}
|
||||
|
||||
b.session.AddHandler(b.handleSessionReady)
|
||||
|
@ -106,6 +121,7 @@ type command interface {
|
|||
func (b *Bot) registerCommands() error {
|
||||
commands := []command{
|
||||
&groupCommand{localizer: b.localizer, logger: b.logger, groupSvc: b.groupSvc, choiceSvc: b.choiceSvc},
|
||||
&coordsCommand{localizer: b.localizer, logger: b.logger, villageSvc: b.villageSvc, choiceSvc: b.choiceSvc},
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
|
|
|
@ -0,0 +1,377 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/discord/internal/discordi18n"
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type coordsCommand struct {
|
||||
localizer *discordi18n.Localizer
|
||||
logger *zap.Logger
|
||||
villageSvc VillageService
|
||||
choiceSvc *choiceService
|
||||
}
|
||||
|
||||
func (c *coordsCommand) name() string {
|
||||
return "coords"
|
||||
}
|
||||
|
||||
func (c *coordsCommand) register(s *discordgo.Session) error {
|
||||
if err := c.create(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.AddHandler(c.handle)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *coordsCommand) create(s *discordgo.Session) error {
|
||||
dm := true
|
||||
|
||||
optionVersionDefault, optionVersionLocalizations, err := c.localizer.LocalizeDiscord("cmd.coords.option.version.description")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
optionServerDefault, optionServerLocalizations, err := c.localizer.LocalizeDiscord("cmd.coords.option.server.description")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
optionCoordsDefault, optionCoordsLocalizations, err := c.localizer.LocalizeDiscord("cmd.coords.option.coords.description")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.coords.description")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versionChoices, err := c.choiceSvc.versions(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{
|
||||
Name: c.name(),
|
||||
Description: cmdDescriptionDefault,
|
||||
DescriptionLocalizations: &cmdDescriptionLocalizations,
|
||||
DMPermission: &dm,
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Name: "version",
|
||||
Description: optionVersionDefault,
|
||||
DescriptionLocalizations: optionVersionLocalizations,
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Choices: versionChoices,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "server",
|
||||
Description: optionServerDefault,
|
||||
DescriptionLocalizations: optionServerLocalizations,
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "coords",
|
||||
Description: optionCoordsDefault,
|
||||
DescriptionLocalizations: optionCoordsLocalizations,
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Required: true,
|
||||
MaxLength: domain.VillageCoordsStringMaxLength,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *coordsCommand) handle(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
ctx := context.Background()
|
||||
|
||||
//nolint:exhaustive
|
||||
switch i.Type {
|
||||
case discordgo.InteractionApplicationCommand:
|
||||
c.handleCommand(ctx, s, i)
|
||||
case discordgo.InteractionMessageComponent:
|
||||
c.handleMessageComponent(ctx, s, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *coordsCommand) handleCommand(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
cmdData := i.ApplicationCommandData()
|
||||
|
||||
if cmdData.Name != c.name() {
|
||||
return
|
||||
}
|
||||
|
||||
locale := string(i.Locale)
|
||||
|
||||
params, err := c.optionsToTranslateCoordsParams(cmdData.Options)
|
||||
if err != nil {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res, err := c.villageSvc.TranslateCoords(ctx, params, 1)
|
||||
if err != nil {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.InteractionRespond(i.Interaction, c.buildResponse(res, locale))
|
||||
}
|
||||
|
||||
const coordsTranslationVillagesPerPage = 7
|
||||
|
||||
func (c *coordsCommand) optionsToTranslateCoordsParams(
|
||||
options []*discordgo.ApplicationCommandInteractionDataOption,
|
||||
) (domain.TranslateVillageCoordsParams, error) {
|
||||
version := ""
|
||||
server := ""
|
||||
coordsStr := ""
|
||||
|
||||
for _, opt := range options {
|
||||
switch opt.Name {
|
||||
case "version":
|
||||
version = opt.StringValue()
|
||||
case "server":
|
||||
server = opt.StringValue()
|
||||
case "coords":
|
||||
coordsStr = opt.StringValue()
|
||||
}
|
||||
}
|
||||
|
||||
return domain.NewTranslateVillageCoordsParams(version, server, coordsStr, coordsTranslationVillagesPerPage)
|
||||
}
|
||||
|
||||
func (c *coordsCommand) handleMessageComponent(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
cmpData := i.MessageComponentData()
|
||||
|
||||
if cmpData.ComponentType != discordgo.ButtonComponent {
|
||||
return
|
||||
}
|
||||
|
||||
locale := string(i.Locale)
|
||||
|
||||
page, hash, err := c.parseButtonID(cmpData.CustomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := c.villageSvc.TranslateCoordsFromHash(ctx, hash, page)
|
||||
if err != nil {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.InteractionRespond(i.Interaction, c.buildResponse(res, locale))
|
||||
}
|
||||
|
||||
func (c *coordsCommand) buildResponse(res domain.TranslateVillageCoordsResult, locale string) *discordgo.InteractionResponse {
|
||||
titleMessageID := "cmd.coords.embed.title"
|
||||
title, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: titleMessageID})
|
||||
if err != nil {
|
||||
c.logger.Error("no message with the specified id", zap.String("id", titleMessageID), zap.Error(err))
|
||||
return &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
footerMessageID := "cmd.coords.embed.footer"
|
||||
footer, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{
|
||||
MessageID: footerMessageID,
|
||||
TemplateData: map[string]any{
|
||||
"Page": res.Page,
|
||||
"MaxPage": res.MaxPage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("no message with the specified id", zap.String("id", footerMessageID), zap.Error(err))
|
||||
return &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buttonNextPageMessageID := "cmd.coords.embed.button.next"
|
||||
buttonNextPage, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: buttonNextPageMessageID})
|
||||
if err != nil {
|
||||
c.logger.Error("no message with the specified id", zap.String("id", buttonNextPageMessageID), zap.Error(err))
|
||||
return &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buttonPrevPageMessageID := "cmd.coords.embed.button.prev"
|
||||
buttonPrevPage, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: buttonPrevPageMessageID})
|
||||
if err != nil {
|
||||
c.logger.Error("no message with the specified id", zap.String("id", buttonPrevPageMessageID), zap.Error(err))
|
||||
return &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
description, err := c.buildDescription(locale, res.Villages, res.NotFound)
|
||||
if err != nil {
|
||||
return &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: c.localizer.LocalizeError(locale, err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Type: discordgo.EmbedTypeRich,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Color: colorGrey,
|
||||
Timestamp: formatTimestamp(time.Now()),
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: footer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.ActionsRow{
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.Button{
|
||||
Style: discordgo.PrimaryButton,
|
||||
CustomID: c.buildButtonID(res.Page-1, res.ParamsSHA256),
|
||||
Label: buttonPrevPage,
|
||||
Disabled: !res.HasPrev,
|
||||
},
|
||||
discordgo.Button{
|
||||
Style: discordgo.PrimaryButton,
|
||||
CustomID: c.buildButtonID(res.Page+1, res.ParamsSHA256),
|
||||
Label: buttonNextPage,
|
||||
Disabled: !res.HasNext,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *coordsCommand) buildDescription(locale string, villages []domain.Village, notFound []string) (string, error) {
|
||||
var builderDescription strings.Builder
|
||||
|
||||
for i, v := range villages {
|
||||
if i > 0 {
|
||||
builderDescription.WriteString("\n")
|
||||
}
|
||||
|
||||
descriptionMessageID := "cmd.coords.embed.description.village"
|
||||
description, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{
|
||||
MessageID: descriptionMessageID,
|
||||
TemplateData: map[string]any{
|
||||
"Village": v,
|
||||
},
|
||||
Funcs: template.FuncMap{
|
||||
"buildPlayerMarkdown": buildPlayerMarkdown,
|
||||
"buildLink": buildLink,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("no message with the specified id", zap.String("id", descriptionMessageID), zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
builderDescription.WriteString(description)
|
||||
}
|
||||
|
||||
if len(notFound) == 0 {
|
||||
return builderDescription.String(), nil
|
||||
}
|
||||
|
||||
if builderDescription.Len() > 0 {
|
||||
builderDescription.WriteString("\n")
|
||||
}
|
||||
|
||||
descriptionMessageID := "cmd.coords.embed.description.not-found"
|
||||
description, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{
|
||||
MessageID: descriptionMessageID,
|
||||
TemplateData: map[string]any{
|
||||
"Coords": strings.Join(notFound, ", "),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("no message with the specified id", zap.String("id", descriptionMessageID), zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
builderDescription.WriteString(description)
|
||||
|
||||
return builderDescription.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
coordsTranslationButtonIDPrefix = "coords"
|
||||
coordsTranslationButtonIDSeparator = "-"
|
||||
)
|
||||
|
||||
func (c *coordsCommand) buildButtonID(page int32, hash string) string {
|
||||
return fmt.Sprintf("%s%s%d%s%s", coordsTranslationButtonIDPrefix, coordsTranslationButtonIDSeparator, page, coordsTranslationButtonIDSeparator, hash)
|
||||
}
|
||||
|
||||
func (c *coordsCommand) parseButtonID(id string) (int32, string, error) {
|
||||
chunks := strings.Split(id, coordsTranslationButtonIDSeparator)
|
||||
if len(chunks) != 3 || chunks[0] != coordsTranslationButtonIDPrefix {
|
||||
return 0, "", fmt.Errorf("couldn't parse '%s': incorrect format", id)
|
||||
}
|
||||
|
||||
page, err := strconv.ParseInt(chunks[1], 10, 32)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("couldn't parse '%s': %w", id, err)
|
||||
}
|
||||
|
||||
return int32(page), chunks[2], nil
|
||||
}
|
|
@ -2,7 +2,6 @@ package discord
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -21,7 +20,7 @@ type groupCommand struct {
|
|||
localizer *discordi18n.Localizer
|
||||
logger *zap.Logger
|
||||
groupSvc GroupService
|
||||
choiceSvc ChoiceService
|
||||
choiceSvc *choiceService
|
||||
}
|
||||
|
||||
func (c *groupCommand) name() string {
|
||||
|
@ -100,12 +99,12 @@ func (c *groupCommand) buildSubcommandCreate() (*discordgo.ApplicationCommandOpt
|
|||
}
|
||||
|
||||
func (c *groupCommand) buildSubcommandCreateOptions() ([]*discordgo.ApplicationCommandOption, error) {
|
||||
versionChoices, err := c.getVersionChoices()
|
||||
versionChoices, err := c.choiceSvc.versions(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
languageChoices, err := c.getLanguageChoices()
|
||||
languageChoices, err := c.choiceSvc.languages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -208,40 +207,6 @@ func (c *groupCommand) buildSubcommandCreateOptions() ([]*discordgo.ApplicationC
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (c *groupCommand) getVersionChoices() ([]*discordgo.ApplicationCommandOptionChoice, error) {
|
||||
choices, err := c.choiceSvc.Versions(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ChoiceService.Versions: %w", err)
|
||||
}
|
||||
|
||||
dcChoices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(choices))
|
||||
for _, v := range choices {
|
||||
dcChoices = append(dcChoices, &discordgo.ApplicationCommandOptionChoice{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
})
|
||||
}
|
||||
|
||||
return dcChoices, nil
|
||||
}
|
||||
|
||||
func (c *groupCommand) getLanguageChoices() ([]*discordgo.ApplicationCommandOptionChoice, error) {
|
||||
tags := c.localizer.LanguageTags()
|
||||
choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
lang, langLocalizations, err := c.localizer.LocalizeDiscord(buildLangMessageID(tag.String()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
|
||||
Name: lang,
|
||||
NameLocalizations: langLocalizations,
|
||||
Value: tag.String(),
|
||||
})
|
||||
}
|
||||
return choices, nil
|
||||
}
|
||||
|
||||
func (c *groupCommand) buildSubcommandList() (*discordgo.ApplicationCommandOption, error) {
|
||||
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.list.description")
|
||||
if err != nil {
|
||||
|
@ -420,7 +385,7 @@ func (c *groupCommand) buildSubcommandSet() (*discordgo.ApplicationCommandOption
|
|||
}
|
||||
|
||||
func (c *groupCommand) buildSubcommandSetLanguage() (*discordgo.ApplicationCommandOption, error) {
|
||||
languageChoices, err := c.getLanguageChoices()
|
||||
languageChoices, err := c.choiceSvc.languages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -877,6 +842,7 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
|
|||
Type: discordgo.EmbedTypeRich,
|
||||
Title: title,
|
||||
Fields: fields,
|
||||
Color: colorGrey,
|
||||
Timestamp: formatTimestamp(time.Now()),
|
||||
},
|
||||
},
|
||||
|
@ -964,6 +930,7 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
|
|||
Type: discordgo.EmbedTypeRich,
|
||||
Title: g.ID,
|
||||
Description: description,
|
||||
Color: colorGrey,
|
||||
Timestamp: formatTimestamp(time.Now()),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,9 +3,12 @@ package discord
|
|||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
colorGrey = 0x808080
|
||||
embedDescriptionCharLimit = 4096
|
||||
)
|
||||
|
||||
|
@ -34,3 +37,19 @@ func boolToEmoji(val bool) string {
|
|||
func buildLangMessageID(languageTag string) string {
|
||||
return "lang." + languageTag
|
||||
}
|
||||
|
||||
func buildPlayerMarkdown(p domain.NullPlayerMeta) string {
|
||||
if !p.Valid {
|
||||
return "-"
|
||||
}
|
||||
|
||||
tag := p.Player.Tribe.Tribe.Tag
|
||||
if tag == "" {
|
||||
tag = "-"
|
||||
}
|
||||
|
||||
return buildLink(p.Player.Name, p.Player.ProfileURL) +
|
||||
" [" +
|
||||
buildLink(tag, p.Player.Tribe.Tribe.ProfileURL) +
|
||||
"]"
|
||||
}
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
"err.tribe-tag-not-found": "Tribe (tag={{ .Tag }}) not found.",
|
||||
"err.tribe-id-not-found": "Tribe (id={{ .ID }}) not found.",
|
||||
"err.tribe-does-not-exist": "The tribe (tag={{ .Tag }}) doesn't exist.",
|
||||
"err.greater-equal-than": "{{ .Field }} must be no less than {{ .Threshold }}.",
|
||||
"err.less-equal-than": "{{ .Field }} must be no greater than {{ .Threshold }}.",
|
||||
"err.length-too-short": "{{ .Field }}: the length must be no less than than {{ .Threshold }}.",
|
||||
"err.village-coords-string-too-long": "The given string is too long ({{ len .Str }}/{{ .Max }}).",
|
||||
"err.village-coords-not-found-in-string": "No coords found in the given string.",
|
||||
"err.translate-village-coords-params-not-found": "This action is no longer available.",
|
||||
|
||||
"job.execute-monitors.embed.description": "{{ buildPlayerMarkdown .Ennoblement.NewOwner }} has taken {{ buildLink .Ennoblement.Village.FullName .Ennoblement.Village.ProfileURL }} (Old owner: {{ buildPlayerMarkdown .Ennoblement.Village.Player }}).",
|
||||
|
||||
|
@ -77,5 +83,16 @@
|
|||
|
||||
"cmd.group.delete.description": "Deletes a group",
|
||||
"cmd.group.delete.option.group.description": "Group ID",
|
||||
"cmd.group.delete.success": "The group has been successfully deleted."
|
||||
"cmd.group.delete.success": "The group has been successfully deleted.",
|
||||
|
||||
"cmd.coords.description": "Translates village coords",
|
||||
"cmd.coords.option.version.description": "e.g. www.tribalwars.net, www.plemiona.pl",
|
||||
"cmd.coords.option.server.description": "Tribal Wars server (e.g. en115, pl170)",
|
||||
"cmd.coords.option.coords.description": "Coords",
|
||||
"cmd.coords.embed.title": "Villages",
|
||||
"cmd.coords.embed.description.village": "{{ buildLink .Village.FullName .Village.ProfileURL }} owned by {{ buildPlayerMarkdown .Village.Player }}",
|
||||
"cmd.coords.embed.description.not-found": "**Not found**: {{ .Coords }}",
|
||||
"cmd.coords.embed.button.next": "Next page",
|
||||
"cmd.coords.embed.button.prev": "Previous page",
|
||||
"cmd.coords.embed.footer": "Page {{ .Page }}/{{ .MaxPage }}"
|
||||
}
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
"err.tribe-tag-not-found": "Plemię (skrót={{ .Tag }}) nie istnieje.",
|
||||
"err.tribe-id-not-found": "Plemię (id={{ .ID }}) nie istnieje.",
|
||||
"err.tribe-does-not-exist": "Plemię (skrót={{ .Tag }}) nie istnieje.",
|
||||
"err.greater-equal-than": "{{ .Field }} musi byc nie mniejszy niż {{ .Threshold }}.",
|
||||
"err.less-equal-than": "{{ .Field }} musi być nie większy niż {{ .Threshold }}.",
|
||||
"err.length-too-short": "{{ .Field }}: długość nie może być mniejsza niż {{ .Threshold }}.",
|
||||
"err.village-coords-string-too-long": "Podany ciąg znaków jest zbyt długi ({{ len .Str }}/{{ .Max }}).",
|
||||
"err.village-coords-not-found-in-string": "Nie znaleziono współrzędnych w podanym ciągu znaków.",
|
||||
"err.translate-village-coords-params-not-found": "Ta akcja nie jest już dostępna.",
|
||||
|
||||
"job.execute-monitors.embed.description": "{{ buildPlayerMarkdown .Ennoblement.NewOwner }} przejął {{ buildLink .Ennoblement.Village.FullName .Ennoblement.Village.ProfileURL }} (Poprzedni właściciel: {{ buildPlayerMarkdown .Ennoblement.Village.Player }}).",
|
||||
|
||||
|
@ -77,5 +83,16 @@
|
|||
|
||||
"cmd.group.delete.description": "Usuwa grupę",
|
||||
"cmd.group.delete.option.group.description": "ID grupy",
|
||||
"cmd.group.delete.success": "Grupa została pomyślnie usunięta."
|
||||
"cmd.group.delete.success": "Grupa została pomyślnie usunięta.",
|
||||
|
||||
"cmd.coords.description": "Tłumaczy kordy wiosek",
|
||||
"cmd.coords.option.version.description": "np. www.tribalwars.net, www.plemiona.pl",
|
||||
"cmd.coords.option.server.description": "Serwer (np. en115, pl170)",
|
||||
"cmd.coords.option.coords.description": "Kordy",
|
||||
"cmd.coords.embed.title": "Wioski",
|
||||
"cmd.coords.embed.description.village": "{{ buildLink .Village.FullName .Village.ProfileURL }} należąca do {{ buildPlayerMarkdown .Village.Player }}",
|
||||
"cmd.coords.embed.description.not-found": "**Nie znaleziono**: {{ .Coords }}",
|
||||
"cmd.coords.embed.button.next": "Następna strona",
|
||||
"cmd.coords.embed.button.prev": "Poprzednia strona",
|
||||
"cmd.coords.embed.footer": "Strona {{ .Page }}/{{ .MaxPage }}"
|
||||
}
|
||||
|
|
|
@ -129,28 +129,12 @@ func (c ennoblementNotificationToMessageEmbedConverter) buildDescription(n domai
|
|||
"Ennoblement": n.Ennoblement,
|
||||
},
|
||||
Funcs: template.FuncMap{
|
||||
"buildPlayerMarkdown": c.buildPlayerMarkdown,
|
||||
"buildPlayerMarkdown": buildPlayerMarkdown,
|
||||
"buildLink": buildLink,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (c ennoblementNotificationToMessageEmbedConverter) buildPlayerMarkdown(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 (c ennoblementNotificationToMessageEmbedConverter) mapNotificationTypeToColor(t domain.EnnoblementNotificationType) int {
|
||||
switch t {
|
||||
case domain.EnnoblementNotificationTypeGain:
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/discord/internal/discordi18n"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
type choiceService struct {
|
||||
localizer *discordi18n.Localizer
|
||||
choiceSvc ChoiceService
|
||||
}
|
||||
|
||||
func (s *choiceService) versions(ctx context.Context) ([]*discordgo.ApplicationCommandOptionChoice, error) {
|
||||
choices, err := s.choiceSvc.Versions(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ChoiceService.Versions: %w", err)
|
||||
}
|
||||
|
||||
dcChoices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(choices))
|
||||
for _, v := range choices {
|
||||
dcChoices = append(dcChoices, &discordgo.ApplicationCommandOptionChoice{
|
||||
Name: v.Name,
|
||||
Value: v.Value,
|
||||
})
|
||||
}
|
||||
|
||||
return dcChoices, nil
|
||||
}
|
||||
|
||||
func (s *choiceService) languages() ([]*discordgo.ApplicationCommandOptionChoice, error) {
|
||||
tags := s.localizer.LanguageTags()
|
||||
|
||||
choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
lang, langLocalizations, err := s.localizer.LocalizeDiscord(buildLangMessageID(tag.String()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
|
||||
Name: lang,
|
||||
NameLocalizations: langLocalizations,
|
||||
Value: tag.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return choices, nil
|
||||
}
|
|
@ -9,3 +9,19 @@ type NullBool struct {
|
|||
Bool bool
|
||||
Valid bool // Valid is true if Bool is not NULL
|
||||
}
|
||||
|
||||
func uniq[T comparable](sl []T) []T {
|
||||
res := make([]T, 0, len(sl))
|
||||
seen := make(map[T]struct{}, len(sl))
|
||||
|
||||
for _, it := range sl {
|
||||
if _, ok := seen[it]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[it] = struct{}{}
|
||||
res = append(res, it)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -32,3 +32,69 @@ func (e RequiredError) Params() map[string]any {
|
|||
"Field": e.Field,
|
||||
}
|
||||
}
|
||||
|
||||
type GreaterEqualThanError struct {
|
||||
Field string
|
||||
Threshold int
|
||||
}
|
||||
|
||||
var _ TranslatableError = GreaterEqualThanError{}
|
||||
|
||||
func (e GreaterEqualThanError) Error() string {
|
||||
return fmt.Sprintf("%s must be no less than %d", e.Field, e.Threshold)
|
||||
}
|
||||
|
||||
func (e GreaterEqualThanError) Slug() string {
|
||||
return "greater-equal-than"
|
||||
}
|
||||
|
||||
func (e GreaterEqualThanError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"Field": e.Field,
|
||||
"Threshold": e.Threshold,
|
||||
}
|
||||
}
|
||||
|
||||
type LessEqualThanError struct {
|
||||
Field string
|
||||
Threshold int
|
||||
}
|
||||
|
||||
var _ TranslatableError = LessEqualThanError{}
|
||||
|
||||
func (e LessEqualThanError) Error() string {
|
||||
return fmt.Sprintf("%s must be no no greater than %d", e.Field, e.Threshold)
|
||||
}
|
||||
|
||||
func (e LessEqualThanError) Slug() string {
|
||||
return "less-equal-than"
|
||||
}
|
||||
|
||||
func (e LessEqualThanError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"Field": e.Field,
|
||||
"Threshold": e.Threshold,
|
||||
}
|
||||
}
|
||||
|
||||
type LengthTooShortError struct {
|
||||
Field string
|
||||
Threshold int
|
||||
}
|
||||
|
||||
var _ TranslatableError = LengthTooShortError{}
|
||||
|
||||
func (e LengthTooShortError) Error() string {
|
||||
return fmt.Sprintf("%s: the length must be no less than %d", e.Field, e.Threshold)
|
||||
}
|
||||
|
||||
func (e LengthTooShortError) Slug() string {
|
||||
return "length-too-short"
|
||||
}
|
||||
|
||||
func (e LengthTooShortError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"Field": e.Field,
|
||||
"Threshold": e.Threshold,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ type TribeTagNotFoundError struct {
|
|||
var _ TranslatableError = TribeTagNotFoundError{}
|
||||
|
||||
func (e TribeTagNotFoundError) Error() string {
|
||||
return fmt.Sprintf("tribe (versionCode=%s,serverKey=%s,tag=%s) not found", e.VersionCode, e.ServerKey, e.Tag)
|
||||
return fmt.Sprintf("tribe (VersionCode=%s,ServerKey=%s,Tag=%s) not found", e.VersionCode, e.ServerKey, e.Tag)
|
||||
}
|
||||
|
||||
func (e TribeTagNotFoundError) Slug() string {
|
||||
|
@ -58,7 +58,7 @@ type TribeIDNotFoundError struct {
|
|||
var _ TranslatableError = TribeIDNotFoundError{}
|
||||
|
||||
func (e TribeIDNotFoundError) Error() string {
|
||||
return fmt.Sprintf("tribe (versionCode=%s,serverKey=%s,id=%d) not found", e.VersionCode, e.ServerKey, e.ID)
|
||||
return fmt.Sprintf("tribe (VersionCode=%s,ServerKey=%s,ID=%d) not found", e.VersionCode, e.ServerKey, e.ID)
|
||||
}
|
||||
|
||||
func (e TribeIDNotFoundError) Slug() string {
|
||||
|
|
|
@ -16,7 +16,7 @@ type TWServerNotFoundError struct {
|
|||
var _ TranslatableError = TWServerNotFoundError{}
|
||||
|
||||
func (e TWServerNotFoundError) Error() string {
|
||||
return fmt.Sprintf("server (versionCode=%s,key=%s) not found", e.VersionCode, e.Key)
|
||||
return fmt.Sprintf("server (VersionCode=%s,Key=%s) not found", e.VersionCode, e.Key)
|
||||
}
|
||||
|
||||
func (e TWServerNotFoundError) Slug() string {
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Village struct {
|
||||
ID int64
|
||||
FullName string
|
||||
|
@ -10,9 +19,215 @@ type Village struct {
|
|||
Player NullPlayerMeta
|
||||
}
|
||||
|
||||
func (v Village) Coords() string {
|
||||
return fmt.Sprintf("%d|%d", v.X, v.Y)
|
||||
}
|
||||
|
||||
type VillageMeta struct {
|
||||
ID int64
|
||||
FullName string
|
||||
ProfileURL string
|
||||
Player NullPlayerMeta
|
||||
}
|
||||
|
||||
type VillageCoordsStringTooLongError struct {
|
||||
Str string
|
||||
Max int
|
||||
}
|
||||
|
||||
var _ TranslatableError = VillageCoordsStringTooLongError{}
|
||||
|
||||
func (e VillageCoordsStringTooLongError) Error() string {
|
||||
return fmt.Sprintf("coords string is too long (%d/%d)", len(e.Str), e.Max)
|
||||
}
|
||||
|
||||
func (e VillageCoordsStringTooLongError) Slug() string {
|
||||
return "village-coords-string-too-long"
|
||||
}
|
||||
|
||||
func (e VillageCoordsStringTooLongError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"Str": e.Str,
|
||||
"Max": e.Max,
|
||||
}
|
||||
}
|
||||
|
||||
type VillageCoordsNotFoundInStringError struct {
|
||||
Str string
|
||||
}
|
||||
|
||||
var _ TranslatableError = VillageCoordsNotFoundInStringError{}
|
||||
|
||||
func (e VillageCoordsNotFoundInStringError) Error() string {
|
||||
return fmt.Sprintf("no coords found in string: '%s'", e.Str)
|
||||
}
|
||||
|
||||
func (e VillageCoordsNotFoundInStringError) Slug() string {
|
||||
return "village-coords-not-found-in-string"
|
||||
}
|
||||
|
||||
func (e VillageCoordsNotFoundInStringError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"Str": e.Str,
|
||||
}
|
||||
}
|
||||
|
||||
type TranslateVillageCoordsParams struct {
|
||||
versionCode string
|
||||
serverKey string
|
||||
coords []string
|
||||
perPage int32
|
||||
maxPage int32
|
||||
sha256 string
|
||||
}
|
||||
|
||||
var villageCoordsRegex = regexp.MustCompile(`(\d+)\|(\d+)`)
|
||||
|
||||
const (
|
||||
VillageCoordsStringMaxLength = 1000
|
||||
translateVillageCoordsMinPerPage = 1
|
||||
)
|
||||
|
||||
func NewTranslateVillageCoordsParams(versionCode, serverKey, coordsStr string, perPage int32) (TranslateVillageCoordsParams, error) {
|
||||
if versionCode == "" {
|
||||
return TranslateVillageCoordsParams{}, RequiredError{Field: "VersionCode"}
|
||||
}
|
||||
|
||||
if serverKey == "" {
|
||||
return TranslateVillageCoordsParams{}, RequiredError{Field: "ServerKey"}
|
||||
}
|
||||
|
||||
if perPage < translateVillageCoordsMinPerPage {
|
||||
return TranslateVillageCoordsParams{}, GreaterEqualThanError{
|
||||
Field: "PerPage",
|
||||
Threshold: translateVillageCoordsMinPerPage,
|
||||
}
|
||||
}
|
||||
|
||||
if len(coordsStr) > VillageCoordsStringMaxLength {
|
||||
return TranslateVillageCoordsParams{}, VillageCoordsStringTooLongError{
|
||||
Max: VillageCoordsStringMaxLength,
|
||||
Str: coordsStr,
|
||||
}
|
||||
}
|
||||
|
||||
coords := uniq(villageCoordsRegex.FindAllString(coordsStr, -1))
|
||||
if len(coords) == 0 {
|
||||
return TranslateVillageCoordsParams{}, VillageCoordsNotFoundInStringError{
|
||||
Str: coordsStr,
|
||||
}
|
||||
}
|
||||
|
||||
sum256 := sha256.Sum256(fmt.Appendf(nil, "%s-%s-%s-%d", versionCode, serverKey, strings.Join(coords, ","), perPage))
|
||||
|
||||
return TranslateVillageCoordsParams{
|
||||
versionCode: versionCode,
|
||||
serverKey: serverKey,
|
||||
coords: coords,
|
||||
perPage: perPage,
|
||||
maxPage: int32(math.Ceil(float64(len(coords)) / float64(perPage))),
|
||||
sha256: hex.EncodeToString(sum256[:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UnmarshalTranslateVillageCoordsParamsFromDatabase unmarshals TranslateVillageCoordsParams from the database.
|
||||
//
|
||||
// It should be used only for unmarshalling from the database!
|
||||
// You can't use UnmarshalTranslateVillageCoordsParamsFromDatabase as constructor - It may put domain into the invalid state!
|
||||
func UnmarshalTranslateVillageCoordsParamsFromDatabase(
|
||||
versionCode string,
|
||||
serverKey string,
|
||||
coords []string,
|
||||
perPage int32,
|
||||
maxPage int32,
|
||||
sha256Hash string,
|
||||
) (TranslateVillageCoordsParams, error) {
|
||||
if versionCode == "" {
|
||||
return TranslateVillageCoordsParams{}, RequiredError{Field: "VersionCode"}
|
||||
}
|
||||
|
||||
if serverKey == "" {
|
||||
return TranslateVillageCoordsParams{}, RequiredError{Field: "ServerKey"}
|
||||
}
|
||||
|
||||
if len(coords) == 0 {
|
||||
return TranslateVillageCoordsParams{}, LengthTooShortError{
|
||||
Field: "Coords",
|
||||
Threshold: 1,
|
||||
}
|
||||
}
|
||||
|
||||
if perPage < translateVillageCoordsMinPerPage {
|
||||
return TranslateVillageCoordsParams{}, GreaterEqualThanError{
|
||||
Field: "PerPage",
|
||||
Threshold: translateVillageCoordsMinPerPage,
|
||||
}
|
||||
}
|
||||
|
||||
if sha256Hash == "" {
|
||||
return TranslateVillageCoordsParams{}, RequiredError{Field: "SHA256"}
|
||||
}
|
||||
|
||||
return TranslateVillageCoordsParams{
|
||||
versionCode: versionCode,
|
||||
serverKey: serverKey,
|
||||
coords: coords,
|
||||
perPage: perPage,
|
||||
maxPage: maxPage,
|
||||
sha256: sha256Hash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t TranslateVillageCoordsParams) VersionCode() string {
|
||||
return t.versionCode
|
||||
}
|
||||
|
||||
func (t TranslateVillageCoordsParams) ServerKey() string {
|
||||
return t.serverKey
|
||||
}
|
||||
|
||||
func (t TranslateVillageCoordsParams) Coords() []string {
|
||||
return t.coords
|
||||
}
|
||||
|
||||
func (t TranslateVillageCoordsParams) PerPage() int32 {
|
||||
return t.perPage
|
||||
}
|
||||
|
||||
func (t TranslateVillageCoordsParams) MaxPage() int32 {
|
||||
return t.maxPage
|
||||
}
|
||||
|
||||
func (t TranslateVillageCoordsParams) SHA256() string {
|
||||
return t.sha256
|
||||
}
|
||||
|
||||
type TranslateVillageCoordsResult struct {
|
||||
Villages []Village
|
||||
NotFound []string
|
||||
HasNext bool
|
||||
HasPrev bool
|
||||
Page int32
|
||||
MaxPage int32
|
||||
ParamsSHA256 string
|
||||
}
|
||||
|
||||
type TranslateVillageCoordsParamsNotFoundError struct {
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
var _ TranslatableError = TranslateVillageCoordsParamsNotFoundError{}
|
||||
|
||||
func (e TranslateVillageCoordsParamsNotFoundError) Error() string {
|
||||
return fmt.Sprintf("params (SHA256=%s) not found", e.SHA256)
|
||||
}
|
||||
|
||||
func (e TranslateVillageCoordsParamsNotFoundError) Slug() string {
|
||||
return "translate-village-coords-params-not-found"
|
||||
}
|
||||
|
||||
func (e TranslateVillageCoordsParamsNotFoundError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"SHA256": e.SHA256,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewTranslateVillageCoordsParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tooLongStr := ""
|
||||
for len(tooLongStr) <= 1000 {
|
||||
tooLongStr += "too long"
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
versionCode string
|
||||
serverKey string
|
||||
coordsStr string
|
||||
coords []string
|
||||
perPage int32
|
||||
maxPage int32
|
||||
err error
|
||||
}{
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "pl181",
|
||||
coordsStr: "test string 123|123 898|123",
|
||||
coords: []string{"123|123", "898|123"},
|
||||
perPage: 5,
|
||||
maxPage: 1,
|
||||
},
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "pl181",
|
||||
coordsStr: "123|123 898|123 988|123 123|158 785|567",
|
||||
coords: []string{"123|123", "898|123", "988|123", "123|158", "785|567"},
|
||||
perPage: 4,
|
||||
maxPage: 2,
|
||||
},
|
||||
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "pl181",
|
||||
coordsStr: "123|123 123|123 898|898 898|898",
|
||||
coords: []string{"123|123", "898|898"},
|
||||
perPage: 4,
|
||||
maxPage: 1,
|
||||
},
|
||||
{
|
||||
versionCode: "",
|
||||
serverKey: "pl181",
|
||||
coordsStr: "123|123",
|
||||
err: domain.RequiredError{
|
||||
Field: "VersionCode",
|
||||
},
|
||||
perPage: 1,
|
||||
},
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "",
|
||||
coordsStr: "123|123",
|
||||
err: domain.RequiredError{
|
||||
Field: "ServerKey",
|
||||
},
|
||||
perPage: 1,
|
||||
},
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "pl181",
|
||||
coordsStr: "123|123",
|
||||
err: domain.GreaterEqualThanError{
|
||||
Field: "PerPage",
|
||||
Threshold: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "pl181",
|
||||
coordsStr: "no coords",
|
||||
err: domain.VillageCoordsNotFoundInStringError{
|
||||
Str: "no coords",
|
||||
},
|
||||
perPage: 1,
|
||||
},
|
||||
{
|
||||
versionCode: "pl",
|
||||
serverKey: "pl181",
|
||||
coordsStr: tooLongStr,
|
||||
err: domain.VillageCoordsStringTooLongError{
|
||||
Str: tooLongStr,
|
||||
Max: 1000,
|
||||
},
|
||||
perPage: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
testName := fmt.Sprintf("versionCode=%s | serverKey=%s | perPage=%d | coordsStr=%s", tt.versionCode, tt.serverKey, tt.perPage, tt.coordsStr)
|
||||
if len(testName) > 100 {
|
||||
testName = testName[:97] + "..."
|
||||
}
|
||||
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := domain.NewTranslateVillageCoordsParams(tt.versionCode, tt.serverKey, tt.coordsStr, tt.perPage)
|
||||
if tt.err != nil {
|
||||
assert.ErrorIs(t, err, tt.err)
|
||||
assert.Zero(t, res)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.versionCode, res.VersionCode())
|
||||
assert.Equal(t, tt.serverKey, res.ServerKey())
|
||||
assert.Equal(t, tt.coords, res.Coords())
|
||||
assert.Equal(t, tt.perPage, res.PerPage())
|
||||
assert.Equal(t, tt.maxPage, res.MaxPage())
|
||||
assert.NotEmpty(t, res.SHA256())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTranslateVillageCoordsParamsFromDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewTranslateVillageCoordsParams("pl", "pl181", "123|123 871|981", 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
paramsUnmarshal, err := domain.UnmarshalTranslateVillageCoordsParamsFromDatabase(
|
||||
params.VersionCode(),
|
||||
params.ServerKey(),
|
||||
params.Coords(),
|
||||
params.PerPage(),
|
||||
params.MaxPage(),
|
||||
params.SHA256(),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, params, paramsUnmarshal)
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
)
|
||||
|
||||
//counterfeiter:generate -o internal/mock/village_repository.gen.go . VillageRepository
|
||||
type VillageRepository interface {
|
||||
SaveTranslateCoordsParams(ctx context.Context, params domain.TranslateVillageCoordsParams) error
|
||||
GetTranslateCoordsParams(ctx context.Context, sha256Hash string) (domain.TranslateVillageCoordsParams, error)
|
||||
}
|
||||
|
||||
type Village struct {
|
||||
repo VillageRepository
|
||||
twhelpSvc TWHelpService
|
||||
}
|
||||
|
||||
func NewVillage(repo VillageRepository, twhelpSvc TWHelpService) *Village {
|
||||
return &Village{repo: repo, twhelpSvc: twhelpSvc}
|
||||
}
|
||||
|
||||
func (v *Village) TranslateCoords(
|
||||
ctx context.Context,
|
||||
params domain.TranslateVillageCoordsParams,
|
||||
page int32,
|
||||
) (domain.TranslateVillageCoordsResult, error) {
|
||||
if page < 1 {
|
||||
return domain.TranslateVillageCoordsResult{}, domain.GreaterEqualThanError{Field: "page", Threshold: 1}
|
||||
}
|
||||
|
||||
if page > params.MaxPage() {
|
||||
return domain.TranslateVillageCoordsResult{}, domain.LessEqualThanError{Field: "page", Threshold: int(params.MaxPage())}
|
||||
}
|
||||
|
||||
if err := v.repo.SaveTranslateCoordsParams(ctx, params); err != nil {
|
||||
return domain.TranslateVillageCoordsResult{}, err
|
||||
}
|
||||
|
||||
low := params.PerPage() * (page - 1)
|
||||
high := params.PerPage() * page
|
||||
if coordsLen := int32(len(params.Coords())); coordsLen < high {
|
||||
high = coordsLen
|
||||
}
|
||||
|
||||
coords := params.Coords()[low:high]
|
||||
|
||||
villages, err := v.twhelpSvc.ListVillagesByCoords(ctx, params.VersionCode(), params.ServerKey(), coords, 0, high-low)
|
||||
if err != nil {
|
||||
return domain.TranslateVillageCoordsResult{}, err
|
||||
}
|
||||
|
||||
notFound := make([]string, 0, high)
|
||||
for _, co := range coords {
|
||||
found := false
|
||||
|
||||
for _, v := range villages {
|
||||
if co == v.Coords() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
notFound = append(notFound, co)
|
||||
}
|
||||
}
|
||||
|
||||
return domain.TranslateVillageCoordsResult{
|
||||
Villages: villages,
|
||||
NotFound: notFound,
|
||||
HasNext: params.MaxPage() > page,
|
||||
HasPrev: page > 1,
|
||||
Page: page,
|
||||
MaxPage: params.MaxPage(),
|
||||
ParamsSHA256: params.SHA256(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *Village) TranslateCoordsFromHash(ctx context.Context, paramsSHA256Hash string, page int32) (domain.TranslateVillageCoordsResult, error) {
|
||||
params, err := v.repo.GetTranslateCoordsParams(ctx, paramsSHA256Hash)
|
||||
if err != nil {
|
||||
return domain.TranslateVillageCoordsResult{}, err
|
||||
}
|
||||
|
||||
return v.TranslateCoords(ctx, params, page)
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/service"
|
||||
"gitea.dwysokinski.me/twhelp/dcbot/internal/service/internal/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVillage_TranslateCoords(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
villages := []domain.Village{
|
||||
{
|
||||
X: 123,
|
||||
Y: 123,
|
||||
},
|
||||
{
|
||||
X: 897,
|
||||
Y: 123,
|
||||
},
|
||||
{
|
||||
X: 778,
|
||||
Y: 689,
|
||||
},
|
||||
}
|
||||
|
||||
notFound := []string{
|
||||
"789|123",
|
||||
"678|123",
|
||||
}
|
||||
|
||||
var builderCoordsStr strings.Builder
|
||||
|
||||
for _, v := range villages {
|
||||
builderCoordsStr.WriteString(v.Coords() + " ")
|
||||
}
|
||||
|
||||
for _, coords := range notFound {
|
||||
builderCoordsStr.WriteString(coords + " ")
|
||||
}
|
||||
|
||||
params, err := domain.NewTranslateVillageCoordsParams("pl", "pl181", builderCoordsStr.String(), 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
page int32
|
||||
hasNext bool
|
||||
hasPrev bool
|
||||
villages []domain.Village
|
||||
notFound []string
|
||||
}{
|
||||
{
|
||||
page: 1,
|
||||
hasNext: true,
|
||||
hasPrev: false,
|
||||
villages: []domain.Village{
|
||||
villages[0],
|
||||
villages[1],
|
||||
},
|
||||
},
|
||||
{
|
||||
page: 2,
|
||||
hasNext: true,
|
||||
hasPrev: true,
|
||||
villages: []domain.Village{
|
||||
villages[2],
|
||||
},
|
||||
notFound: []string{
|
||||
notFound[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
page: 3,
|
||||
hasNext: false,
|
||||
hasPrev: true,
|
||||
notFound: []string{
|
||||
notFound[1],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
twhelpSvc := &mock.FakeTWHelpService{}
|
||||
twhelpSvc.ListVillagesByCoordsCalls(func(
|
||||
ctx context.Context,
|
||||
versionCode string,
|
||||
serverKey string,
|
||||
coords []string,
|
||||
offset int32,
|
||||
perPage int32,
|
||||
) ([]domain.Village, error) {
|
||||
if versionCode != params.VersionCode() || serverKey != params.ServerKey() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
res := make([]domain.Village, 0, len(coords))
|
||||
|
||||
for _, c := range coords {
|
||||
for _, v := range villages {
|
||||
if v.Coords() == c {
|
||||
res = append(res, v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
})
|
||||
|
||||
res, err := service.NewVillage(&mock.FakeVillageRepository{}, twhelpSvc).
|
||||
TranslateCoords(context.Background(), params, tt.page)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res.Villages, len(tt.villages))
|
||||
for _, v := range tt.villages {
|
||||
assert.Contains(t, res.Villages, v)
|
||||
}
|
||||
assert.Len(t, res.NotFound, len(tt.notFound))
|
||||
for _, coords := range tt.notFound {
|
||||
assert.Contains(t, res.NotFound, coords)
|
||||
}
|
||||
assert.Equal(t, tt.hasPrev, res.HasPrev)
|
||||
assert.Equal(t, tt.hasNext, res.HasNext)
|
||||
assert.Equal(t, tt.page, res.Page)
|
||||
assert.Equal(t, params.MaxPage(), res.MaxPage)
|
||||
assert.Equal(t, params.SHA256(), res.ParamsSHA256)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ERR: page < 1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := service.NewVillage(nil, nil).
|
||||
TranslateCoords(context.Background(), domain.TranslateVillageCoordsParams{}, 0)
|
||||
assert.ErrorIs(t, err, domain.GreaterEqualThanError{
|
||||
Field: "page",
|
||||
Threshold: 1,
|
||||
})
|
||||
assert.Zero(t, res)
|
||||
})
|
||||
|
||||
t.Run("ERR: page > max page", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := service.NewVillage(nil, nil).
|
||||
TranslateCoords(context.Background(), domain.TranslateVillageCoordsParams{}, 1)
|
||||
assert.ErrorIs(t, err, domain.LessEqualThanError{
|
||||
Field: "page",
|
||||
Threshold: 0,
|
||||
})
|
||||
assert.Zero(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVillage_TranslateCoordsFromHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewTranslateVillageCoordsParams("pl", "pl181", "123|123 897|123 778|689 789|123 678|123", 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
repo := &mock.FakeVillageRepository{}
|
||||
repo.GetTranslateCoordsParamsCalls(func(ctx context.Context, hash string) (domain.TranslateVillageCoordsParams, error) {
|
||||
if hash != params.SHA256() {
|
||||
return domain.TranslateVillageCoordsParams{}, domain.TranslateVillageCoordsParamsNotFoundError{
|
||||
SHA256: hash,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
})
|
||||
|
||||
res, err := service.NewVillage(repo, &mock.FakeTWHelpService{}).
|
||||
TranslateCoordsFromHash(context.Background(), params.SHA256(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, res)
|
||||
}
|
|
@ -23,6 +23,11 @@ spec:
|
|||
secretKeyRef:
|
||||
name: twhelp-dcbot-secret
|
||||
key: db-dsn
|
||||
- name: REDIS_CONNECTION_STRING
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: twhelp-dcbot-secret
|
||||
key: redis-connection-string
|
||||
- name: DB_MAX_OPEN_CONNECTIONS
|
||||
value: "10"
|
||||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
|
|
|
@ -16,6 +16,11 @@ spec:
|
|||
secretKeyRef:
|
||||
name: twhelp-dcbot-secret
|
||||
key: db-dsn
|
||||
- name: REDIS_CONNECTION_STRING
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: twhelp-dcbot-secret
|
||||
key: redis-connection-string
|
||||
- name: DB_MAX_OPEN_CONNECTIONS
|
||||
value: "10"
|
||||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
|
|
|
@ -6,4 +6,6 @@ type: Opaque
|
|||
data:
|
||||
# postgres://dcbot:dcbot@twdcbotdb-postgresql:5432/dcbot?sslmode=disable
|
||||
db-dsn: cG9zdGdyZXM6Ly9kY2JvdDpkY2JvdEB0d2RjYm90ZGItcG9zdGdyZXNxbDo1NDMyL2RjYm90P3NzbG1vZGU9ZGlzYWJsZQ==
|
||||
token: tokenhere
|
||||
# redis://:dcbot@twdcbotredis-master:6379/0
|
||||
redis-connection-string: cmVkaXM6Ly86ZGNib3RAdHdkY2JvdHJlZGlzLW1hc3Rlcjo2Mzc5LzA=
|
||||
token: tokenhere
|
||||
|
|
|
@ -16,6 +16,11 @@ spec:
|
|||
secretKeyRef:
|
||||
name: twhelp-dcbot-secret
|
||||
key: db-dsn
|
||||
- name: REDIS_CONNECTION_STRING
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: twhelp-dcbot-secret
|
||||
key: redis-connection-string
|
||||
- name: DB_MAX_OPEN_CONNECTIONS
|
||||
value: "15"
|
||||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
|
|
|
@ -34,4 +34,13 @@ deploy:
|
|||
auth.username: dcbot
|
||||
auth.password: dcbot
|
||||
auth.database: dcbot
|
||||
- name: twdcbotredis
|
||||
repo: https://charts.bitnami.com/bitnami
|
||||
remoteChart: redis
|
||||
version: 17.11.8
|
||||
wait: true
|
||||
setValues:
|
||||
architecture: standalone
|
||||
image.tag: 7.0.12
|
||||
auth.password: dcbot
|
||||
kubectl: {}
|
||||
|
|
Loading…
Reference in New Issue