diff --git a/bot/button/handlers/gdprconfirm.go b/bot/button/handlers/gdprconfirm.go index c42d1ebf..9762ae69 100644 --- a/bot/button/handlers/gdprconfirm.go +++ b/bot/button/handlers/gdprconfirm.go @@ -1,9 +1,9 @@ package handlers import ( + "context" "crypto/sha256" "fmt" - "strconv" "strings" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" @@ -11,7 +11,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" "github.com/TicketsBot-cloud/worker/bot/command" cmdcontext "github.com/TicketsBot-cloud/worker/bot/command/context" - "github.com/TicketsBot-cloud/worker/bot/constants" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/gdprrelay" @@ -20,22 +19,18 @@ import ( "github.com/TicketsBot-cloud/worker/i18n" ) -type GDPRConfirmAllTranscriptsHandler struct{} - -func (h *GDPRConfirmAllTranscriptsHandler) Matcher() matcher.Matcher { - return matcher.NewFuncMatcher(func(customId string) bool { - return strings.HasPrefix(customId, "gdpr_confirm_all_transcripts_") - }) -} - -func (h *GDPRConfirmAllTranscriptsHandler) Properties() registry.Properties { - return registry.Properties{ - Flags: registry.SumFlags(registry.DMsAllowed), - Timeout: constants.TimeoutGDPR, - } +// All six confirmations resolve, log, publish and report back identically, differing only in +// these fields. +type gdprConfirmSpec struct { + requestType gdprrelay.RequestType + logName string + colour customisation.Colour + // Carries no selection, so there is nothing to look up. + stateless bool + render func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string } -func (h *GDPRConfirmAllTranscriptsHandler) Execute(ctx *cmdcontext.ButtonContext) { +func executeGDPRConfirm(ctx *cmdcontext.ButtonContext, spec gdprConfirmSpec) { locale := utils.ExtractLanguageFromCustomId(ctx.InteractionData.CustomId) if !gdprrelay.IsWorkerAlive(redis.Client) { @@ -44,28 +39,43 @@ func (h *GDPRConfirmAllTranscriptsHandler) Execute(ctx *cmdcontext.ButtonContext return } - guildIds := utils.ParseGuildIds(ctx.InteractionData.CustomId) - if len(guildIds) == 0 { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) - return + userId := ctx.UserId() + + var guildIds []uint64 + var ticketIds []int + + if !spec.stateless { + // Consuming deletes the token, so a second click cannot queue the request twice. + data, ok := utils.ConsumeGDPRConfirmation(ctx, redis.Client, ctx.InteractionData.CustomId, userId) + if !ok { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorConfirmationExpired)) + return + } + + guildIds = data.GuildIds + ticketIds = data.TicketIds + + if len(guildIds) == 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) + return + } } guildNames := utils.FetchGuildNames(ctx, guildIds) request := gdprrelay.GDPRRequest{ - Type: gdprrelay.RequestTypeAllTranscripts, - UserId: ctx.UserId(), + Type: spec.requestType, + UserId: userId, GuildIds: guildIds, GuildNames: guildNames, + TicketIds: ticketIds, Language: locale.IsoLongCode, InteractionToken: ctx.Interaction.Token, InteractionGuildId: ctx.GuildId(), ApplicationId: ctx.Worker().BotId, } - scrambledId := sha256.New() - fmt.Fprintf(scrambledId, "%d", ctx.UserId()) - id, err := dbclient.Client.GdprLogs.InsertLog(fmt.Sprintf("%x", scrambledId.Sum(nil)), "AllTranscripts", "Queued") + id, err := insertGDPRLog(userId, spec.logName) if err != nil { ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) return @@ -76,283 +86,202 @@ func (h *GDPRConfirmAllTranscriptsHandler) Execute(ctx *cmdcontext.ButtonContext return } - guildDisplays := make([]string, len(guildIds)) - for i, guildId := range guildIds { - guildDisplays[i] = utils.FormatGuildDisplay(guildId, guildNames) - } + content := spec.render(locale, request, guildNames) + content += i18n.GetMessage(locale, i18n.GdprQueuedFooter) - var content string - if len(guildIds) == 1 { - content = i18n.GetMessage(locale, i18n.GdprQueuedAllTranscripts, guildDisplays[0]) - } else { - content = i18n.GetMessage(locale, i18n.GdprQueuedAllTranscriptsMulti, strings.Join(guildDisplays, "\n* ")) + components := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: content, + }), } - content += i18n.GetMessage(locale, i18n.GdprQueuedFooter) - components := []component.Component{component.BuildTextDisplay(component.TextDisplay{Content: content})} title := i18n.GetMessage(locale, i18n.GdprQueuedTitle) - container := utils.BuildContainerWithComponents(ctx, customisation.Orange, title, components) + container := utils.BuildContainerWithComponents(ctx, spec.colour, title, components) ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) } -type GDPRConfirmSpecificTranscriptsHandler struct{} - -func (h *GDPRConfirmSpecificTranscriptsHandler) Matcher() matcher.Matcher { - return matcher.NewFuncMatcher(func(customId string) bool { - return strings.HasPrefix(customId, "gdpr_confirm_specific_") - }) -} +func insertGDPRLog(userId uint64, requestType string) (int, error) { + scrambledId := sha256.New() + fmt.Fprintf(scrambledId, "%d", userId) -func (h *GDPRConfirmSpecificTranscriptsHandler) Properties() registry.Properties { - return registry.Properties{ - Flags: registry.SumFlags(registry.DMsAllowed), - Timeout: constants.TimeoutGDPR, - } + return dbclient.Client.GdprLogs.InsertLog(fmt.Sprintf("%x", scrambledId.Sum(nil)), requestType, "Queued") } -func (h *GDPRConfirmSpecificTranscriptsHandler) Execute(ctx *cmdcontext.ButtonContext) { - locale := utils.ExtractLanguageFromCustomId(ctx.InteractionData.CustomId) - - if !gdprrelay.IsWorkerAlive(redis.Client) { - container := utils.BuildGDPRWorkerOfflineView(ctx, locale) - ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) - return - } - - parts := strings.Split(ctx.InteractionData.CustomId, "_") - if len(parts) < 5 { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) - return - } - - guildId, err := strconv.ParseUint(parts[3], 10, 64) +func storeGDPRConfirmation(ctx context.Context, action string, locale *i18n.Locale, userId uint64, guildIds []uint64, ticketIds []int) (string, error) { + token, err := utils.StoreGDPRConfirmation(ctx, redis.Client, utils.GDPRConfirmation{ + UserId: userId, + GuildIds: guildIds, + TicketIds: ticketIds, + Locale: locale.IsoShortCode, + }) if err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) - return + return "", err } - var ticketIds []int - for i := 4; i < len(parts)-1; i++ { - if ticketId, err := strconv.Atoi(parts[i]); err == nil { - ticketIds = append(ticketIds, ticketId) - } - } - - if len(ticketIds) == 0 { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidTicketIds)) - return - } - - guildNames := utils.FetchGuildNames(ctx, []uint64{guildId}) - - request := gdprrelay.GDPRRequest{ - Type: gdprrelay.RequestTypeSpecificTranscripts, - UserId: ctx.UserId(), - GuildIds: []uint64{guildId}, - GuildNames: guildNames, - TicketIds: ticketIds, - Language: locale.IsoLongCode, - InteractionToken: ctx.Interaction.Token, - InteractionGuildId: ctx.GuildId(), - ApplicationId: ctx.Worker().BotId, - } + return utils.BuildGDPRConfirmId(action, token, locale.IsoShortCode), nil +} - scrambledId := sha256.New() - fmt.Fprintf(scrambledId, "%d", ctx.UserId()) - id, err := dbclient.Client.GdprLogs.InsertLog(fmt.Sprintf("%x", scrambledId.Sum(nil)), "SpecificTranscripts", "Queued") - if err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) - return +func guildDisplays(guildIds []uint64, guildNames map[uint64]string) []string { + displays := make([]string, len(guildIds)) + for i, guildId := range guildIds { + displays[i] = utils.FormatGuildDisplay(guildId, guildNames) } - if err := gdprrelay.Publish(redis.Client, request, id); err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) - return - } + return displays +} - ticketIdStrs := make([]string, len(ticketIds)) +func ticketIdList(ticketIds []int) string { + strs := make([]string, len(ticketIds)) for i, id := range ticketIds { - ticketIdStrs[i] = fmt.Sprintf("%d", id) + strs[i] = fmt.Sprintf("%d", id) } - guildDisplay := utils.FormatGuildDisplay(guildId, guildNames) - content := i18n.GetMessage(locale, i18n.GdprQueuedSpecificTranscripts, guildDisplay, strings.Join(ticketIdStrs, ", ")) - content += i18n.GetMessage(locale, i18n.GdprQueuedFooter) - - components := []component.Component{component.BuildTextDisplay(component.TextDisplay{Content: content})} - title := i18n.GetMessage(locale, i18n.GdprQueuedTitle) - container := utils.BuildContainerWithComponents(ctx, customisation.Orange, title, components) - ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) + return strings.Join(strs, ", ") } -type GDPRConfirmAllMessagesHandler struct{} - -func (h *GDPRConfirmAllMessagesHandler) Matcher() matcher.Matcher { +func gdprConfirmMatcher(prefix string) matcher.Matcher { return matcher.NewFuncMatcher(func(customId string) bool { - return strings.HasPrefix(customId, "gdpr_confirm_all_messages_") + return strings.HasPrefix(customId, prefix) }) } -func (h *GDPRConfirmAllMessagesHandler) Properties() registry.Properties { - return registry.Properties{ - Flags: registry.SumFlags(registry.DMsAllowed), - Timeout: constants.TimeoutGDPR, - } -} +type GDPRConfirmAllTranscriptsHandler struct{} -func (h *GDPRConfirmAllMessagesHandler) Execute(ctx *cmdcontext.ButtonContext) { - locale := utils.ExtractLanguageFromCustomId(ctx.InteractionData.CustomId) +func (h *GDPRConfirmAllTranscriptsHandler) Matcher() matcher.Matcher { + return gdprConfirmMatcher("gdpr_confirm_all_transcripts_") +} - if !gdprrelay.IsWorkerAlive(redis.Client) { - container := utils.BuildGDPRWorkerOfflineView(ctx, locale) - ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) - return - } +func (h *GDPRConfirmAllTranscriptsHandler) Properties() registry.Properties { + return gdprProperties() +} - userId := ctx.UserId() - guildIds := utils.ParseGuildIds(ctx.InteractionData.CustomId) +func (h *GDPRConfirmAllTranscriptsHandler) Execute(ctx *cmdcontext.ButtonContext) { + executeGDPRConfirm(ctx, gdprConfirmSpec{ + requestType: gdprrelay.RequestTypeAllTranscripts, + logName: "AllTranscripts", + colour: customisation.Orange, + render: func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string { + displays := guildDisplays(request.GuildIds, guildNames) + if len(displays) == 1 { + return i18n.GetMessage(locale, i18n.GdprQueuedAllTranscripts, displays[0]) + } + return i18n.GetMessage(locale, i18n.GdprQueuedAllTranscriptsMulti, strings.Join(displays, "\n* ")) + }, + }) +} - guildNames := utils.FetchGuildNames(ctx, guildIds) +type GDPRConfirmSpecificTranscriptsHandler struct{} - request := gdprrelay.GDPRRequest{ - Type: gdprrelay.RequestTypeAllMessages, - UserId: userId, - GuildIds: guildIds, - GuildNames: guildNames, - Language: locale.IsoLongCode, - InteractionToken: ctx.Interaction.Token, - InteractionGuildId: ctx.GuildId(), - ApplicationId: ctx.Worker().BotId, - } +func (h *GDPRConfirmSpecificTranscriptsHandler) Matcher() matcher.Matcher { + return gdprConfirmMatcher("gdpr_confirm_specific_") +} - scrambledId := sha256.New() - fmt.Fprintf(scrambledId, "%d", ctx.UserId()) - id, err := dbclient.Client.GdprLogs.InsertLog(fmt.Sprintf("%x", scrambledId.Sum(nil)), "AllMessages", "Queued") - if err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) - return - } +func (h *GDPRConfirmSpecificTranscriptsHandler) Properties() registry.Properties { + return gdprProperties() +} - if err := gdprrelay.Publish(redis.Client, request, id); err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) - return - } +func (h *GDPRConfirmSpecificTranscriptsHandler) Execute(ctx *cmdcontext.ButtonContext) { + executeGDPRConfirm(ctx, gdprConfirmSpec{ + requestType: gdprrelay.RequestTypeSpecificTranscripts, + logName: "SpecificTranscripts", + colour: customisation.Orange, + render: func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string { + guildDisplay := utils.FormatGuildDisplay(request.GuildIds[0], guildNames) + return i18n.GetMessage(locale, i18n.GdprQueuedSpecificTranscripts, guildDisplay, ticketIdList(request.TicketIds)) + }, + }) +} - guildDisplays := make([]string, len(guildIds)) - for i, guildId := range guildIds { - guildDisplays[i] = utils.FormatGuildDisplay(guildId, guildNames) - } +type GDPRConfirmAllMessagesHandler struct{} - var content string - if len(guildIds) == 1 { - content = i18n.GetMessage(locale, i18n.GdprQueuedAllMessages, guildDisplays[0]) - } else { - content = i18n.GetMessage(locale, i18n.GdprQueuedAllMessagesMulti, strings.Join(guildDisplays, "\n* ")) - } - content += i18n.GetMessage(locale, i18n.GdprQueuedFooter) +func (h *GDPRConfirmAllMessagesHandler) Matcher() matcher.Matcher { + return gdprConfirmMatcher("gdpr_confirm_all_messages_") +} - components := []component.Component{ - component.BuildTextDisplay(component.TextDisplay{ - Content: content, - }), - } +func (h *GDPRConfirmAllMessagesHandler) Properties() registry.Properties { + return gdprProperties() +} - title := i18n.GetMessage(locale, i18n.GdprQueuedTitle) - container := utils.BuildContainerWithComponents(ctx, customisation.Orange, title, components) - response := command.NewMessageResponseWithComponents([]component.Component{container}) - ctx.Edit(response) +func (h *GDPRConfirmAllMessagesHandler) Execute(ctx *cmdcontext.ButtonContext) { + executeGDPRConfirm(ctx, gdprConfirmSpec{ + requestType: gdprrelay.RequestTypeAllMessages, + logName: "AllMessages", + colour: customisation.Orange, + render: func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string { + displays := guildDisplays(request.GuildIds, guildNames) + if len(displays) == 1 { + return i18n.GetMessage(locale, i18n.GdprQueuedAllMessages, displays[0]) + } + return i18n.GetMessage(locale, i18n.GdprQueuedAllMessagesMulti, strings.Join(displays, "\n* ")) + }, + }) } type GDPRConfirmMessagesHandler struct{} func (h *GDPRConfirmMessagesHandler) Matcher() matcher.Matcher { - return matcher.NewFuncMatcher(func(customId string) bool { - return strings.HasPrefix(customId, "gdpr_confirm_messages_") - }) + return gdprConfirmMatcher("gdpr_confirm_messages_") } func (h *GDPRConfirmMessagesHandler) Properties() registry.Properties { - return registry.Properties{ - Flags: registry.SumFlags(registry.DMsAllowed), - Timeout: constants.TimeoutGDPR, - } + return gdprProperties() } func (h *GDPRConfirmMessagesHandler) Execute(ctx *cmdcontext.ButtonContext) { - locale := utils.ExtractLanguageFromCustomId(ctx.InteractionData.CustomId) - - if !gdprrelay.IsWorkerAlive(redis.Client) { - container := utils.BuildGDPRWorkerOfflineView(ctx, locale) - ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) - return - } + executeGDPRConfirm(ctx, gdprConfirmSpec{ + requestType: gdprrelay.RequestTypeSpecificMessages, + logName: "SpecificMessages", + colour: customisation.Orange, + render: func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string { + guildDisplay := utils.FormatGuildDisplay(request.GuildIds[0], guildNames) + return i18n.GetMessage(locale, i18n.GdprQueuedSpecificMessages, guildDisplay, ticketIdList(request.TicketIds)) + }, + }) +} - userId := ctx.UserId() +type GDPRConfirmExportGuildHandler struct{} - parts := strings.Split(ctx.InteractionData.CustomId, "_") - if len(parts) < 4 { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) - return - } +func (h *GDPRConfirmExportGuildHandler) Matcher() matcher.Matcher { + return gdprConfirmMatcher("gdpr_confirm_export_guild_") +} - guildId, err := strconv.ParseUint(parts[3], 10, 64) - if err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) - return - } +func (h *GDPRConfirmExportGuildHandler) Properties() registry.Properties { + return gdprProperties() +} - var ticketIds []int - if len(parts) > 4 { - for i := 4; i < len(parts)-1; i++ { - if id, err := strconv.Atoi(parts[i]); err == nil && id > 0 { - ticketIds = append(ticketIds, id) +func (h *GDPRConfirmExportGuildHandler) Execute(ctx *cmdcontext.ButtonContext) { + executeGDPRConfirm(ctx, gdprConfirmSpec{ + requestType: gdprrelay.RequestTypeExportGuild, + logName: "ExportGuild", + colour: customisation.Orange, + render: func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string { + displays := guildDisplays(request.GuildIds, guildNames) + if len(displays) == 1 { + return i18n.GetMessage(locale, i18n.GdprQueuedExportGuild, displays[0]) } - } - } - - guildNames := utils.FetchGuildNames(ctx, []uint64{guildId}) - - request := gdprrelay.GDPRRequest{ - Type: gdprrelay.RequestTypeSpecificMessages, - UserId: userId, - GuildIds: []uint64{guildId}, - GuildNames: guildNames, - TicketIds: ticketIds, - Language: locale.IsoLongCode, - InteractionToken: ctx.Interaction.Token, - InteractionGuildId: ctx.GuildId(), - ApplicationId: ctx.Worker().BotId, - } - - scrambledId := sha256.New() - fmt.Fprintf(scrambledId, "%d", ctx.UserId()) - id, err := dbclient.Client.GdprLogs.InsertLog(fmt.Sprintf("%x", scrambledId.Sum(nil)), "SpecificMessages", "Queued") - if err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) - return - } - - if err := gdprrelay.Publish(redis.Client, request, id); err != nil { - ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorQueueFailed)) - return - } + return i18n.GetMessage(locale, i18n.GdprQueuedExportGuildMulti, strings.Join(displays, "\n* ")) + }, + }) +} - ticketIdStrs := make([]string, len(ticketIds)) - for i, id := range ticketIds { - ticketIdStrs[i] = fmt.Sprintf("%d", id) - } +type GDPRConfirmExportUserHandler struct{} - guildDisplay := utils.FormatGuildDisplay(guildId, guildNames) - content := i18n.GetMessage(locale, i18n.GdprQueuedSpecificMessages, guildDisplay, strings.Join(ticketIdStrs, ", ")) - content += i18n.GetMessage(locale, i18n.GdprQueuedFooter) +func (h *GDPRConfirmExportUserHandler) Matcher() matcher.Matcher { + return gdprConfirmMatcher("gdpr_confirm_export_user_") +} - components := []component.Component{ - component.BuildTextDisplay(component.TextDisplay{ - Content: content, - }), - } +func (h *GDPRConfirmExportUserHandler) Properties() registry.Properties { + return gdprProperties() +} - title := i18n.GetMessage(locale, i18n.GdprQueuedTitle) - container := utils.BuildContainerWithComponents(ctx, customisation.Orange, title, components) - ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) +func (h *GDPRConfirmExportUserHandler) Execute(ctx *cmdcontext.ButtonContext) { + executeGDPRConfirm(ctx, gdprConfirmSpec{ + requestType: gdprrelay.RequestTypeExportUser, + logName: "ExportUser", + colour: customisation.Orange, + stateless: true, + render: func(locale *i18n.Locale, request gdprrelay.GDPRRequest, guildNames map[uint64]string) string { + return i18n.GetMessage(locale, i18n.GdprQueuedExportUser) + }, + }) } diff --git a/bot/button/handlers/gdprconfirmview.go b/bot/button/handlers/gdprconfirmview.go index cabe7d8d..1ddaa672 100644 --- a/bot/button/handlers/gdprconfirmview.go +++ b/bot/button/handlers/gdprconfirmview.go @@ -1,7 +1,6 @@ package handlers import ( - "fmt" "strings" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" @@ -19,6 +18,8 @@ const ( GDPRSpecificTranscripts GDPRAllMessages GDPRSpecificMessages + GDPRExportGuild + GDPRExportUser ) type GDPRConfirmationData struct { @@ -57,9 +58,33 @@ func buildGDPRConfirmationView(ctx interface{}, locale *i18n.Locale, data GDPRCo case GDPRSpecificMessages: content = i18n.GetMessage(locale, i18n.GdprConfirmSpecificMessages, data.GuildNames[0], data.TicketIdsStr) + + case GDPRExportGuild: + if len(data.GuildIds) == 1 { + content = i18n.GetMessage(locale, i18n.GdprConfirmExportGuild, data.GuildNames[0]) + } else { + serversList := strings.Join(data.GuildNames, "\n* ") + content = i18n.GetMessage(locale, i18n.GdprConfirmExportGuildMulti, serversList) + } + + case GDPRExportUser: + content = i18n.GetMessage(locale, i18n.GdprConfirmExportUser) + } + + isExport := data.RequestType == GDPRExportGuild || data.RequestType == GDPRExportUser + + if !isExport { + content += i18n.GetMessage(locale, i18n.GdprConfirmWarning) } - content += i18n.GetMessage(locale, i18n.GdprConfirmWarning) + buttonStyle := component.ButtonStyleDanger + buttonLabel := i18n.GetMessage(locale, i18n.GdprConfirmButton) + buttonEmoji := utils.BuildEmoji("⚠️") + if isExport { + buttonStyle = component.ButtonStylePrimary + buttonLabel = i18n.GetMessage(locale, i18n.GdprConfirmExportButton) + buttonEmoji = utils.BuildEmoji("📦") + } innerComponents := []component.Component{ component.BuildTextDisplay(component.TextDisplay{ @@ -68,10 +93,10 @@ func buildGDPRConfirmationView(ctx interface{}, locale *i18n.Locale, data GDPRCo component.BuildSeparator(component.Separator{}), component.BuildActionRow( component.BuildButton(component.Button{ - Label: i18n.GetMessage(locale, i18n.GdprConfirmButton), + Label: buttonLabel, CustomId: data.ConfirmButtonId, - Style: component.ButtonStyleDanger, - Emoji: utils.BuildEmoji("⚠️"), + Style: buttonStyle, + Emoji: buttonEmoji, }), ), } @@ -89,14 +114,3 @@ func buildGDPRConfirmationView(ctx interface{}, locale *i18n.Locale, data GDPRCo return []component.Component{container} } - -func buildAllMessagesConfirmationComponents(ctx *cmdcontext.ButtonContext, locale *i18n.Locale, userId uint64) []component.Component { - data := GDPRConfirmationData{ - RequestType: GDPRAllMessages, - UserId: userId, - Locale: locale, - ConfirmButtonId: fmt.Sprintf("gdpr_confirm_all_messages_%s", locale.IsoShortCode), - } - - return buildGDPRConfirmationView(ctx, locale, data) -} diff --git a/bot/button/handlers/gdprexportbutton.go b/bot/button/handlers/gdprexportbutton.go new file mode 100644 index 00000000..599ce054 --- /dev/null +++ b/bot/button/handlers/gdprexportbutton.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "fmt" + "strings" + + "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" + "github.com/TicketsBot-cloud/worker/bot/button" + "github.com/TicketsBot-cloud/worker/bot/button/registry" + "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" + "github.com/TicketsBot-cloud/worker/bot/command" + cmdcontext "github.com/TicketsBot-cloud/worker/bot/command/context" + "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/gdprrelay" + "github.com/TicketsBot-cloud/worker/bot/redis" + "github.com/TicketsBot-cloud/worker/bot/utils" + "github.com/TicketsBot-cloud/worker/i18n" +) + +type GDPRExportGuildHandler struct{} + +func (h *GDPRExportGuildHandler) Matcher() matcher.Matcher { + return matcher.NewFuncMatcher(func(customId string) bool { + return strings.HasPrefix(customId, "gdpr_export_guild_") + }) +} + +func (h *GDPRExportGuildHandler) Properties() registry.Properties { + return gdprProperties() +} + +func (h *GDPRExportGuildHandler) Execute(ctx *cmdcontext.ButtonContext) { + locale := utils.ExtractLanguageFromCustomId(ctx.InteractionData.CustomId) + + if !gdprrelay.IsWorkerAlive(redis.Client) { + container := utils.BuildGDPRWorkerOfflineView(ctx, locale) + ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) + return + } + + guilds, err := getOwnedGuildsWithTranscripts(ctx, ctx.UserId()) + if err != nil { + ctx.HandleError(err) + return + } + + if len(guilds) == 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorNoServers)) + return + } + + var modal interaction.ModalResponseData + if len(guilds) > 25 { + modal = buildExportGuildTextModal(locale) + } else { + modal = buildExportGuildModal(locale, guilds) + } + + ctx.Modal(button.ResponseModal{Data: modal}) +} + +func buildExportGuildModal(locale *i18n.Locale, guilds []guildInfo) interaction.ModalResponseData { + options := buildGuildSelectOptions(guilds) + minVal, maxVal := 1, len(options) + if maxVal > 25 { + maxVal = 25 + } + + return interaction.ModalResponseData{ + CustomId: fmt.Sprintf("gdpr_modal_export_guild_%s", locale.IsoShortCode), + Title: i18n.GetMessage(locale, i18n.GdprModalExportGuildTitle), + Components: []component.Component{ + component.BuildLabel(component.Label{ + Label: i18n.GetMessage(locale, i18n.GdprModalSelectServers), + Component: component.BuildSelectMenu(component.SelectMenu{ + CustomId: "server_ids", + Placeholder: i18n.GetMessage(locale, i18n.GdprModalSelectServers), + MinValues: &minVal, + MaxValues: &maxVal, + Options: options, + }), + }), + }, + } +} + +func buildExportGuildTextModal(locale *i18n.Locale) interaction.ModalResponseData { + return interaction.ModalResponseData{ + CustomId: fmt.Sprintf("gdpr_modal_export_guild_text_%s", locale.IsoShortCode), + Title: i18n.GetMessage(locale, i18n.GdprModalExportGuildTitle), + Components: []component.Component{ + component.BuildLabel(component.Label{ + Label: i18n.GetMessage(locale, i18n.GdprModalServerIdsLabel), + Component: component.BuildInputText(component.InputText{ + CustomId: "server_ids", + Style: component.TextStyleParagraph, + Placeholder: utils.Ptr(i18n.GetMessage(locale, i18n.GdprModalServerIdsPlaceholder)), + Required: utils.Ptr(true), + MinLength: utils.Ptr(uint32(17)), + MaxLength: utils.Ptr(uint32(2000)), + }), + }), + }, + } +} + +// Skips guild selection and goes straight to confirmation. +type GDPRExportUserHandler struct{} + +func (h *GDPRExportUserHandler) Matcher() matcher.Matcher { + return matcher.NewFuncMatcher(func(customId string) bool { + return strings.HasPrefix(customId, "gdpr_export_user_") + }) +} + +func (h *GDPRExportUserHandler) Properties() registry.Properties { + return gdprProperties() +} + +func (h *GDPRExportUserHandler) Execute(ctx *cmdcontext.ButtonContext) { + locale := utils.ExtractLanguageFromCustomId(ctx.InteractionData.CustomId) + + if !gdprrelay.IsWorkerAlive(redis.Client) { + container := utils.BuildGDPRWorkerOfflineView(ctx, locale) + ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) + return + } + + data := GDPRConfirmationData{ + RequestType: GDPRExportUser, + UserId: ctx.UserId(), + Locale: locale, + ConfirmButtonId: fmt.Sprintf("gdpr_confirm_export_user_%s", locale.IsoShortCode), + } + + components := buildGDPRConfirmationView(ctx, locale, data) + if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents(components)); err != nil { + ctx.HandleError(err) + } +} diff --git a/bot/button/handlers/gdprguildcheck.go b/bot/button/handlers/gdprguildcheck.go new file mode 100644 index 00000000..8949fc1d --- /dev/null +++ b/bot/button/handlers/gdprguildcheck.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "context" + "fmt" + "strings" + + "github.com/TicketsBot-cloud/gdl/rest" + "github.com/TicketsBot-cloud/worker" +) + +type guildCheck struct { + ValidIds []uint64 + Names []string + Unreachable []string + NotOwner bool +} + +// Deliberately not Worker.GetGuild: that answers from the cache, and a cached row outlives the bot +// being removed from the guild. Any fetch failure counts as unreachable. +func checkOwnedGuilds(ctx context.Context, w *worker.Context, userId uint64, guildIds []uint64) guildCheck { + var out guildCheck + + for _, guildId := range guildIds { + guild, err := rest.GetGuild(ctx, w.Token, w.RateLimiter, guildId) + if err != nil { + out.Unreachable = append(out.Unreachable, formatGuildRef(guildId, "")) + continue + } + + if guild.OwnerId != userId { + out.NotOwner = true + continue + } + + out.Names = append(out.Names, formatGuildRef(guildId, guild.Name)) + out.ValidIds = append(out.ValidIds, guildId) + } + + return out +} + +func (c guildCheck) UnreachableList() string { + return strings.Join(c.Unreachable, "\n* ") +} + +func formatGuildRef(guildId uint64, name string) string { + if name == "" { + return fmt.Sprintf("%d", guildId) + } + + return fmt.Sprintf("%s (ID: %d)", name, guildId) +} diff --git a/bot/button/handlers/gdprmodal.go b/bot/button/handlers/gdprmodal.go index a73d0d8e..16e221bf 100644 --- a/bot/button/handlers/gdprmodal.go +++ b/bot/button/handlers/gdprmodal.go @@ -63,7 +63,7 @@ func getOwnedGuildsWithTranscripts(ctx *cmdcontext.ButtonContext, userId uint64) transcriptQuery := fmt.Sprintf(` SELECT DISTINCT guild_id FROM tickets - WHERE has_transcript = true AND guild_id IN (%s) + WHERE has_transcript = true AND open = false AND guild_id IN (%s) GROUP BY guild_id`, placeholders) rows, err := dbclient.Client.Tickets.Query(ctx, transcriptQuery, params...) @@ -544,25 +544,26 @@ func (h *GDPRModalAllTranscriptsHandler) Execute(ctx *context.ModalContext) { return } - var serverNames []string - var validGuildIds []uint64 - - for _, guildId := range guildIds { - guild, err := ctx.Worker().GetGuild(guildId) - if err != nil || guild.OwnerId != userId { - continue - } + check := checkOwnedGuilds(ctx, ctx.Worker(), userId, guildIds) - serverNames = append(serverNames, fmt.Sprintf("%s (ID: %d)", guild.Name, guildId)) - validGuildIds = append(validGuildIds, guildId) + if len(check.Unreachable) > 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorBotNotInServer, check.UnreachableList())) + return } - if len(validGuildIds) == 0 { + if len(check.ValidIds) == 0 { ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorNotOwner)) return } - guildIdsStr := strings.Trim(strings.ReplaceAll(fmt.Sprint(validGuildIds), " ", ","), "[]") + serverNames := check.Names + validGuildIds := check.ValidIds + + confirmId, err := storeGDPRConfirmation(ctx, "all_transcripts", locale, userId, validGuildIds, nil) + if err != nil { + ctx.HandleError(err) + return + } data := GDPRConfirmationData{ RequestType: GDPRAllTranscripts, @@ -570,7 +571,7 @@ func (h *GDPRModalAllTranscriptsHandler) Execute(ctx *context.ModalContext) { GuildIds: validGuildIds, GuildNames: serverNames, Locale: locale, - ConfirmButtonId: fmt.Sprintf("gdpr_confirm_all_transcripts_%s_%s", guildIdsStr, locale.IsoShortCode), + ConfirmButtonId: confirmId, } components := buildGDPRConfirmationView(ctx, locale, data) @@ -647,26 +648,33 @@ func (h *GDPRModalSpecificTranscriptsHandler) Execute(ctx *context.ModalContext) return } - guild, err := ctx.Worker().GetGuild(guildId) - if err != nil || guild.OwnerId != userId { + check := checkOwnedGuilds(ctx, ctx.Worker(), userId, []uint64{guildId}) + + if len(check.Unreachable) > 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorBotNotInServer, check.UnreachableList())) + return + } + + if len(check.ValidIds) == 0 { ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorNotOwner)) return } - var ticketIdStrs []string - for _, id := range ticketIdList { - ticketIdStrs = append(ticketIdStrs, strconv.Itoa(id)) + confirmId, err := storeGDPRConfirmation(ctx, "specific", locale, userId, []uint64{guildId}, ticketIdList) + if err != nil { + ctx.HandleError(err) + return } data := GDPRConfirmationData{ RequestType: GDPRSpecificTranscripts, UserId: userId, GuildIds: []uint64{guildId}, - GuildNames: []string{fmt.Sprintf("%s (ID: %d)", guild.Name, guildId)}, + GuildNames: check.Names, TicketIds: ticketIdList, TicketIdsStr: ticketIds, Locale: locale, - ConfirmButtonId: fmt.Sprintf("gdpr_confirm_specific_%d_%s_%s", guildId, strings.Join(ticketIdStrs, "_"), locale.IsoShortCode), + ConfirmButtonId: confirmId, } components := buildGDPRConfirmationView(ctx, locale, data) @@ -757,7 +765,11 @@ func (h *GDPRModalAllMessagesHandler) Execute(ctx *context.ModalContext) { return } - guildIdsStr := strings.Trim(strings.ReplaceAll(fmt.Sprint(validGuildIds), " ", ","), "[]") + confirmId, err := storeGDPRConfirmation(ctx, "all_messages", locale, userId, validGuildIds, nil) + if err != nil { + ctx.HandleError(err) + return + } data := GDPRConfirmationData{ RequestType: GDPRAllMessages, @@ -765,7 +777,7 @@ func (h *GDPRModalAllMessagesHandler) Execute(ctx *context.ModalContext) { GuildIds: validGuildIds, GuildNames: serverNames, Locale: locale, - ConfirmButtonId: fmt.Sprintf("gdpr_confirm_all_messages_%s_%s", guildIdsStr, locale.IsoShortCode), + ConfirmButtonId: confirmId, } components := buildGDPRConfirmationView(ctx, locale, data) @@ -848,8 +860,11 @@ func (h *GDPRModalSpecificMessagesHandler) Execute(ctx *context.ModalContext) { return } - ticketIdsEncoded := strings.ReplaceAll(ticketIds, ",", "_") - ticketIdsEncoded = strings.ReplaceAll(ticketIdsEncoded, " ", "") + confirmId, err := storeGDPRConfirmation(ctx, "messages", locale, userId, []uint64{guildId}, ticketIdList) + if err != nil { + ctx.HandleError(err) + return + } data := GDPRConfirmationData{ RequestType: GDPRSpecificMessages, @@ -859,7 +874,7 @@ func (h *GDPRModalSpecificMessagesHandler) Execute(ctx *context.ModalContext) { TicketIds: ticketIdList, TicketIdsStr: ticketIds, Locale: locale, - ConfirmButtonId: fmt.Sprintf("gdpr_confirm_messages_%d_%s_%s", guildId, ticketIdsEncoded, locale.IsoShortCode), + ConfirmButtonId: confirmId, } components := buildGDPRConfirmationView(ctx, locale, data) @@ -867,3 +882,103 @@ func (h *GDPRModalSpecificMessagesHandler) Execute(ctx *context.ModalContext) { ctx.HandleError(err) } } + +type GDPRModalExportGuildHandler struct{} + +func (h *GDPRModalExportGuildHandler) Matcher() matcher.Matcher { + return matcher.NewFuncMatcher(func(customId string) bool { + return strings.HasPrefix(customId, "gdpr_modal_export_guild_") + }) +} + +func (h *GDPRModalExportGuildHandler) Properties() registry.Properties { + return registry.Properties{ + Flags: registry.SumFlags(registry.DMsAllowed), + Timeout: constants.TimeoutGDPR, + } +} + +func (h *GDPRModalExportGuildHandler) Execute(ctx *context.ModalContext) { + locale := utils.ExtractLanguageFromCustomId(ctx.Interaction.Data.CustomId) + userId := ctx.UserId() + + if !gdprrelay.IsWorkerAlive(redis.Client) { + container := utils.BuildGDPRWorkerOfflineView(ctx, locale) + ctx.Edit(command.NewMessageResponseWithComponents([]component.Component{container})) + return + } + + var guildIds []uint64 + + for _, actionRow := range ctx.Interaction.Data.Components { + if actionRow.Component != nil { + switch actionRow.Component.CustomId { + case "server_ids": + if actionRow.Component.Values != nil { + for _, val := range actionRow.Component.Values { + if id, err := strconv.ParseUint(val, 10, 64); err == nil { + guildIds = append(guildIds, id) + } + } + } else if actionRow.Component.Value != "" { + guildIds = utils.ParseGuildIdsFromInput(actionRow.Component.Value) + } + } + } else { + for _, comp := range actionRow.Components { + switch comp.CustomId { + case "server_ids": + if comp.Values != nil { + for _, val := range comp.Values { + if id, err := strconv.ParseUint(val, 10, 64); err == nil { + guildIds = append(guildIds, id) + } + } + } else if comp.Value != "" { + guildIds = utils.ParseGuildIdsFromInput(comp.Value) + } + } + } + } + } + + if len(guildIds) == 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorInvalidServerId)) + return + } + + check := checkOwnedGuilds(ctx, ctx.Worker(), userId, guildIds) + + if len(check.Unreachable) > 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorBotNotInServer, check.UnreachableList())) + return + } + + if len(check.ValidIds) == 0 { + ctx.ReplyRaw(customisation.Red, "Error", i18n.GetMessage(locale, i18n.GdprErrorNotOwner)) + return + } + + serverNames := check.Names + validGuildIds := check.ValidIds + + confirmId, err := storeGDPRConfirmation(ctx, "export_guild", locale, userId, validGuildIds, nil) + if err != nil { + ctx.HandleError(err) + return + } + + exportGuildData := GDPRConfirmationData{ + RequestType: GDPRExportGuild, + UserId: userId, + GuildIds: validGuildIds, + GuildNames: serverNames, + Locale: locale, + ConfirmButtonId: confirmId, + } + + exportComponents := buildGDPRConfirmationView(ctx, locale, exportGuildData) + if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents(exportComponents)); err != nil { + ctx.HandleError(err) + } +} diff --git a/bot/button/manager/manager.go b/bot/button/manager/manager.go index 9b0edbdd..f6e1bc8a 100644 --- a/bot/button/manager/manager.go +++ b/bot/button/manager/manager.go @@ -71,6 +71,10 @@ func (m *ComponentInteractionManager) RegisterCommands() { new(handlers.GDPRConfirmSpecificTranscriptsHandler), new(handlers.GDPRConfirmAllMessagesHandler), new(handlers.GDPRConfirmMessagesHandler), + new(handlers.GDPRExportGuildHandler), + new(handlers.GDPRExportUserHandler), + new(handlers.GDPRConfirmExportGuildHandler), + new(handlers.GDPRConfirmExportUserHandler), new(handlers.JoinThreadHandler), new(handlers.KBNavigationHandler), new(handlers.KBCreateTicketHandler), @@ -111,6 +115,7 @@ func (m *ComponentInteractionManager) RegisterCommands() { new(handlers.GDPRModalSpecificTranscriptsHandler), new(handlers.GDPRModalAllMessagesHandler), new(handlers.GDPRModalSpecificMessagesHandler), + new(handlers.GDPRModalExportGuildHandler), new(handlers.PremiumKeySubmitHandler), new(edit.LabelChangeSubmitHandler), new(modals.AdminDebugServerPanelSettingsModalHandler), diff --git a/bot/command/impl/general/gdpr.go b/bot/command/impl/general/gdpr.go index d82a1d98..6e9434d1 100644 --- a/bot/command/impl/general/gdpr.go +++ b/bot/command/impl/general/gdpr.go @@ -31,6 +31,11 @@ var ( {i18n.GdprButtonAllMessages, "gdpr_all_messages"}, {i18n.GdprButtonSpecificMessages, "gdpr_specific_messages"}, } + + exportButtons = []gdprButton{ + {i18n.GdprButtonExportGuild, "gdpr_export_guild"}, + {i18n.GdprButtonExportUser, "gdpr_export_user"}, + } ) type GDPRCommand struct{} @@ -86,6 +91,9 @@ func buildGDPRComponents(ctx registry.CommandContext, locale *i18n.Locale) []com buildTextSection(i18n.GetMessage(locale, i18n.GdprMessageSectionTitle)), buildButtonRow(ctx, locale, messageButtons), component.BuildSeparator(component.Separator{}), + buildTextSection(i18n.GetMessage(locale, i18n.GdprExportSectionTitle)), + buildButtonRow(ctx, locale, exportButtons), + component.BuildSeparator(component.Separator{}), buildTextSection(i18n.GetMessage(locale, i18n.GdprWarningText)), component.BuildSeparator(component.Separator{}), buildTextSection(i18n.GetMessage(locale, i18n.GdprResources, "https://gdpr.eu/what-is-gdpr/", "https://gdpr-info.eu/art-17-gdpr/", "https://gdpr-info.eu/art-15-gdpr/")), diff --git a/bot/gdprrelay/gdprrelay.go b/bot/gdprrelay/gdprrelay.go index b38b0d85..3106c0f9 100644 --- a/bot/gdprrelay/gdprrelay.go +++ b/bot/gdprrelay/gdprrelay.go @@ -16,6 +16,8 @@ const ( RequestTypeSpecificTranscripts RequestTypeAllMessages RequestTypeSpecificMessages + RequestTypeExportGuild + RequestTypeExportUser ) type GDPRRequest struct { diff --git a/bot/utils/gdprconfirmation.go b/bot/utils/gdprconfirmation.go new file mode 100644 index 00000000..443afda4 --- /dev/null +++ b/bot/utils/gdprconfirmation.go @@ -0,0 +1,91 @@ +package utils + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/go-redis/redis/v8" +) + +// Matches the lifetime of the interaction token the confirmation belongs to. +const gdprConfirmationTTL = 15 * time.Minute + +const gdprConfirmationKeyPrefix = "tickets:gdpr:confirm:" + +// Discord caps custom_id at 100 characters, which fits only three snowflakes. Holding the +// selection here keeps the id a constant size whatever the user picked, and lets the handler +// delete the key on use so a double click cannot queue the same request twice. +type GDPRConfirmation struct { + UserId uint64 `json:"user_id"` + GuildIds []uint64 `json:"guild_ids,omitempty"` + TicketIds []int `json:"ticket_ids,omitempty"` + Locale string `json:"locale,omitempty"` +} + +func StoreGDPRConfirmation(ctx context.Context, client *redis.Client, data GDPRConfirmation) (string, error) { + raw := make([]byte, 8) + if _, err := rand.Read(raw); err != nil { + return "", err + } + + token := hex.EncodeToString(raw) + + encoded, err := json.Marshal(data) + if err != nil { + return "", err + } + + if err := client.Set(ctx, gdprConfirmationKeyPrefix+token, encoded, gdprConfirmationTTL).Err(); err != nil { + return "", err + } + + return token, nil +} + +// Loads and deletes the pending request; false when the token is unknown, used, or expired. +func ConsumeGDPRConfirmation(ctx context.Context, client *redis.Client, customId string, userId uint64) (GDPRConfirmation, bool) { + token := ExtractGDPRToken(customId) + if token == "" { + return GDPRConfirmation{}, false + } + + key := gdprConfirmationKeyPrefix + token + + encoded, err := client.Get(ctx, key).Bytes() + if err != nil { + return GDPRConfirmation{}, false + } + + client.Del(ctx, key) + + var data GDPRConfirmation + if err := json.Unmarshal(encoded, &data); err != nil { + return GDPRConfirmation{}, false + } + + // Bound to its owner so a leaked id cannot be replayed by anyone else. + if data.UserId != userId { + return GDPRConfirmation{}, false + } + + return data, true +} + +// Parses "gdpr_confirm___". +func ExtractGDPRToken(customId string) string { + parts := strings.Split(customId, "_") + if len(parts) < 2 { + return "" + } + + return parts[len(parts)-2] +} + +func BuildGDPRConfirmId(action, token, langCode string) string { + return fmt.Sprintf("gdpr_confirm_%s_%s_%s", action, token, langCode) +} diff --git a/bot/utils/gdprutils.go b/bot/utils/gdprutils.go index a4006ad2..a9e57198 100644 --- a/bot/utils/gdprutils.go +++ b/bot/utils/gdprutils.go @@ -13,45 +13,6 @@ import ( "github.com/TicketsBot-cloud/worker/i18n" ) -func ParseGuildIds(customId string) []uint64 { - // Expected format: "gdpr_confirm_all_transcripts_{guildIds}_{langCode}" - // where {guildIds} is comma-separated like "123456,789012" and {langCode} is like "en" - - // Find the last underscore which contains the language code - lastUnderscoreIdx := strings.LastIndex(customId, "_") - if lastUnderscoreIdx == -1 || lastUnderscoreIdx == len(customId)-1 { - return nil - } - - // Remove the language code part to get everything before it - withoutLangCode := customId[:lastUnderscoreIdx] - - // Find the second-to-last underscore which separates the prefix from the guild IDs - secondLastUnderscoreIdx := strings.LastIndex(withoutLangCode, "_") - if secondLastUnderscoreIdx == -1 || secondLastUnderscoreIdx == len(withoutLangCode)-1 { - return nil - } - - // Extract guild IDs (everything between second-to-last and last underscore) - guildIdsStr := withoutLangCode[secondLastUnderscoreIdx+1:] - - // Handle comma-separated guild IDs - guildIdParts := strings.Split(guildIdsStr, ",") - - var guildIds []uint64 - for _, idStr := range guildIdParts { - idStr = strings.TrimSpace(idStr) - if idStr == "" { - continue - } - if id, err := strconv.ParseUint(idStr, 10, 64); err == nil { - guildIds = append(guildIds, id) - } - } - - return guildIds -} - func ParseTicketIds(input string) []int { input = strings.ReplaceAll(input, ";", ",") input = strings.ReplaceAll(input, "\n", ",") diff --git a/i18n/messages.go b/i18n/messages.go index 181fd8c0..83581540 100644 --- a/i18n/messages.go +++ b/i18n/messages.go @@ -433,13 +433,27 @@ var ( GdprQueuedSpecificMessages MessageId = "gdpr.queued.specific_messages" GdprQueuedFooter MessageId = "gdpr.queued.footer" - GdprErrorNoServers MessageId = "gdpr.error.no_servers" - GdprErrorInvalidServerId MessageId = "gdpr.error.invalid_server_id" - GdprErrorInvalidTicketIds MessageId = "gdpr.error.invalid_ticket_ids" - GdprErrorNotOwner MessageId = "gdpr.error.not_owner" - GdprErrorServerNotFound MessageId = "gdpr.error.server_not_found" - GdprErrorQueueFailed MessageId = "gdpr.error.queue_failed" - GdprErrorWorkerOffline MessageId = "gdpr.error.worker_offline" + GdprErrorNoServers MessageId = "gdpr.error.no_servers" + GdprErrorInvalidServerId MessageId = "gdpr.error.invalid_server_id" + GdprErrorInvalidTicketIds MessageId = "gdpr.error.invalid_ticket_ids" + GdprErrorNotOwner MessageId = "gdpr.error.not_owner" + GdprErrorServerNotFound MessageId = "gdpr.error.server_not_found" + GdprErrorQueueFailed MessageId = "gdpr.error.queue_failed" + GdprErrorWorkerOffline MessageId = "gdpr.error.worker_offline" + GdprErrorConfirmationExpired MessageId = "gdpr.error.confirmation_expired" + GdprErrorBotNotInServer MessageId = "gdpr.error.bot_not_in_server" + + GdprExportSectionTitle MessageId = "gdpr.section.export" + GdprButtonExportGuild MessageId = "gdpr.button.export_guild" + GdprButtonExportUser MessageId = "gdpr.button.export_user" + GdprModalExportGuildTitle MessageId = "gdpr.modal.export_guild" + GdprConfirmExportGuild MessageId = "gdpr.confirm.export_guild" + GdprConfirmExportGuildMulti MessageId = "gdpr.confirm.export_guild_multi" + GdprConfirmExportUser MessageId = "gdpr.confirm.export_user" + GdprConfirmExportButton MessageId = "gdpr.confirm.export_button" + GdprQueuedExportGuild MessageId = "gdpr.queued.export_guild" + GdprQueuedExportGuildMulti MessageId = "gdpr.queued.export_guild_multi" + GdprQueuedExportUser MessageId = "gdpr.queued.export_user" MessageEditTitle MessageId = "commands.edit.title" MessageEditDescription MessageId = "commands.edit.description"