dcbot/internal/discord/command_coords.go

378 lines
11 KiB
Go

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
}