Reviewed-on: #116
This commit is contained in:
parent
71a61533db
commit
d1ba315bd2
|
@ -9,6 +9,7 @@ steps:
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
TESTS_DB_DSN: postgres://postgres:dcbot@database:5432/dcbot?sslmode=disable
|
TESTS_DB_DSN: postgres://postgres:dcbot@database:5432/dcbot?sslmode=disable
|
||||||
|
TESTS_REDIS_CONNECTION_STRING: redis://:@redis:6379/0
|
||||||
commands:
|
commands:
|
||||||
- make generate
|
- make generate
|
||||||
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
@ -19,6 +20,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: dcbot
|
POSTGRES_DB: dcbot
|
||||||
POSTGRES_PASSWORD: dcbot
|
POSTGRES_PASSWORD: dcbot
|
||||||
|
- name: redis
|
||||||
|
image: redis:7.0.12
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
|
@ -392,6 +395,6 @@ depends_on:
|
||||||
- migrations-manifest
|
- migrations-manifest
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: 4fa2583c281aacc0e4d0dc87296a2ce6ff7facf71d1ce2e9f5d4157b35635d4f
|
hmac: ed0db55292798e0b63ba43ae50c749730c9a72423f6387b670e3fd7f68d2f5ad
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -35,6 +35,7 @@ func NewBunDB() (*bun.DB, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), dbPingTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), dbPingTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := db.PingContext(ctx); err != nil {
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
return nil, fmt.Errorf("db.PingContext: %w", err)
|
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()
|
_ = db.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
redisClient, err := internal.NewRedisClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
twhelpService, err := internal.NewTWHelpService(c.App.Version)
|
twhelpService, err := internal.NewTWHelpService(c.App.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("internal.NewTWHelpService: %w", err)
|
return fmt.Errorf("internal.NewTWHelpService: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
groupRepo := adapter.NewGroupBun(db)
|
groupRepo := adapter.NewGroupBun(db)
|
||||||
|
villageRepo := adapter.NewVillageRedis(redisClient)
|
||||||
|
|
||||||
choiceSvc := service.NewChoice(twhelpService)
|
choiceSvc := service.NewChoice(twhelpService)
|
||||||
groupSvc := service.NewGroup(groupRepo, twhelpService, logger, cfg.MaxGroupsPerServer, cfg.MaxMonitorsPerGroup)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("discord.NewBot: %w", err)
|
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/nicksnyder/go-i18n/v2 v2.2.1
|
||||||
github.com/ory/dockertest/v3 v3.10.0
|
github.com/ory/dockertest/v3 v3.10.0
|
||||||
github.com/pressly/goose/v3 v3.11.2
|
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/robfig/cron/v3 v3.0.1
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/uptrace/bun v1.1.14
|
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/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // 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/containerd/continuity v0.3.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // 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/cli v23.0.6+incompatible // indirect
|
||||||
github.com/docker/docker v23.0.6+incompatible // indirect
|
github.com/docker/docker v23.0.6+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // 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 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
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/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 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
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 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
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 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
|
||||||
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:CScadyCJ2ZKUDpAMZta6vK8I+6/m60VIjGIV7Wg/Eu4=
|
||||||
github.com/docker/cli v23.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
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=
|
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/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 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs=
|
||||||
github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk=
|
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/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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
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"
|
"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) {
|
func TestMain(m *testing.M) {
|
||||||
goose.SetBaseFS(os.DirFS("../../migrations"))
|
goose.SetBaseFS(os.DirFS("../../migrations"))
|
||||||
goose.SetLogger(goose.NopLogger())
|
goose.SetLogger(goose.NopLogger())
|
||||||
|
|
||||||
_, isDBDSNPresent := os.LookupEnv(envDBDSN)
|
_, isDBDSNPresent := os.LookupEnv(envDBDSN)
|
||||||
|
_, isRedisConnectionStringPresent := os.LookupEnv(envRedisConnectionString)
|
||||||
|
|
||||||
// all required envs are set, there is no need to use dockertest
|
// all required envs are set, there is no need to use dockertest
|
||||||
if isDBDSNPresent {
|
if isDBDSNPresent && isRedisConnectionStringPresent {
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +49,12 @@ func TestMain(m *testing.M) {
|
||||||
resources = append(resources, resource)
|
resources = append(resources, resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isRedisConnectionStringPresent {
|
||||||
|
resource, connString := newRedis(pool)
|
||||||
|
_ = os.Setenv(envRedisConnectionString, connString.String())
|
||||||
|
resources = append(resources, resource)
|
||||||
|
}
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
||||||
for _, r := range resources {
|
for _, r := range resources {
|
||||||
|
@ -66,7 +76,7 @@ func newPostgres(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
||||||
|
|
||||||
pw, _ := dsn.User.Password()
|
pw, _ := dsn.User.Password()
|
||||||
|
|
||||||
resource, runErr := pool.RunWithOptions(&dockertest.RunOptions{
|
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||||
Repository: "postgres",
|
Repository: "postgres",
|
||||||
Tag: "14.8",
|
Tag: "14.8",
|
||||||
Env: []string{
|
Env: []string{
|
||||||
|
@ -80,8 +90,8 @@ func newPostgres(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
||||||
Name: "no",
|
Name: "no",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if runErr != nil {
|
if err != nil {
|
||||||
log.Fatalf("couldn't run postgres: %s", runErr)
|
log.Fatalf("couldn't run postgres: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = resource.Expire(120)
|
_ = resource.Expire(120)
|
||||||
|
@ -91,6 +101,32 @@ func newPostgres(pool *dockertest.Pool) (*dockertest.Resource, *url.URL) {
|
||||||
return resource, dsn
|
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 {
|
func getHostPort(resource *dockertest.Resource, id string) string {
|
||||||
dockerURL := os.Getenv("DOCKER_HOST")
|
dockerURL := os.Getenv("DOCKER_HOST")
|
||||||
if dockerURL == "" {
|
if dockerURL == "" {
|
||||||
|
|
|
@ -29,7 +29,7 @@ func newBunDB(tb testing.TB) *bun.DB {
|
||||||
sqldb := sql.OpenDB(
|
sqldb := sql.OpenDB(
|
||||||
pgdriver.NewConnector(
|
pgdriver.NewConnector(
|
||||||
pgdriver.WithDSN(os.Getenv(envDBDSN)),
|
pgdriver.WithDSN(os.Getenv(envDBDSN)),
|
||||||
pgdriver.WithConnParams(map[string]interface{}{
|
pgdriver.WithConnParams(map[string]any{
|
||||||
"search_path": schema,
|
"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)
|
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 {
|
type Bot struct {
|
||||||
session *discordgo.Session
|
session *discordgo.Session
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
groupSvc GroupService
|
groupSvc GroupService
|
||||||
choiceSvc ChoiceService
|
choiceSvc *choiceService
|
||||||
logger *zap.Logger
|
villageSvc VillageService
|
||||||
localizer *discordi18n.Localizer
|
logger *zap.Logger
|
||||||
|
localizer *discordi18n.Localizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBot(
|
func NewBot(
|
||||||
token string,
|
token string,
|
||||||
groupSvc GroupService,
|
groupSvc GroupService,
|
||||||
client ChoiceService,
|
choiceSvc ChoiceService,
|
||||||
|
villageSvc VillageService,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) (*Bot, error) {
|
) (*Bot, error) {
|
||||||
localizer, err := discordi18n.NewLocalizer(language.English)
|
localizer, err := discordi18n.NewLocalizer(language.English)
|
||||||
|
@ -67,10 +78,14 @@ func NewBot(
|
||||||
cron.SkipIfStillRunning(cron.DiscardLogger),
|
cron.SkipIfStillRunning(cron.DiscardLogger),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
groupSvc: groupSvc,
|
groupSvc: groupSvc,
|
||||||
choiceSvc: client,
|
choiceSvc: &choiceService{
|
||||||
logger: logger,
|
choiceSvc: choiceSvc,
|
||||||
localizer: localizer,
|
localizer: localizer,
|
||||||
|
},
|
||||||
|
villageSvc: villageSvc,
|
||||||
|
logger: logger,
|
||||||
|
localizer: localizer,
|
||||||
}
|
}
|
||||||
|
|
||||||
b.session.AddHandler(b.handleSessionReady)
|
b.session.AddHandler(b.handleSessionReady)
|
||||||
|
@ -106,6 +121,7 @@ type command interface {
|
||||||
func (b *Bot) registerCommands() error {
|
func (b *Bot) registerCommands() error {
|
||||||
commands := []command{
|
commands := []command{
|
||||||
&groupCommand{localizer: b.localizer, logger: b.logger, groupSvc: b.groupSvc, choiceSvc: b.choiceSvc},
|
&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 {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -21,7 +20,7 @@ type groupCommand struct {
|
||||||
localizer *discordi18n.Localizer
|
localizer *discordi18n.Localizer
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
groupSvc GroupService
|
groupSvc GroupService
|
||||||
choiceSvc ChoiceService
|
choiceSvc *choiceService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *groupCommand) name() string {
|
func (c *groupCommand) name() string {
|
||||||
|
@ -100,12 +99,12 @@ func (c *groupCommand) buildSubcommandCreate() (*discordgo.ApplicationCommandOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *groupCommand) buildSubcommandCreateOptions() ([]*discordgo.ApplicationCommandOption, error) {
|
func (c *groupCommand) buildSubcommandCreateOptions() ([]*discordgo.ApplicationCommandOption, error) {
|
||||||
versionChoices, err := c.getVersionChoices()
|
versionChoices, err := c.choiceSvc.versions(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
languageChoices, err := c.getLanguageChoices()
|
languageChoices, err := c.choiceSvc.languages()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -208,40 +207,6 @@ func (c *groupCommand) buildSubcommandCreateOptions() ([]*discordgo.ApplicationC
|
||||||
}, nil
|
}, 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) {
|
func (c *groupCommand) buildSubcommandList() (*discordgo.ApplicationCommandOption, error) {
|
||||||
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.list.description")
|
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.list.description")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -420,7 +385,7 @@ func (c *groupCommand) buildSubcommandSet() (*discordgo.ApplicationCommandOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *groupCommand) buildSubcommandSetLanguage() (*discordgo.ApplicationCommandOption, error) {
|
func (c *groupCommand) buildSubcommandSetLanguage() (*discordgo.ApplicationCommandOption, error) {
|
||||||
languageChoices, err := c.getLanguageChoices()
|
languageChoices, err := c.choiceSvc.languages()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -877,6 +842,7 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
|
||||||
Type: discordgo.EmbedTypeRich,
|
Type: discordgo.EmbedTypeRich,
|
||||||
Title: title,
|
Title: title,
|
||||||
Fields: fields,
|
Fields: fields,
|
||||||
|
Color: colorGrey,
|
||||||
Timestamp: formatTimestamp(time.Now()),
|
Timestamp: formatTimestamp(time.Now()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -964,6 +930,7 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
|
||||||
Type: discordgo.EmbedTypeRich,
|
Type: discordgo.EmbedTypeRich,
|
||||||
Title: g.ID,
|
Title: g.ID,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
Color: colorGrey,
|
||||||
Timestamp: formatTimestamp(time.Now()),
|
Timestamp: formatTimestamp(time.Now()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,9 +3,12 @@ package discord
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
colorGrey = 0x808080
|
||||||
embedDescriptionCharLimit = 4096
|
embedDescriptionCharLimit = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,3 +37,19 @@ func boolToEmoji(val bool) string {
|
||||||
func buildLangMessageID(languageTag string) string {
|
func buildLangMessageID(languageTag string) string {
|
||||||
return "lang." + languageTag
|
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-tag-not-found": "Tribe (tag={{ .Tag }}) not found.",
|
||||||
"err.tribe-id-not-found": "Tribe (id={{ .ID }}) 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.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 }}).",
|
"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.description": "Deletes a group",
|
||||||
"cmd.group.delete.option.group.description": "Group ID",
|
"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-tag-not-found": "Plemię (skrót={{ .Tag }}) nie istnieje.",
|
||||||
"err.tribe-id-not-found": "Plemię (id={{ .ID }}) nie istnieje.",
|
"err.tribe-id-not-found": "Plemię (id={{ .ID }}) nie istnieje.",
|
||||||
"err.tribe-does-not-exist": "Plemię (skrót={{ .Tag }}) 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 }}).",
|
"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.description": "Usuwa grupę",
|
||||||
"cmd.group.delete.option.group.description": "ID grupy",
|
"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,
|
"Ennoblement": n.Ennoblement,
|
||||||
},
|
},
|
||||||
Funcs: template.FuncMap{
|
Funcs: template.FuncMap{
|
||||||
"buildPlayerMarkdown": c.buildPlayerMarkdown,
|
"buildPlayerMarkdown": buildPlayerMarkdown,
|
||||||
"buildLink": buildLink,
|
"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 {
|
func (c ennoblementNotificationToMessageEmbedConverter) mapNotificationTypeToColor(t domain.EnnoblementNotificationType) int {
|
||||||
switch t {
|
switch t {
|
||||||
case domain.EnnoblementNotificationTypeGain:
|
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
|
Bool bool
|
||||||
Valid bool // Valid is true if Bool is not NULL
|
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,
|
"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{}
|
var _ TranslatableError = TribeTagNotFoundError{}
|
||||||
|
|
||||||
func (e TribeTagNotFoundError) Error() string {
|
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 {
|
func (e TribeTagNotFoundError) Slug() string {
|
||||||
|
@ -58,7 +58,7 @@ type TribeIDNotFoundError struct {
|
||||||
var _ TranslatableError = TribeIDNotFoundError{}
|
var _ TranslatableError = TribeIDNotFoundError{}
|
||||||
|
|
||||||
func (e TribeIDNotFoundError) Error() string {
|
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 {
|
func (e TribeIDNotFoundError) Slug() string {
|
||||||
|
|
|
@ -16,7 +16,7 @@ type TWServerNotFoundError struct {
|
||||||
var _ TranslatableError = TWServerNotFoundError{}
|
var _ TranslatableError = TWServerNotFoundError{}
|
||||||
|
|
||||||
func (e TWServerNotFoundError) Error() string {
|
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 {
|
func (e TWServerNotFoundError) Slug() string {
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Village struct {
|
type Village struct {
|
||||||
ID int64
|
ID int64
|
||||||
FullName string
|
FullName string
|
||||||
|
@ -10,9 +19,215 @@ type Village struct {
|
||||||
Player NullPlayerMeta
|
Player NullPlayerMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v Village) Coords() string {
|
||||||
|
return fmt.Sprintf("%d|%d", v.X, v.Y)
|
||||||
|
}
|
||||||
|
|
||||||
type VillageMeta struct {
|
type VillageMeta struct {
|
||||||
ID int64
|
ID int64
|
||||||
FullName string
|
FullName string
|
||||||
ProfileURL string
|
ProfileURL string
|
||||||
Player NullPlayerMeta
|
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:
|
secretKeyRef:
|
||||||
name: twhelp-dcbot-secret
|
name: twhelp-dcbot-secret
|
||||||
key: db-dsn
|
key: db-dsn
|
||||||
|
- name: REDIS_CONNECTION_STRING
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: twhelp-dcbot-secret
|
||||||
|
key: redis-connection-string
|
||||||
- name: DB_MAX_OPEN_CONNECTIONS
|
- name: DB_MAX_OPEN_CONNECTIONS
|
||||||
value: "10"
|
value: "10"
|
||||||
- name: DB_MAX_IDLE_CONNECTIONS
|
- name: DB_MAX_IDLE_CONNECTIONS
|
||||||
|
|
|
@ -16,6 +16,11 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: twhelp-dcbot-secret
|
name: twhelp-dcbot-secret
|
||||||
key: db-dsn
|
key: db-dsn
|
||||||
|
- name: REDIS_CONNECTION_STRING
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: twhelp-dcbot-secret
|
||||||
|
key: redis-connection-string
|
||||||
- name: DB_MAX_OPEN_CONNECTIONS
|
- name: DB_MAX_OPEN_CONNECTIONS
|
||||||
value: "10"
|
value: "10"
|
||||||
- name: DB_MAX_IDLE_CONNECTIONS
|
- name: DB_MAX_IDLE_CONNECTIONS
|
||||||
|
|
|
@ -6,4 +6,6 @@ type: Opaque
|
||||||
data:
|
data:
|
||||||
# postgres://dcbot:dcbot@twdcbotdb-postgresql:5432/dcbot?sslmode=disable
|
# postgres://dcbot:dcbot@twdcbotdb-postgresql:5432/dcbot?sslmode=disable
|
||||||
db-dsn: cG9zdGdyZXM6Ly9kY2JvdDpkY2JvdEB0d2RjYm90ZGItcG9zdGdyZXNxbDo1NDMyL2RjYm90P3NzbG1vZGU9ZGlzYWJsZQ==
|
db-dsn: cG9zdGdyZXM6Ly9kY2JvdDpkY2JvdEB0d2RjYm90ZGItcG9zdGdyZXNxbDo1NDMyL2RjYm90P3NzbG1vZGU9ZGlzYWJsZQ==
|
||||||
token: tokenhere
|
# redis://:dcbot@twdcbotredis-master:6379/0
|
||||||
|
redis-connection-string: cmVkaXM6Ly86ZGNib3RAdHdkY2JvdHJlZGlzLW1hc3Rlcjo2Mzc5LzA=
|
||||||
|
token: tokenhere
|
||||||
|
|
|
@ -16,6 +16,11 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: twhelp-dcbot-secret
|
name: twhelp-dcbot-secret
|
||||||
key: db-dsn
|
key: db-dsn
|
||||||
|
- name: REDIS_CONNECTION_STRING
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: twhelp-dcbot-secret
|
||||||
|
key: redis-connection-string
|
||||||
- name: DB_MAX_OPEN_CONNECTIONS
|
- name: DB_MAX_OPEN_CONNECTIONS
|
||||||
value: "15"
|
value: "15"
|
||||||
- name: DB_MAX_IDLE_CONNECTIONS
|
- name: DB_MAX_IDLE_CONNECTIONS
|
||||||
|
|
|
@ -34,4 +34,13 @@ deploy:
|
||||||
auth.username: dcbot
|
auth.username: dcbot
|
||||||
auth.password: dcbot
|
auth.password: dcbot
|
||||||
auth.database: 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: {}
|
kubectl: {}
|
||||||
|
|
Loading…
Reference in New Issue