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 }