feat: coords translation

This commit is contained in:
Dawid Wysokiński 2023-07-05 08:37:52 +02:00
parent a8f309c299
commit 8a19630a9f
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
19 changed files with 802 additions and 91 deletions

View File

@ -49,9 +49,10 @@ func New() *cli.Command {
groupRepo := adapter.NewGroupBun(db)
choiceSvc := service.NewChoice(twhelpService)
coordsSvc := service.NewCoords(twhelpService)
groupSvc := service.NewGroup(groupRepo, twhelpService, logger, cfg.MaxGroupsPerServer, cfg.MaxMonitorsPerGroup)
bot, err := discord.NewBot(cfg.Token, groupSvc, choiceSvc, logger)
bot, err := discord.NewBot(cfg.Token, groupSvc, choiceSvc, coordsSvc, logger)
if err != nil {
return fmt.Errorf("discord.NewBot: %w", err)
}
@ -69,8 +70,6 @@ func New() *cli.Command {
return fmt.Errorf("couldn't create file (path=%s): %w", healthyFilePath, err)
}
logger.Info("Bot is up and running")
waitForSignal(c.Context)
return nil

View File

@ -126,7 +126,7 @@ func (t *TWHelpHTTP) GetExistingTribeByTag(ctx context.Context, versionCode, ser
return t.convertTribeToDomain(tribes[0]), nil
}
func (t *TWHelpHTTP) ListTribesTag(
func (t *TWHelpHTTP) ListTribesByTag(
ctx context.Context,
versionCode, serverKey string,
tribeTags []string,
@ -153,12 +153,16 @@ func (t *TWHelpHTTP) ListTribesTag(
return res, nil
}
func (t *TWHelpHTTP) ListVillagesCoords(
func (t *TWHelpHTTP) ListVillagesByCoords(
ctx context.Context,
versionCode, serverKey string,
coords []string,
offset, limit int32,
) ([]domain.Village, error) {
if len(coords) == 0 {
return nil, nil
}
villages, err := t.client.ListVillages(ctx, versionCode, serverKey, twhelp.ListVillagesQueryParams{
Limit: limit,
Offset: offset,
@ -247,6 +251,8 @@ func (t *TWHelpHTTP) convertVillageToDomain(v twhelp.Village) domain.Village {
FullName: v.FullName,
ProfileURL: v.ProfileURL,
Points: v.Points,
X: v.X,
Y: v.Y,
Player: t.convertNullPlayerMetaToDomain(v.Player),
}
}

View File

@ -29,6 +29,10 @@ type GroupService interface {
Delete(ctx context.Context, id, serverID string) error
}
type CoordsService interface {
Translate(ctx context.Context, params domain.TranslateCoordsParams) (domain.TranslateCoordsResult, error)
}
type ChoiceService interface {
Versions(ctx context.Context) ([]domain.Choice, error)
}
@ -37,7 +41,8 @@ type Bot struct {
session *discordgo.Session
cron *cron.Cron
groupSvc GroupService
choiceSvc ChoiceService
choiceSvc *choiceService
coordsSvc CoordsService
logger *zap.Logger
localizer *discordi18n.Localizer
}
@ -45,7 +50,8 @@ type Bot struct {
func NewBot(
token string,
groupSvc GroupService,
client ChoiceService,
choiceSvc ChoiceService,
coordsSvc CoordsService,
logger *zap.Logger,
) (*Bot, error) {
localizer, err := discordi18n.NewLocalizer(language.English)
@ -68,7 +74,8 @@ func NewBot(
),
),
groupSvc: groupSvc,
choiceSvc: client,
choiceSvc: &choiceService{choiceSvc: choiceSvc, localizer: localizer},
coordsSvc: coordsSvc,
logger: logger,
localizer: localizer,
}
@ -106,6 +113,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, coordsSvc: b.coordsSvc, choiceSvc: b.choiceSvc},
}
for _, cmd := range commands {
@ -143,6 +151,7 @@ func (b *Bot) registerCronJobs() error {
func (b *Bot) handleSessionReady(s *discordgo.Session, _ *discordgo.Ready) {
_ = s.UpdateGameStatus(0, "Tribal Wars")
b.logger.Info("Bot is up and running")
}
func (b *Bot) logCommands(_ *discordgo.Session, i *discordgo.InteractionCreate) {

View File

@ -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
}

View File

@ -0,0 +1,236 @@
package discord
import (
"context"
"encoding/json"
"fmt"
"strings"
"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
coordsSvc CoordsService
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.CoordsStringMaxLength,
},
},
})
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)
}
}
const coordsTranslationVillagesPerPage = 7
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.coordsSvc.Translate(ctx, params)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
var builderDescription strings.Builder
for i, v := range res.Villages {
if i > 0 {
builderDescription.WriteString("\n")
}
builderDescription.WriteString(v.FullName)
}
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))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Type: discordgo.EmbedTypeRich,
Title: title,
Description: builderDescription.String(),
Timestamp: formatTimestamp(time.Now()),
Footer: &discordgo.MessageEmbedFooter{
Text: fmt.Sprintf("Page %d/%d", res.Page, res.MaxPage),
},
},
},
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.Button{
Style: discordgo.PrimaryButton,
CustomID: "coords-" + res.PrevPageID.String(),
Label: "Previous page",
Disabled: res.PrevPageID.IsZero(),
},
discordgo.Button{
Style: discordgo.PrimaryButton,
CustomID: "coords-" + res.NextPageID.String(),
Label: "Next page",
Disabled: res.NextPageID.IsZero(),
},
},
},
},
},
})
}
func (c *coordsCommand) optionsToTranslateCoordsParams(
options []*discordgo.ApplicationCommandInteractionDataOption,
) (domain.TranslateCoordsParams, 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.NewTranslateCoordsParams(version, server, coordsStr, coordsTranslationVillagesPerPage)
}
func (c *coordsCommand) handleMessageComponent(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
cmpData := i.MessageComponentData()
b, _ := json.MarshalIndent(i, "", "\t")
fmt.Println(cmpData, string(b))
if cmpData.ComponentType != discordgo.ButtonComponent {
return
}
}

View File

@ -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 {
@ -59,15 +58,15 @@ func (c *groupCommand) create(s *discordgo.Session) error {
subcommands = append(subcommands, cmd)
}
groupCmdDescriptionDefault, groupCmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.description")
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.description")
if err != nil {
return err
}
_, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{
Name: c.name(),
Description: groupCmdDescriptionDefault,
DescriptionLocalizations: &groupCmdDescriptionLocalizations,
Description: cmdDescriptionDefault,
DescriptionLocalizations: &cmdDescriptionLocalizations,
DefaultMemberPermissions: &perm,
DMPermission: &dm,
Options: subcommands,
@ -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
}
@ -658,6 +623,10 @@ func (c *groupCommand) buildSubcommandDelete() (*discordgo.ApplicationCommandOpt
}
func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type != discordgo.InteractionApplicationCommand {
return
}
cmdData := i.ApplicationCommandData()
if cmdData.Name != c.name() {
@ -718,7 +687,13 @@ func (c *groupCommand) handleCreate(ctx context.Context, s *discordgo.Session, i
},
})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -742,6 +717,7 @@ func (c *groupCommand) optionsToCreateGroupParams(
internals := false
barbarians := false
languageTag := ""
for _, opt := range options {
switch opt.Name {
case "version":
@ -760,6 +736,7 @@ func (c *groupCommand) optionsToCreateGroupParams(
barbarians = opt.BoolValue()
}
}
return domain.NewCreateGroupParams(
guildID,
version,
@ -804,7 +781,13 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
MessageID: langMessageID,
})
if localizeErr != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", langMessageID), zap.Error(localizeErr))
c.logger.Error("no message with the specified id", zap.String("id", langMessageID), zap.Error(localizeErr))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, localizeErr),
},
})
return
}
@ -821,7 +804,13 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
},
})
if localizeErr != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", fieldValueMessageID), zap.Error(localizeErr))
c.logger.Error("no message with the specified id", zap.String("id", fieldValueMessageID), zap.Error(localizeErr))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, localizeErr),
},
})
return
}
@ -835,7 +824,13 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
titleMessageID := "cmd.group.list.embed.title"
title, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: titleMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", titleMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", titleMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -893,7 +888,13 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
MessageID: langMessageID,
})
if localizeErr != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", langMessageID), zap.Error(localizeErr))
c.logger.Error("no message with the specified id", zap.String("id", langMessageID), zap.Error(localizeErr))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, localizeErr),
},
})
return
}
@ -911,7 +912,13 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
},
})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", descriptionMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", descriptionMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -978,7 +985,13 @@ func (c *groupCommand) handleSetLanguage(ctx context.Context, s *discordgo.Sessi
successMessageID := "cmd.group.set.language.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1021,7 +1034,13 @@ func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.S
successMessageID := "cmd.group.set.channel-gains.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1064,7 +1083,13 @@ func (c *groupCommand) handleSetChannelLosses(ctx context.Context, s *discordgo.
successMessageID := "cmd.group.set.channel-losses.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1107,7 +1132,13 @@ func (c *groupCommand) handleSetInternals(ctx context.Context, s *discordgo.Sess
successMessageID := "cmd.group.set.internals.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1150,7 +1181,13 @@ func (c *groupCommand) handleSetBarbarians(ctx context.Context, s *discordgo.Ses
successMessageID := "cmd.group.set.barbarians.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1205,7 +1242,13 @@ func (c *groupCommand) handleTribeAdd(ctx context.Context, s *discordgo.Session,
successMessageID := "cmd.group.tribe.add.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1248,7 +1291,13 @@ func (c *groupCommand) handleTribeRemove(ctx context.Context, s *discordgo.Sessi
successMessageID := "cmd.group.tribe.remove.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
@ -1277,7 +1326,13 @@ func (c *groupCommand) handleDelete(ctx context.Context, s *discordgo.Session, i
successMessageID := "cmd.group.delete.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
c.logger.Error("no message with the specified id", zap.String("id", successMessageID), zap.Error(err))
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}

View File

@ -14,6 +14,8 @@
"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.coords-string-too-long": "The given string is too long ({{ len .Str }}/{{ .Max }}).",
"err.no-coords-found-in-string": "No coords found in the given string.",
"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 +79,11 @@
"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"
}

View File

@ -14,6 +14,8 @@
"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.coords-string-too-long": "Podany ciąg znaków jest zbyt długi ({{ len .Str }}/{{ .Max }}).",
"err.no-coords-found-in-string": "Nie znaleziono współrzędnych w podanym ciągu znaków.",
"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 +79,11 @@
"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"
}

122
internal/domain/coords.go Normal file
View File

@ -0,0 +1,122 @@
package domain
import (
"fmt"
"math"
"regexp"
)
type CoordsStringTooLongError struct {
Str string
Max int
}
var _ TranslatableError = CoordsStringTooLongError{}
func (e CoordsStringTooLongError) Error() string {
return fmt.Sprintf("coords string is too long (%d/%d)", len(e.Str), e.Max)
}
func (e CoordsStringTooLongError) Slug() string {
return "coords-string-too-long"
}
func (e CoordsStringTooLongError) Params() map[string]any {
return map[string]any{
"Str": e.Str,
"Max": e.Max,
}
}
type NoCoordsFoundInStringError struct {
Str string
}
var _ TranslatableError = NoCoordsFoundInStringError{}
func (e NoCoordsFoundInStringError) Error() string {
return fmt.Sprintf("no coords found in string: '%s'", e.Str)
}
func (e NoCoordsFoundInStringError) Slug() string {
return "no-coords-found-in-string"
}
func (e NoCoordsFoundInStringError) Params() map[string]any {
return map[string]any{
"Str": e.Str,
}
}
type TranslateCoordsParams struct {
versionCode string
serverKey string
coords []string
perPage int32
maxPage int32
}
var coordsRegex = regexp.MustCompile(`(\d+)\|(\d+)`)
const CoordsStringMaxLength = 1000
func NewTranslateCoordsParams(versionCode, serverKey, coordsStr string, perPage int32) (TranslateCoordsParams, error) {
if versionCode == "" {
return TranslateCoordsParams{}, RequiredError{Field: "VersionCode"}
}
if serverKey == "" {
return TranslateCoordsParams{}, RequiredError{Field: "ServerKey"}
}
if len(coordsStr) > CoordsStringMaxLength {
return TranslateCoordsParams{}, CoordsStringTooLongError{
Max: CoordsStringMaxLength,
Str: coordsStr,
}
}
coords := uniq(coordsRegex.FindAllString(coordsStr, -1))
if len(coords) == 0 {
return TranslateCoordsParams{}, NoCoordsFoundInStringError{
Str: coordsStr,
}
}
return TranslateCoordsParams{
versionCode: versionCode,
serverKey: serverKey,
coords: coords,
perPage: perPage,
maxPage: int32(math.Ceil(float64(len(coords)) / float64(perPage))),
}, nil
}
func (c TranslateCoordsParams) VersionCode() string {
return c.versionCode
}
func (c TranslateCoordsParams) ServerKey() string {
return c.serverKey
}
func (c TranslateCoordsParams) Coords() []string {
return c.coords
}
func (c TranslateCoordsParams) PerPage() int32 {
return c.perPage
}
func (c TranslateCoordsParams) MaxPage() int32 {
return c.maxPage
}
type TranslateCoordsResult struct {
Villages []Village
Untranslated []string
Page int32
MaxPage int32
NextPageID PaginationPageID
PrevPageID PaginationPageID
}

View File

@ -0,0 +1,121 @@
package domain_test
import (
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestNewTranslateCoordsParams(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",
},
},
{
versionCode: "pl",
serverKey: "",
coordsStr: "123|123",
err: domain.RequiredError{
Field: "ServerKey",
},
},
{
versionCode: "pl",
serverKey: "pl181",
coordsStr: "no coords",
err: domain.NoCoordsFoundInStringError{
Str: "no coords",
},
},
{
versionCode: "pl",
serverKey: "pl181",
coordsStr: "no coords",
err: domain.NoCoordsFoundInStringError{
Str: "no coords",
},
},
{
versionCode: "pl",
serverKey: "pl181",
coordsStr: tooLongStr,
err: domain.CoordsStringTooLongError{
Str: tooLongStr,
Max: 1000,
},
},
}
for _, tt := range tests {
tt := tt
testName := fmt.Sprintf("versionCode=%s | serverKey=%s | coordsStr=%s", tt.versionCode, tt.serverKey, tt.coordsStr)
if len(testName) > 100 {
testName = testName[:97] + "..."
}
t.Run(testName, func(t *testing.T) {
t.Parallel()
res, err := domain.NewTranslateCoordsParams(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())
})
}
}

View File

@ -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
}

View File

@ -0,0 +1,16 @@
package domain_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestNewPaginationPageID(t *testing.T) {
t.Parallel()
id := domain.NewPaginationPageID(15, []byte("data"))
assert.NotEmpty(t, id.String())
assert.False(t, id.IsZero())
}

View File

@ -0,0 +1,26 @@
package domain
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
type PaginationPageID struct {
s string
}
func NewPaginationPageID(page int32, data []byte) PaginationPageID {
hash := sha256.Sum256(fmt.Append(data, "page", page))
return PaginationPageID{
s: hex.EncodeToString(hash[:]),
}
}
func (p PaginationPageID) String() string {
return p.s
}
func (p PaginationPageID) IsZero() bool {
return len(p.s) == 0
}

View File

@ -55,9 +55,15 @@ type Village struct {
FullName string
ProfileURL string
Points int64
X int64
Y int64
Player NullPlayerMeta
}
func (v Village) Coords() string {
return fmt.Sprintf("%d|%d", v.X, v.Y)
}
type VillageMeta struct {
ID int64
FullName string

View File

@ -14,9 +14,39 @@ func NewCoords(twhelpSvc TWHelpService) *Coords {
return &Coords{twhelpSvc: twhelpSvc}
}
func (c *Coords) Translate(ctx context.Context, version, server string, coords ...string) ([]domain.Village, error) {
if len(coords) == 0 {
return nil, nil
func (c *Coords) Translate(ctx context.Context, params domain.TranslateCoordsParams) (domain.TranslateCoordsResult, error) {
perPage := params.PerPage()
if coordsLen := int32(len(params.Coords())); coordsLen < perPage {
perPage = coordsLen
}
return c.twhelpSvc.ListVillagesCoords(ctx, version, server, coords, 0, int32(len(coords)))
coords := params.Coords()[:perPage]
villages, err := c.twhelpSvc.ListVillagesByCoords(ctx, params.VersionCode(), params.ServerKey(), coords, 0, perPage)
if err != nil {
return domain.TranslateCoordsResult{}, err
}
untranslated := make([]string, 0, perPage)
for _, co := range coords {
found := false
for _, v := range villages {
if co == v.Coords() {
found = true
break
}
}
if !found {
untranslated = append(untranslated, co)
}
}
return domain.TranslateCoordsResult{
Villages: villages,
Page: 1,
MaxPage: params.MaxPage(),
Untranslated: untranslated,
}, nil
}

View File

@ -110,7 +110,7 @@ func (g *Group) RemoveTribe(ctx context.Context, id, serverID, tribeTag string)
return domain.GroupWithMonitors{}, err
}
tribes, err := g.twhelpSvc.ListTribesTag(ctx, groupBeforeUpdate.VersionCode, groupBeforeUpdate.ServerKey, []string{tribeTag}, 0, listTribesLimit)
tribes, err := g.twhelpSvc.ListTribesByTag(ctx, groupBeforeUpdate.VersionCode, groupBeforeUpdate.ServerKey, []string{tribeTag}, 0, listTribesLimit)
if err != nil {
return domain.GroupWithMonitors{}, fmt.Errorf("TWHelpClient.ListTribes: %w", err)
}

View File

@ -266,7 +266,7 @@ func TestGroup_RemoveTribe(t *testing.T) {
ID: group.Monitors[0].TribeID,
Tag: "*TAG*",
}
twhelpSvc.ListTribesTagReturns([]domain.Tribe{tribe}, nil)
twhelpSvc.ListTribesByTagReturns([]domain.Tribe{tribe}, nil)
g, err := service.
NewGroup(repo, twhelpSvc, zap.NewNop(), 1, 1).

View File

@ -17,7 +17,7 @@ type TWHelpService interface {
GetOpenServer(ctx context.Context, versionCode, serverKey string) (domain.TWServer, error)
GetTribeByID(ctx context.Context, versionCode, serverKey string, id int64) (domain.Tribe, error)
GetExistingTribeByTag(ctx context.Context, versionCode, serverKey, tribeTag string) (domain.Tribe, error)
ListTribesTag(ctx context.Context, versionCode, serverKey string, tribeTags []string, offset, limit int32) ([]domain.Tribe, error)
ListVillagesCoords(ctx context.Context, versionCode, serverKey string, coords []string, offset, limit int32) ([]domain.Village, error)
ListTribesByTag(ctx context.Context, versionCode, serverKey string, tribeTags []string, offset, limit int32) ([]domain.Tribe, error)
ListVillagesByCoords(ctx context.Context, versionCode, serverKey string, coords []string, offset, limit int32) ([]domain.Village, error)
ListEnnoblementsSince(ctx context.Context, versionCode, serverKey string, since time.Time, offset, limit int32) ([]domain.Ennoblement, error)
}

View File

@ -5,6 +5,11 @@ import (
"time"
)
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
type ErrorCode string
const (
@ -143,6 +148,20 @@ func (p *NullPlayerMeta) UnmarshalJSON(data []byte) error {
return nil
}
type Village struct {
ID int64 `json:"id"`
FullName string `json:"fullName"`
ProfileURL string `json:"profileUrl"`
Points int64 `json:"points"`
X int64 `json:"x"`
Y int64 `json:"y"`
Player NullPlayerMeta `json:"player"`
}
type listVillagesResp struct {
Data []Village `json:"data"`
}
type VillageMeta struct {
ID int64 `json:"id"`
FullName string `json:"fullName"`
@ -160,20 +179,3 @@ type Ennoblement struct {
type listEnnoblementsResp struct {
Data []Ennoblement `json:"data"`
}
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
type Village struct {
ID int64 `json:"id"`
FullName string `json:"fullName"`
ProfileURL string `json:"profileUrl"`
Points int64 `json:"points"`
Player NullPlayerMeta `json:"player"`
}
type listVillagesResp struct {
Data []Village `json:"data"`
}