From e15169655a6774d2bfbfe1c7e2c7f21eee9d79d4 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 30 May 2025 16:40:33 +0100 Subject: [PATCH 01/13] cv2 Signed-off-by: Ben --- bot/button/handlers/help_category.go | 78 +++++++++ bot/button/handlers/help_page.go | 83 ++++++++++ bot/button/manager/manager.go | 2 + bot/command/commandcategory.go | 17 ++ bot/command/impl/general/help.go | 99 ++---------- bot/command/impl/general/vote.go | 78 ++++++--- bot/command/manager/manager.go | 13 ++ bot/command/messageresponse.go | 7 + bot/command/registry/command.go | 24 +++ bot/logic/help.go | 230 +++++++++++++++++++++++++++ bot/utils/messageutils.go | 28 ++++ config/config.go | 5 +- event/httplisten.go | 3 + go.mod | 6 +- 14 files changed, 558 insertions(+), 115 deletions(-) create mode 100644 bot/button/handlers/help_category.go create mode 100644 bot/button/handlers/help_page.go create mode 100644 bot/logic/help.go diff --git a/bot/button/handlers/help_category.go b/bot/button/handlers/help_category.go new file mode 100644 index 0000000..b2102c9 --- /dev/null +++ b/bot/button/handlers/help_category.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "math" + "strings" + + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" + "github.com/TicketsBot-cloud/worker/bot/button/registry" + "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" + "github.com/TicketsBot-cloud/worker/bot/command" + "github.com/TicketsBot-cloud/worker/bot/command/context" + "github.com/TicketsBot-cloud/worker/bot/command/manager" + "github.com/TicketsBot-cloud/worker/bot/logic" +) + +type HelpCategoryHandler struct{} + +func (h *HelpCategoryHandler) Matcher() matcher.Matcher { + return &matcher.FuncMatcher{ + Func: func(customId string) bool { + return strings.HasPrefix(customId, "help_") + }, + } +} + +func (h *HelpCategoryHandler) Properties() registry.Properties { + return registry.Properties{ + Flags: registry.SumFlags(registry.GuildAllowed), + } +} + +func (h *HelpCategoryHandler) Execute(ctx *context.SelectMenuContext) { + if len(ctx.InteractionData.Values) == 0 { + return + } + + commandManager := new(manager.CommandManager) + commandManager.RegisterCommands() + + helpCategoryCustomId := ctx.InteractionData.Values[0] + + helpCategory := command.General + + switch helpCategoryCustomId { + case "help_general": + helpCategory = command.General + case "help_tickets": + helpCategory = command.Tickets + case "help_settings": + helpCategory = command.Settings + case "help_tags": + helpCategory = command.Tags + case "help_statistics": + helpCategory = command.Statistics + default: + return + } + + container, err := logic.BuildHelpMessage(helpCategory, 1, ctx, commandManager.GetCommands()) + if err != nil { + ctx.HandleError(err) + return + } + + // get commands in selected category + commands := commandManager.GetCommandByCategory(helpCategory) + pageCount := float64(len(commands)) / float64(5) + + if pageCount == 0 { + pageCount = 1 + } + + ctx.Edit(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + *container, + *logic.BuildHelpMessagePaginationButtons(helpCategory.ToRawString(), 1, int(math.Ceil(pageCount))), + *logic.BuildHelpMessageCategorySelector(commandManager.GetCommands(), ctx, helpCategory), + })) +} diff --git a/bot/button/handlers/help_page.go b/bot/button/handlers/help_page.go new file mode 100644 index 0000000..fbaf3b3 --- /dev/null +++ b/bot/button/handlers/help_page.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "math" + "strconv" + "strings" + + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" + "github.com/TicketsBot-cloud/worker/bot/button/registry" + "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" + "github.com/TicketsBot-cloud/worker/bot/command" + "github.com/TicketsBot-cloud/worker/bot/command/context" + "github.com/TicketsBot-cloud/worker/bot/command/manager" + "github.com/TicketsBot-cloud/worker/bot/logic" +) + +type HelpPageHandler struct{} + +func (h *HelpPageHandler) Matcher() matcher.Matcher { + return &matcher.FuncMatcher{ + Func: func(customId string) bool { + return strings.HasPrefix(customId, "helppage_") + }, + } +} + +func (h *HelpPageHandler) Properties() registry.Properties { + return registry.Properties{ + Flags: registry.SumFlags(registry.GuildAllowed), + } +} + +func (h *HelpPageHandler) Execute(ctx *context.ButtonContext) { + if ctx.InteractionData.CustomId == "" { + return + } + + var ( + customIdParts = strings.Split(ctx.InteractionData.CustomId, "_") + category = customIdParts[1] + page, _ = strconv.ParseInt(customIdParts[2], 10, 64) + ) + + commandManager := new(manager.CommandManager) + commandManager.RegisterCommands() + + helpCategory := command.General + + switch category { + case "general": + helpCategory = command.General + case "tickets": + helpCategory = command.Tickets + case "settings": + helpCategory = command.Settings + case "tags": + helpCategory = command.Tags + case "statistics": + helpCategory = command.Statistics + default: + return + } + + container, err := logic.BuildHelpMessage(helpCategory, int(page), ctx, commandManager.GetCommands()) + if err != nil { + ctx.HandleError(err) + return + } + + // get commands in selected category + commands := commandManager.GetCommandByCategory(helpCategory) + pageCount := float64(len(commands)) / float64(5) + + if pageCount == 0 { + pageCount = 1 + } + + ctx.Edit(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + *container, + *logic.BuildHelpMessagePaginationButtons(helpCategory.ToRawString(), int(page), int(math.Ceil(pageCount))), + *logic.BuildHelpMessageCategorySelector(commandManager.GetCommands(), ctx, helpCategory), + })) +} diff --git a/bot/button/manager/manager.go b/bot/button/manager/manager.go index 1111315..b894c6a 100644 --- a/bot/button/manager/manager.go +++ b/bot/button/manager/manager.go @@ -66,12 +66,14 @@ func (m *ComponentInteractionManager) RegisterCommands() { new(handlers.RedeemVoteCreditsHandler), new(handlers.ViewStaffHandler), new(handlers.ViewSurveyHandler), + new(handlers.HelpPageHandler), ) m.selectRegistry = append(m.selectRegistry, new(handlers.LanguageSelectorHandler), new(handlers.MultiPanelHandler), new(handlers.PremiumKeyOpenHandler), + new(handlers.HelpCategoryHandler), ) m.modalRegistry = append(m.modalRegistry, diff --git a/bot/command/commandcategory.go b/bot/command/commandcategory.go index b672fdb..0a069e0 100644 --- a/bot/command/commandcategory.go +++ b/bot/command/commandcategory.go @@ -17,3 +17,20 @@ var Categories = []Category{ Tags, Statistics, } + +func (c Category) ToRawString() string { + switch c { + case General: + return "general" + case Tickets: + return "tickets" + case Settings: + return "settings" + case Tags: + return "tags" + case Statistics: + return "statistics" + default: + return string(c) + } +} diff --git a/bot/command/impl/general/help.go b/bot/command/impl/general/help.go index 410cf30..2372544 100644 --- a/bot/command/impl/general/help.go +++ b/bot/command/impl/general/help.go @@ -1,22 +1,15 @@ package general import ( - "fmt" - "sort" - "strings" "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" - "github.com/TicketsBot-cloud/worker/bot/customisation" - "github.com/TicketsBot-cloud/worker/bot/utils" - "github.com/TicketsBot-cloud/worker/config" + "github.com/TicketsBot-cloud/worker/bot/logic" "github.com/TicketsBot-cloud/worker/i18n" - "github.com/elliotchance/orderedmap" ) type HelpCommand struct { @@ -41,95 +34,21 @@ func (c HelpCommand) GetExecutor() interface{} { } func (c HelpCommand) Execute(ctx registry.CommandContext) { - commandCategories := orderedmap.NewOrderedMap() + container, err := logic.BuildHelpMessage(command.General, 1, ctx, c.Registry) - // initialise map with the correct order of categories - for _, category := range command.Categories { - commandCategories.Set(category, nil) - } - - permLevel, err := ctx.UserPermissionLevel(ctx) if err != nil { ctx.HandleError(err) return } - commandIds, err := command.LoadCommandIds(ctx.Worker(), ctx.Worker().BotId) + _, err = ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + *container, + *logic.BuildHelpMessagePaginationButtons(command.General.ToRawString(), 1, 1), + *logic.BuildHelpMessageCategorySelector(c.Registry, ctx, command.General), + })) + if err != nil { ctx.HandleError(err) return } - - for _, cmd := range c.Registry { - properties := cmd.Properties() - - // check bot admin / helper only commands - if (properties.AdminOnly && !utils.IsBotAdmin(ctx.UserId())) || (properties.HelperOnly && !utils.IsBotHelper(ctx.UserId())) { - continue - } - - // Show slash commands only - if properties.Type != interaction.ApplicationCommandTypeChatInput { - continue - } - - // check whitelabel hidden cmds - if properties.MainBotOnly && ctx.Worker().IsWhitelabel { - continue - } - - if permLevel >= cmd.Properties().PermissionLevel { // only send commands the user has permissions for - var current []registry.Command - if commands, ok := commandCategories.Get(properties.Category); ok { - if commands == nil { - current = make([]registry.Command, 0) - } else { - current = commands.([]registry.Command) - } - } - current = append(current, cmd) - - commandCategories.Set(properties.Category, current) - } - } - - embed := embed.NewEmbed(). - SetColor(ctx.GetColour(customisation.Green)). - SetTitle(ctx.GetMessage(i18n.TitleHelp)) - - for _, category := range commandCategories.Keys() { - var commands []registry.Command - if retrieved, ok := commandCategories.Get(category.(command.Category)); ok { - if retrieved == nil { - commands = make([]registry.Command, 0) - } else { - commands = retrieved.([]registry.Command) - } - } - - sort.Slice(commands, func(i, j int) bool { - return commands[i].Properties().Name < commands[j].Properties().Name - }) - - if len(commands) > 0 { - formatted := make([]string, 0) - for _, cmd := range commands { - var commandId *uint64 - if tmp, ok := commandIds[cmd.Properties().Name]; ok { - commandId = &tmp - } - - formatted = append(formatted, registry.FormatHelp(cmd, ctx.GuildId(), commandId)) - } - - embed.AddField(string(category.(command.Category)), strings.Join(formatted, "\n"), false) - } - } - - if ctx.PremiumTier() == premium.None { - embed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) - } - - // Explicitly ignore error to fix 403 (Cannot send messages to this user) - _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(embed)) } diff --git a/bot/command/impl/general/vote.go b/bot/command/impl/general/vote.go index 1ef1f85..e693c45 100644 --- a/bot/command/impl/general/vote.go +++ b/bot/command/impl/general/vote.go @@ -5,12 +5,10 @@ import ( "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" - "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/config" @@ -48,6 +46,8 @@ func (c VoteCommand) Execute(ctx registry.CommandContext) { return } + credits = 2 + if credits == 0 { commandIds, err := command.LoadCommandIds(ctx.Worker(), ctx.Worker().BotId) if err != nil { @@ -62,48 +62,86 @@ func (c VoteCommand) Execute(ctx registry.CommandContext) { commandMention = "`/vote`" } - embed := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleVote, i18n.MessageVote, nil, commandMention) + _, err = ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + component.BuildContainer(component.Container{ + Components: []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### %s", ctx.GetMessage(i18n.TitleVote)), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageVote, commandMention), + }), + component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), + buildVoteComponent(ctx, false), + }, + }), + })) - if _, err := ctx.ReplyWith(command.NewEphemeralEmbedMessageResponseWithComponents(embed, buildVoteComponents(ctx, false))); err != nil { + if err != nil { ctx.HandleError(err) return } } else { - var embed *embed.Embed - if credits == 1 { - embed = utils.BuildEmbed(ctx, customisation.Green, i18n.TitleVote, i18n.MessageVoteWithCreditsSingular, nil, credits, credits) - } else { - embed = utils.BuildEmbed(ctx, customisation.Green, i18n.TitleVote, i18n.MessageVoteWithCreditsPlural, nil, credits, credits) + componentBody := component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageVoteWithCreditsSingular, credits, credits), + }) + + if credits > 1 { + componentBody = component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageVoteWithCreditsPlural, credits, credits), + }) } - if _, err := ctx.ReplyWith(command.NewEphemeralEmbedMessageResponseWithComponents(embed, buildVoteComponents(ctx, true))); err != nil { + _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + component.BuildContainer(component.Container{ + Components: []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### %s", ctx.GetMessage(i18n.TitleVote)), + }), + component.BuildSeparator(component.Separator{}), + componentBody, + component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), + buildVoteComponent(ctx, true), + }, + }), + })) + + if err != nil { ctx.HandleError(err) return } } } -func buildVoteComponents(ctx registry.CommandContext, allowRedeem bool) []component.Component { - voteButton := component.BuildButton(component.Button{ - Label: ctx.GetMessage(i18n.TitleVote), +func buildVoteComponent(ctx registry.CommandContext, allowRedeem bool) component.Component { + voteButton1 := component.BuildButton(component.Button{ + Label: fmt.Sprintf("%s (TopGG)", ctx.GetMessage(i18n.TitleVote)), + Style: component.ButtonStyleLink, + // Emoji: utils.BuildEmoji("šŸ”—"), + Url: utils.Ptr(config.Conf.Bot.VoteUrl1), + }) + + voteButton2 := component.BuildButton(component.Button{ + Label: fmt.Sprintf("%s (DBL)", ctx.GetMessage(i18n.TitleVote)), Style: component.ButtonStyleLink, - Emoji: utils.BuildEmoji("šŸ”—"), - Url: utils.Ptr(config.Conf.Bot.VoteUrl), + // Emoji: utils.BuildEmoji("šŸ”—"), + Url: utils.Ptr(config.Conf.Bot.VoteUrl2), }) redeemButton := component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.MessageVoteRedeemCredits), CustomId: "redeem_vote_credits", - Style: component.ButtonStylePrimary, - Emoji: utils.BuildEmoji("šŸ’¶"), + Style: component.ButtonStyleSuccess, + // Emoji: utils.BuildEmoji("šŸ’¶"), }) var actionRow component.Component if allowRedeem { - actionRow = component.BuildActionRow(voteButton, redeemButton) + actionRow = component.BuildActionRow(voteButton1, voteButton2, redeemButton) } else { - actionRow = component.BuildActionRow(voteButton) + actionRow = component.BuildActionRow(voteButton1, voteButton2) } - return []component.Component{actionRow} + return actionRow } diff --git a/bot/command/manager/manager.go b/bot/command/manager/manager.go index ced9dcd..f92465a 100644 --- a/bot/command/manager/manager.go +++ b/bot/command/manager/manager.go @@ -3,6 +3,7 @@ package manager import ( "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/rest" + "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/impl/admin" "github.com/TicketsBot-cloud/worker/bot/command/impl/general" "github.com/TicketsBot-cloud/worker/bot/command/impl/settings" @@ -22,6 +23,18 @@ func (cm *CommandManager) GetCommands() map[string]registry.Command { return cm.registry } +func (cm *CommandManager) GetCommandByCategory(category command.Category) map[string]registry.Command { + commands := make(map[string]registry.Command) + + for name, cmd := range cm.registry { + if cmd.Properties().Category == category { + commands[name] = cmd + } + } + + return commands +} + func (cm *CommandManager) RegisterCommands() { cm.registry = make(map[string]registry.Command) diff --git a/bot/command/messageresponse.go b/bot/command/messageresponse.go index 8a86e60..f62b8d5 100644 --- a/bot/command/messageresponse.go +++ b/bot/command/messageresponse.go @@ -58,6 +58,13 @@ func NewEphemeralEmbedMessageResponseWithComponents(e *embed.Embed, components [ } } +func NewEphemeralMessageResponseWithComponents(components []component.Component) MessageResponse { + return MessageResponse{ + Flags: message.SumFlags(message.FlagEphemeral, message.FlagComponentsV2), + Components: components, + } +} + func (r *MessageResponse) IntoApplicationCommandData() interaction.ApplicationCommandCallbackData { return interaction.ApplicationCommandCallbackData{ Tts: r.Tts, diff --git a/bot/command/registry/command.go b/bot/command/registry/command.go index 39a6f05..cf0d05a 100644 --- a/bot/command/registry/command.go +++ b/bot/command/registry/command.go @@ -36,3 +36,27 @@ func FormatHelp(c Command, guildId uint64, commandId *uint64) string { return fmt.Sprintf(": %s", c.Properties().Name, *commandId, description) } } + +func FormatHelp2(c Command, guildId uint64, commandId *uint64) string { + description := i18n.GetMessageFromGuild(guildId, c.Properties().Description) + + if commandId == nil { + var args []string + for _, arg := range c.Properties().Arguments { + if arg.Required { + args = append(args, fmt.Sprintf("[%s] ", arg.Name)) + } else { + args = append(args, fmt.Sprintf("<%s> ", arg.Name)) + } + } + + var argsJoined string + if len(args) > 0 { + argsJoined = " " + strings.Join(args, " ") // Separate between command and first arg + } + + return fmt.Sprintf("**/%s%s**\n-# %s", c.Properties().Name, argsJoined, description) + } else { + return fmt.Sprintf("\n-# %s", c.Properties().Name, *commandId, description) + } +} diff --git a/bot/logic/help.go b/bot/logic/help.go new file mode 100644 index 0000000..581e57c --- /dev/null +++ b/bot/logic/help.go @@ -0,0 +1,230 @@ +package logic + +import ( + "errors" + "fmt" + "sort" + + "github.com/TicketsBot-cloud/common/permission" + "github.com/TicketsBot-cloud/common/premium" + "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" + "github.com/TicketsBot-cloud/worker/bot/command" + "github.com/TicketsBot-cloud/worker/bot/command/registry" + "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/utils" + "github.com/TicketsBot-cloud/worker/config" + "github.com/TicketsBot-cloud/worker/i18n" + "github.com/elliotchance/orderedmap" +) + +func BuildHelpMessage(category command.Category, page int, ctx registry.CommandContext, cmds map[string]registry.Command) (*component.Component, error) { + componentList := []component.Component{} + + // permLevel, _ := ctx.UserPermissionLevel(ctx) + permLevel := permission.Admin + + commandIds, err := command.LoadCommandIds(ctx.Worker(), ctx.Worker().BotId) + if err != nil { + return nil, errors.New("failed to load command IDs") + } + + // Sort commands by name + sortedCmds := make([]registry.Command, 0, len(cmds)) + for _, cmd := range cmds { + sortedCmds = append(sortedCmds, cmd) + } + + sort.Slice(sortedCmds, func(i, j int) bool { + return sortedCmds[i].Properties().Name < sortedCmds[j].Properties().Name + }) + + for _, cmd := range sortedCmds { + properties := cmd.Properties() + + // check bot admin / helper only commands + if (properties.AdminOnly && !utils.IsBotAdmin(ctx.UserId())) || (properties.HelperOnly && !utils.IsBotHelper(ctx.UserId())) { + continue + } + + // Show slash commands only + if properties.Type != interaction.ApplicationCommandTypeChatInput { + continue + } + + // check whitelabel hidden cmds + if properties.MainBotOnly && ctx.Worker().IsWhitelabel { + continue + } + + if properties.Category != category { + continue + } + + if permLevel < cmd.Properties().PermissionLevel { // only send commands the user has permissions for + continue + } + + commandId, ok := commandIds[cmd.Properties().Name] + + if !ok { + continue + } + + componentList = append(componentList, + component.BuildTextDisplay(component.TextDisplay{ + Content: registry.FormatHelp2(cmd, ctx.GuildId(), &commandId), + }), + component.BuildSeparator(component.Separator{}), + ) + + } + + // get certain commands for pagination + componentsPerPage := 10 + if len(componentList) > componentsPerPage { + startIndex := (page - 1) * componentsPerPage + endIndex := startIndex + componentsPerPage + + if startIndex > len(componentList) { + return nil, fmt.Errorf("page %d is out of range", page) + } + + if endIndex > len(componentList) { + endIndex = len(componentList) + } + + componentList = componentList[startIndex:endIndex] + } + + if ctx.PremiumTier() == premium.None { + componentList = append(componentList, + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), + }), + ) + } + + accentColor := ctx.GetColour(customisation.Green) + + container := component.BuildContainer(component.Container{ + Components: append([]component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("## %s\n-# %s", ctx.GetMessage(i18n.TitleHelp), category), + }), + component.BuildSeparator(component.Separator{}), + }, componentList...), + AccentColor: &accentColor, + }) + + return &container, nil +} + +func BuildHelpMessagePaginationButtons(category string, page, totalPages int) *component.Component { + if totalPages <= 1 { + totalPages = 1 + } + + actionRow := component.BuildActionRow( + component.BuildButton(component.Button{ + CustomId: fmt.Sprintf("helppage_%s_%d", category, page-1), + Style: component.ButtonStyleDanger, + Label: "<", + Disabled: page == 1, + }), + component.BuildButton(component.Button{ + CustomId: "help_page_count", + Style: component.ButtonStyleSecondary, + Label: fmt.Sprintf("%d/%d", page, totalPages), + Disabled: true, + }), + component.BuildButton(component.Button{ + CustomId: fmt.Sprintf("helppage_%s_%d", category, page+1), + Style: component.ButtonStyleSuccess, + Label: ">", + Disabled: page >= totalPages, + }), + ) + + return &actionRow +} + +func BuildHelpMessageCategorySelector(r registry.Registry, ctx registry.CommandContext, selectedCategory command.Category) *component.Component { + commandCategories := orderedmap.NewOrderedMap() + + // initialise map with the correct order of categories + for _, category := range command.Categories { + commandCategories.Set(category, nil) + } + + // permLevel, _ := ctx.UserPermissionLevel(ctx) + permLevel := permission.Admin + + for _, cmd := range r { + properties := cmd.Properties() + + // check bot admin / helper only commands + if (properties.AdminOnly && !utils.IsBotAdmin(ctx.UserId())) || (properties.HelperOnly && !utils.IsBotHelper(ctx.UserId())) { + continue + } + + // Show slash commands only + if properties.Type != interaction.ApplicationCommandTypeChatInput { + continue + } + + // check whitelabel hidden cmds + if properties.MainBotOnly && ctx.Worker().IsWhitelabel { + continue + } + + if permLevel >= cmd.Properties().PermissionLevel { // only send commands the user has permissions for + var current []registry.Command + if commands, ok := commandCategories.Get(properties.Category); ok { + if commands == nil { + current = make([]registry.Command, 0) + } else { + current = commands.([]registry.Command) + } + } + current = append(current, cmd) + + commandCategories.Set(properties.Category, current) + } + } + + categorySelectItems := make([]component.SelectOption, commandCategories.Len()) + + for i, key := range commandCategories.Keys() { + var commands []registry.Command + if retrieved, ok := commandCategories.Get(key.(command.Category)); ok { + if retrieved == nil { + commands = make([]registry.Command, 0) + } else { + commands = retrieved.([]registry.Command) + } + } + + var description string + if len(commands) == 1 { + description = fmt.Sprintf("(%d command)", len(commands)) + } else { + description = fmt.Sprintf("(%d commands)", len(commands)) + } + + categorySelectItems[i] = component.SelectOption{ + Label: string(key.(command.Category)), + Value: fmt.Sprintf("help_%s", key.(command.Category).ToRawString()), + Description: description, + Default: key == selectedCategory, + } + } + + row := component.BuildActionRow(component.BuildSelectMenu(component.SelectMenu{ + CustomId: "help_select_category", + Options: categorySelectItems, + Placeholder: "Select a category", + })) + + return &row +} diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index 5475062..ffdac16 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -7,6 +7,7 @@ import ( "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" @@ -58,6 +59,33 @@ func BuildEmbedRaw( return msgEmbed } +func BuildContainerRaw( + colourHex int, title, content string, tier premium.PremiumTier, +) component.Component { + components := Slice( + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("## %s", title), + }), + component.BuildTextDisplay(component.TextDisplay{ + Content: content, + }), + ) + + if tier == premium.None { + components = append(components, + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), + }), + ) + } + + return component.BuildContainer(component.Container{ + AccentColor: &colourHex, + Components: components, + }) +} + func GetColourForGuild(ctx context.Context, worker *worker.Context, colour customisation.Colour, guildId uint64) (int, error) { premiumTier, err := PremiumClient.GetTierByGuildId(ctx, guildId, true, worker.Token, worker.RateLimiter) if err != nil { diff --git a/config/config.go b/config/config.go index 0634069..8a13f60 100644 --- a/config/config.go +++ b/config/config.go @@ -32,8 +32,9 @@ type ( HttpAddress string `env:"HTTP_ADDR"` DashboardUrl string `env:"DASHBOARD_URL" envDefault:"https://dashboard.ticketsbot.cloud"` FrontpageUrl string `env:"FRONTPAGE_URL" envDefault:"https://ticketsbot.cloud"` - VoteUrl string `env:"VOTE_URL" envDefault:"https://vote.ticketsbot.cloud"` - PoweredBy string `env:"POWEREDBY" envDefault:"ticketsbot.cloud"` + VoteUrl1 string `env:"VOTE_URL_1" envDefault:"https://top.gg/bot/1325579039888511056/vote"` + VoteUrl2 string `env:"VOTE_URL_2" envDefault:"https://discordbotlist.com/bots/tickets-v2/upvote"` + PoweredBy string `env:"POWEREDBY" envDefault:"[ticketsbot.cloud](https://ticketsbot.cloud)"` IconUrl string `env:"ICON_URL" envDefault:"https://ticketsbot.cloud/assets/img/logo.png"` SupportServerInvite string `env:"SUPPORT_SERVER_INVITE" envDefault:"https://discord.gg/ticketsbot"` Admins []uint64 `env:"WORKER_BOT_ADMINS"` diff --git a/event/httplisten.go b/event/httplisten.go index 6cff801..a88fae1 100644 --- a/event/httplisten.go +++ b/event/httplisten.go @@ -121,6 +121,7 @@ func interactionHandler(redis *redis.Client, cache *cache.PgCache) func(*gin.Con Cache: cache, RateLimiter: nil, // Use http-proxy ratelimit functionality } + fmt.Println("Handling interaction", payload.InteractionType) switch payload.InteractionType { case interaction.InteractionTypeApplicationCommand: @@ -283,6 +284,7 @@ func handleApplicationCommandResponseAfterDefer(interactionData interaction.Appl Embeds: data.Embeds, AllowedMentions: data.AllowedMentions, Components: data.Components, + Flags: data.Flags, } if _, err := rest.CreateFollowupMessage(context.Background(), interactionData.Token, worker.RateLimiter, worker.BotId, restData); err != nil { @@ -297,6 +299,7 @@ func handleApplicationCommandResponseAfterDefer(interactionData interaction.Appl Embeds: data.Embeds, AllowedMentions: data.AllowedMentions, Components: data.Components, + Flags: data.Flags, } if _, err := rest.EditOriginalInteractionResponse(context.Background(), interactionData.Token, worker.RateLimiter, worker.BotId, restData); err != nil { diff --git a/go.mod b/go.mod index 231fa3a..0af6544 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.23.8 toolchain go1.24.2 // replace github.com/TicketsBot-cloud/database => ../database -// replace github.com/TicketsBot-cloud/gdl => ../gdl -// replace github.com/TicketsBot-cloud/archiverclient => ../archiverclient -// replace github.com/TicketsBot-cloud/logarchiver => ../logarchiver +replace github.com/TicketsBot-cloud/gdl => ../gdl +replace github.com/TicketsBot-cloud/archiverclient => ../archiverclient +replace github.com/TicketsBot-cloud/logarchiver => ../logarchiver require ( cloud.google.com/go/profiler v0.4.2 From 8118e21bd96e772ba5aa1caa66e4a5eb177132d2 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 31 May 2025 15:31:22 +0100 Subject: [PATCH 02/13] cv2 Signed-off-by: Ben --- bot/command/impl/admin/adminblacklist.go | 24 +++++++- bot/command/impl/admin/admincheckblacklist.go | 17 ++++-- bot/command/impl/admin/admincheckpremium.go | 10 +++- bot/command/impl/admin/admingenpremium.go | 11 +++- bot/command/impl/admin/admingetowner.go | 11 +++- .../impl/admin/adminlistguildentitlements.go | 32 +++++----- .../impl/admin/adminlistuserentitlements.go | 33 ++++++----- bot/command/impl/admin/adminrecache.go | 30 +++++++++- .../impl/admin/adminwhitelabelassignguild.go | 11 +++- bot/command/impl/general/about.go | 9 ++- bot/command/impl/general/invite.go | 13 ++++- bot/command/impl/general/jumptotop.go | 12 ++-- bot/command/impl/general/vote.go | 28 +++------ bot/command/impl/settings/addadmin.go | 25 ++++---- bot/command/impl/settings/addsupport.go | 25 ++++---- bot/command/messageresponse.go | 7 +++ bot/logic/help.go | 13 ++--- bot/utils/messageutils.go | 58 +++++++++++++++++++ config/config.go | 1 + 19 files changed, 273 insertions(+), 97 deletions(-) diff --git a/bot/command/impl/admin/adminblacklist.go b/bot/command/impl/admin/adminblacklist.go index 43fe147..f397fee 100644 --- a/bot/command/impl/admin/adminblacklist.go +++ b/bot/command/impl/admin/adminblacklist.go @@ -1,16 +1,19 @@ package admin import ( + "fmt" "strconv" "time" "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -44,12 +47,31 @@ func (AdminBlacklistCommand) Execute(ctx registry.CommandContext, raw string, re return } + // Check if the guild is already blacklisted + if isBlacklisted, blacklistReason, _ := dbclient.Client.ServerBlacklist.IsBlacklisted(ctx, guildId); isBlacklisted { + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Blacklist", + fmt.Sprintf("Guild is already blacklisted.\n\n**Guild ID:** `%d`\n\n**Reason**: `%s`", guildId, utils.ValueOrDefault(blacklistReason, "No reason provided")), + ctx.PremiumTier(), + ), + })) + } + if err := dbclient.Client.ServerBlacklist.Add(ctx, guildId, reason); err != nil { ctx.HandleError(err) return } - ctx.ReplyPlainPermanent("šŸ”Ø") + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Blacklist", + fmt.Sprintf("Guild has been blacklisted successfully.\n\n**Guild ID:** `%d`\n**Reason:** %s", guildId, utils.ValueOrDefault(reason, "No reason provided")), + ctx.PremiumTier(), + ), + })) // Check if whitelabel botId, ok, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, guildId) diff --git a/bot/command/impl/admin/admincheckblacklist.go b/bot/command/impl/admin/admincheckblacklist.go index 6dd55d6..a56aaaf 100644 --- a/bot/command/impl/admin/admincheckblacklist.go +++ b/bot/command/impl/admin/admincheckblacklist.go @@ -7,6 +7,7 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" @@ -50,10 +51,18 @@ func (AdminCheckBlacklistCommand) Execute(ctx registry.CommandContext, raw strin return } + response := fmt.Sprintf("This guild is not blacklisted\n\n**Guild ID:** `%d`", guildId) + if isBlacklisted { - reasonFormatted := utils.ValueOrDefault(reason, "No reason provided") - ctx.ReplyRaw(customisation.Orange, "Blacklist Check", fmt.Sprintf("This guild is blacklisted.\n```%s```", reasonFormatted)) - } else { - ctx.ReplyRaw(customisation.Green, "Blacklist Check", "This guild is not blacklisted") + response = fmt.Sprintf("This guild is blacklisted.\n\n**Guild ID:** `%d`\n**Reason:** `%s`", guildId, utils.ValueOrDefault(reason, "No reason provided")) } + + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Blacklist Check", + response, + ctx.PremiumTier(), + ), + })) } diff --git a/bot/command/impl/admin/admincheckpremium.go b/bot/command/impl/admin/admincheckpremium.go index eb06310..9b90dd3 100644 --- a/bot/command/impl/admin/admincheckpremium.go +++ b/bot/command/impl/admin/admincheckpremium.go @@ -7,6 +7,7 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" @@ -55,5 +56,12 @@ func (AdminCheckPremiumCommand) Execute(ctx registry.CommandContext, raw string) return } - ctx.ReplyRaw(customisation.Green, ctx.GetMessage(i18n.Admin), fmt.Sprintf("Server Name: `%s`\nOwner: <@%d> `%d`\nPremium Tier: %s\nPremium Source: %s", guild.Name, guild.OwnerId, guild.OwnerId, tier.String(), src)) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Premium Check", + fmt.Sprintf("**Server Name:** `%s`\n**Owner:** <@%d> `%d`\n**Premium Tier:** `%s`\n**Premium Source:** `%s`", guild.Name, guild.OwnerId, guild.OwnerId, tier.String(), src), + ctx.PremiumTier(), + ), + })) } diff --git a/bot/command/impl/admin/admingenpremium.go b/bot/command/impl/admin/admingenpremium.go index 168cdac..bb118a3 100644 --- a/bot/command/impl/admin/admingenpremium.go +++ b/bot/command/impl/admin/admingenpremium.go @@ -9,10 +9,12 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" "github.com/google/uuid" ) @@ -108,7 +110,14 @@ func (AdminGenPremiumCommand) Execute(ctx registry.CommandContext, skuIdRaw stri return } - ctx.ReplyPlainPermanent("Check your DMs") + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Premium Key Generation", + fmt.Sprintf("Keys have been generated successfully and sent to your DMs.\n\n**SKU:** `%s`\n**Length:** `%d` days\n**Amount:** `%d`", sku.Label, length, amount), + ctx.PremiumTier(), + ), + })) } func (AdminGenPremiumCommand) AutoCompleteHandler(data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice { diff --git a/bot/command/impl/admin/admingetowner.go b/bot/command/impl/admin/admingetowner.go index f57455f..30824eb 100644 --- a/bot/command/impl/admin/admingetowner.go +++ b/bot/command/impl/admin/admingetowner.go @@ -7,9 +7,11 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -48,5 +50,12 @@ func (AdminGetOwnerCommand) Execute(ctx registry.CommandContext, raw string) { return } - ctx.ReplyRaw(customisation.Green, ctx.GetMessage(i18n.Admin), fmt.Sprintf("`%s` is owned by <@%d> (%d)", guild.Name, guild.OwnerId, guild.OwnerId)) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Get Owner", + fmt.Sprintf("**Guild ID:** `%d`\n**Guild Name:** `%s`\n**Owner ID:** `%d`", guild.Id, guild.Name, guild.OwnerId), + ctx.PremiumTier(), + ), + })) } diff --git a/bot/command/impl/admin/adminlistguildentitlements.go b/bot/command/impl/admin/adminlistguildentitlements.go index 6030853..77dd915 100644 --- a/bot/command/impl/admin/adminlistguildentitlements.go +++ b/bot/command/impl/admin/adminlistguildentitlements.go @@ -7,13 +7,14 @@ import ( "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" w "github.com/TicketsBot-cloud/worker" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -91,22 +92,17 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu return } - embed := embed.NewEmbed(). - SetTitle("Entitlements"). - SetColor(ctx.GetColour(customisation.Blue)) - if len(entitlements) == 0 { - embed.SetDescription("No entitlements found") + ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "This guild has no entitlements") + return } - for i, entitlement := range entitlements { - if i >= 25 { - embed.SetDescription("Too many entitlements to display") - break - } + values := []component.Component{} + for _, entitlement := range entitlements { value := fmt.Sprintf( - "**Tier:** %s\n**Source:** %s\n**Expires:** \n**SKU ID:** %s\n**SKU Priority:** %d", + "####%s\n\n**Tier:** %s\n**Source:** %s\n**Expires:** \n**SKU ID:** %s\n**SKU Priority:** %d\n\n", + entitlement.SkuLabel, entitlement.Tier, entitlement.Source, entitlement.ExpiresAt.Unix(), @@ -114,8 +110,16 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu entitlement.SkuPriority, ) - embed.AddField(entitlement.SkuLabel, value, false) + values = append(values, component.BuildTextDisplay(component.TextDisplay{Content: value})) } - ctx.ReplyWithEmbed(embed) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainer( + ctx, + customisation.Orange, + i18n.Admin, + ctx.PremiumTier(), + values, + ), + })) } diff --git a/bot/command/impl/admin/adminlistuserentitlements.go b/bot/command/impl/admin/adminlistuserentitlements.go index e8b7a04..09f8f7b 100644 --- a/bot/command/impl/admin/adminlistuserentitlements.go +++ b/bot/command/impl/admin/adminlistuserentitlements.go @@ -5,12 +5,13 @@ import ( "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -44,22 +45,18 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use return } - embed := embed.NewEmbed(). - SetTitle("Entitlements"). - SetColor(ctx.GetColour(customisation.Blue)) - if len(entitlements) == 0 { - embed.SetDescription("No entitlements found") + ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "This user has no entitlements") + return } - for i, entitlement := range entitlements { - if i >= 25 { - embed.SetDescription("Too many entitlements to display") - break - } + values := []component.Component{} + + for _, entitlement := range entitlements { value := fmt.Sprintf( - "**Tier:** %s\n**Source:** %s\n**Expires:** \n**SKU ID:** %s\n**SKU Priority:** %d", + "#### %s\n\n**Tier:** %s\n**Source:** %s\n**Expires:** \n**SKU ID:** %s\n**SKU Priority:** %d\n\n", + entitlement.SkuLabel, entitlement.Tier, entitlement.Source, entitlement.ExpiresAt.Unix(), @@ -67,8 +64,16 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use entitlement.SkuPriority, ) - embed.AddField(entitlement.SkuLabel, value, false) + values = append(values, component.BuildTextDisplay(component.TextDisplay{Content: value})) } - ctx.ReplyWithEmbed(embed) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainer( + ctx, + customisation.Orange, + i18n.Admin, + ctx.PremiumTier(), + values, + ), + })) } diff --git a/bot/command/impl/admin/adminrecache.go b/bot/command/impl/admin/adminrecache.go index fbf98ac..46f794a 100644 --- a/bot/command/impl/admin/adminrecache.go +++ b/bot/command/impl/admin/adminrecache.go @@ -2,15 +2,19 @@ package admin import ( "errors" + "fmt" "strconv" "time" "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" w "github.com/TicketsBot-cloud/worker" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" + "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -49,6 +53,8 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, providedGuildId guildId = ctx.GuildId() } + currentTime := time.Now() + // purge cache ctx.Worker().Cache.DeleteGuild(ctx, guildId) ctx.Worker().Cache.DeleteGuildChannels(ctx, guildId) @@ -86,15 +92,33 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, providedGuildId worker = ctx.Worker() } - if _, err := worker.GetGuild(guildId); err != nil { + guild, err := worker.GetGuild(guildId) + if err != nil { ctx.HandleError(err) return } - if _, err := worker.GetGuildChannels(guildId); err != nil { + guildChannels, err := worker.GetGuildChannels(guildId) + if err != nil { ctx.HandleError(err) return } - ctx.ReplyPlainPermanent("done") + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerNoLocale( + ctx, + customisation.Orange, + "Admin - Recache", + ctx.PremiumTier(), + []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("**%s** has been recached successfully.\n\n**Guild ID:** %d\n**Time Taken:** %s", guild.Name, guildId, time.Since(currentTime).Round(time.Millisecond)), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### Cache Stats\n**Channels:** `%d`\n**Roles:** `%d`", len(guildChannels), len(guild.Roles)), + }), + }, + ), + })) } diff --git a/bot/command/impl/admin/adminwhitelabelassignguild.go b/bot/command/impl/admin/adminwhitelabelassignguild.go index 5fe214c..9f4a50e 100644 --- a/bot/command/impl/admin/adminwhitelabelassignguild.go +++ b/bot/command/impl/admin/adminwhitelabelassignguild.go @@ -7,10 +7,12 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -66,5 +68,12 @@ func (AdminWhitelabelAssignGuildCommand) Execute(ctx registry.CommandContext, bo return } - ctx.ReplyRaw(customisation.Green, ctx.GetMessage(i18n.Success), fmt.Sprintf("Assigned bot `%d` to guild `%d`", botId, guildId)) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Whitelabel Assign Guild", + fmt.Sprintf("This bot is now assigned to the guild\n\n**Bot ID:** `%d`\n**Guild ID:** `%d`", botId, guildId), + ctx.PremiumTier(), + ), + })) } diff --git a/bot/command/impl/general/about.go b/bot/command/impl/general/about.go index 262ce3d..03b75a6 100644 --- a/bot/command/impl/general/about.go +++ b/bot/command/impl/general/about.go @@ -5,9 +5,11 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -32,5 +34,10 @@ func (c AboutCommand) GetExecutor() interface{} { } func (AboutCommand) Execute(ctx registry.CommandContext) { - ctx.Reply(customisation.Green, i18n.TitleAbout, i18n.MessageAbout) + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{utils.BuildContainerRaw( + ctx.GetColour(customisation.Green), + ctx.GetMessage(i18n.TitleAbout), + ctx.GetMessage(i18n.MessageAbout), + ctx.PremiumTier(), + )})) } diff --git a/bot/command/impl/general/invite.go b/bot/command/impl/general/invite.go index d3fa1f8..713e3ca 100644 --- a/bot/command/impl/general/invite.go +++ b/bot/command/impl/general/invite.go @@ -5,9 +5,12 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/utils" + "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -32,5 +35,13 @@ func (c InviteCommand) GetExecutor() interface{} { } func (InviteCommand) Execute(ctx registry.CommandContext) { - ctx.Reply(customisation.Green, i18n.TitleInvite, i18n.MessageInvite) + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainer(ctx, customisation.Green, i18n.TitleInvite, ctx.PremiumTier(), []component.Component{ + component.BuildActionRow(component.BuildButton(component.Button{ + Label: ctx.GetMessage(i18n.ClickHere), + Style: component.ButtonStyleLink, + Url: utils.Ptr(config.Conf.Bot.InviteUrl), + })), + }), + })) } diff --git a/bot/command/impl/general/jumptotop.go b/bot/command/impl/general/jumptotop.go index d615815..edb7b50 100644 --- a/bot/command/impl/general/jumptotop.go +++ b/bot/command/impl/general/jumptotop.go @@ -52,20 +52,20 @@ func (JumpToTopCommand) Execute(ctx registry.CommandContext) { } messageLink := fmt.Sprintf("https://discord.com/channels/%d/%d/%d", ctx.GuildId(), ctx.ChannelId(), *ticket.WelcomeMessageId) - - embed := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleJumpToTop, i18n.MessageJumpToTopContent, nil) - res := command.NewEphemeralEmbedMessageResponse(embed) - res.Components = []component.Component{ + components := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: ctx.GetMessage(i18n.MessageJumpToTopContent)}), + component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), component.BuildActionRow(component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.ClickHere), Style: component.ButtonStyleLink, - Emoji: nil, Url: utils.Ptr(messageLink), Disabled: false, })), } - if _, err := ctx.ReplyWith(res); err != nil { + if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainer(ctx, customisation.Green, i18n.TitleJumpToTop, ctx.PremiumTier(), components), + })); err != nil { ctx.HandleError(err) return } diff --git a/bot/command/impl/general/vote.go b/bot/command/impl/general/vote.go index e693c45..f7c45c7 100644 --- a/bot/command/impl/general/vote.go +++ b/bot/command/impl/general/vote.go @@ -9,6 +9,7 @@ import ( "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" + "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/config" @@ -93,21 +94,13 @@ func (c VoteCommand) Execute(ctx registry.CommandContext) { }) } - _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - component.BuildContainer(component.Container{ - Components: []component.Component{ - component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("### %s", ctx.GetMessage(i18n.TitleVote)), - }), - component.BuildSeparator(component.Separator{}), - componentBody, - component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), - buildVoteComponent(ctx, true), - }, + if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainer(ctx, customisation.Green, i18n.TitleVote, ctx.PremiumTier(), []component.Component{ + componentBody, + component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), + buildVoteComponent(ctx, true), }), - })) - - if err != nil { + })); err != nil { ctx.HandleError(err) return } @@ -118,22 +111,19 @@ func buildVoteComponent(ctx registry.CommandContext, allowRedeem bool) component voteButton1 := component.BuildButton(component.Button{ Label: fmt.Sprintf("%s (TopGG)", ctx.GetMessage(i18n.TitleVote)), Style: component.ButtonStyleLink, - // Emoji: utils.BuildEmoji("šŸ”—"), - Url: utils.Ptr(config.Conf.Bot.VoteUrl1), + Url: utils.Ptr(config.Conf.Bot.VoteUrl1), }) voteButton2 := component.BuildButton(component.Button{ Label: fmt.Sprintf("%s (DBL)", ctx.GetMessage(i18n.TitleVote)), Style: component.ButtonStyleLink, - // Emoji: utils.BuildEmoji("šŸ”—"), - Url: utils.Ptr(config.Conf.Bot.VoteUrl2), + Url: utils.Ptr(config.Conf.Bot.VoteUrl2), }) redeemButton := component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.MessageVoteRedeemCredits), CustomId: "redeem_vote_credits", Style: component.ButtonStyleSuccess, - // Emoji: utils.BuildEmoji("šŸ’¶"), }) var actionRow component.Component diff --git a/bot/command/impl/settings/addadmin.go b/bot/command/impl/settings/addadmin.go index 5ac1d6e..670f71b 100644 --- a/bot/command/impl/settings/addadmin.go +++ b/bot/command/impl/settings/addadmin.go @@ -62,17 +62,20 @@ func (c AddAdminCommand) Execute(ctx registry.CommandContext, id uint64) { } // Send confirmation message - e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleAddAdmin, i18n.MessageAddAdminConfirm, nil, mention) - res := command.NewEphemeralEmbedMessageResponseWithComponents(e, utils.Slice(component.BuildActionRow( - component.BuildButton(component.Button{ - Label: ctx.GetMessage(i18n.Confirm), - CustomId: fmt.Sprintf("addadmin-%d-%d", mentionableType, id), - Style: component.ButtonStylePrimary, - Emoji: nil, - }), - ))) - - if _, err := ctx.ReplyWith(res); err != nil { + if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainer(ctx, customisation.Green, i18n.TitleAddAdmin, ctx.PremiumTier(), []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageAddAdminConfirm, mention), + }), + component.BuildActionRow( + component.BuildButton(component.Button{ + Label: ctx.GetMessage(i18n.Confirm), + CustomId: fmt.Sprintf("addadmin-%d-%d", mentionableType, id), + Style: component.ButtonStyleSuccess, + Emoji: nil, + }), + ), + })})); err != nil { ctx.HandleError(err) } } diff --git a/bot/command/impl/settings/addsupport.go b/bot/command/impl/settings/addsupport.go index 62735c9..a877038 100644 --- a/bot/command/impl/settings/addsupport.go +++ b/bot/command/impl/settings/addsupport.go @@ -66,17 +66,20 @@ func (c AddSupportCommand) Execute(ctx registry.CommandContext, id uint64) { } // Send confirmation message - e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleAddSupport, i18n.MessageAddSupportConfirm, nil, mention) - res := command.NewEphemeralEmbedMessageResponseWithComponents(e, utils.Slice(component.BuildActionRow( - component.BuildButton(component.Button{ - Label: ctx.GetMessage(i18n.Confirm), - CustomId: fmt.Sprintf("addsupport-%d-%d", mentionableType, id), - Style: component.ButtonStylePrimary, - Emoji: nil, - }), - ))) - - if _, err := ctx.ReplyWith(res); err != nil { + if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainer(ctx, customisation.Green, i18n.TitleAddSupport, ctx.PremiumTier(), []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageAddSupportConfirm, mention), + }), + component.BuildActionRow( + component.BuildButton(component.Button{ + Label: ctx.GetMessage(i18n.Confirm), + CustomId: fmt.Sprintf("addsupport-%d-%d", mentionableType, id), + Style: component.ButtonStylePrimary, + Emoji: nil, + }), + ), + })})); err != nil { ctx.HandleError(err) } } diff --git a/bot/command/messageresponse.go b/bot/command/messageresponse.go index f62b8d5..9e9f8bb 100644 --- a/bot/command/messageresponse.go +++ b/bot/command/messageresponse.go @@ -65,6 +65,13 @@ func NewEphemeralMessageResponseWithComponents(components []component.Component) } } +func NewMessageResponseWithComponents(components []component.Component) MessageResponse { + return MessageResponse{ + Flags: message.SumFlags(message.FlagComponentsV2), + Components: components, + } +} + func (r *MessageResponse) IntoApplicationCommandData() interaction.ApplicationCommandCallbackData { return interaction.ApplicationCommandCallbackData{ Tts: r.Tts, diff --git a/bot/logic/help.go b/bot/logic/help.go index 581e57c..fbdf540 100644 --- a/bot/logic/help.go +++ b/bot/logic/help.go @@ -5,7 +5,6 @@ import ( "fmt" "sort" - "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" @@ -21,8 +20,8 @@ import ( func BuildHelpMessage(category command.Category, page int, ctx registry.CommandContext, cmds map[string]registry.Command) (*component.Component, error) { componentList := []component.Component{} - // permLevel, _ := ctx.UserPermissionLevel(ctx) - permLevel := permission.Admin + permLevel, _ := ctx.UserPermissionLevel(ctx) + // permLevel := permission.Admin commandIds, err := command.LoadCommandIds(ctx.Worker(), ctx.Worker().BotId) if err != nil { @@ -105,8 +104,6 @@ func BuildHelpMessage(category command.Category, page int, ctx registry.CommandC ) } - accentColor := ctx.GetColour(customisation.Green) - container := component.BuildContainer(component.Container{ Components: append([]component.Component{ component.BuildTextDisplay(component.TextDisplay{ @@ -114,7 +111,7 @@ func BuildHelpMessage(category command.Category, page int, ctx registry.CommandC }), component.BuildSeparator(component.Separator{}), }, componentList...), - AccentColor: &accentColor, + AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), }) return &container, nil @@ -157,8 +154,8 @@ func BuildHelpMessageCategorySelector(r registry.Registry, ctx registry.CommandC commandCategories.Set(category, nil) } - // permLevel, _ := ctx.UserPermissionLevel(ctx) - permLevel := permission.Admin + permLevel, _ := ctx.UserPermissionLevel(ctx) + // permLevel := permission.Admin for _, cmd := range r { properties := cmd.Properties() diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index ffdac16..3f8c33a 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -14,6 +14,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" + "github.com/TicketsBot/common/utils" ) func BuildEmbed( @@ -59,6 +60,62 @@ func BuildEmbedRaw( return msgEmbed } +func BuildContainer( + ctx registry.CommandContext, colour customisation.Colour, title i18n.MessageId, tier premium.PremiumTier, innerComponents []component.Component, +) component.Component { + components := append(Slice( + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### %s", ctx.GetMessage(title)), + }), + component.BuildSeparator(component.Separator{}), + ), innerComponents...) + + if tier == premium.None { + // check if last component is a separator, if not add one + if len(components) == 0 || components[len(components)-1].Type != component.ComponentSeparator { + components = append(components, component.BuildSeparator(component.Separator{})) + } + components = append(components, + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), + }), + ) + } + + return component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(ctx.GetColour(colour)), + Components: components, + }) +} + +func BuildContainerNoLocale( + ctx registry.CommandContext, colour customisation.Colour, title string, tier premium.PremiumTier, innerComponents []component.Component, +) component.Component { + components := append(Slice( + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### %s", title), + }), + component.BuildSeparator(component.Separator{}), + ), innerComponents...) + + if tier == premium.None { + // check if last component is a separator, if not add one + if len(components) == 0 || components[len(components)-1].Type != component.ComponentSeparator { + components = append(components, component.BuildSeparator(component.Separator{})) + } + components = append(components, + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), + }), + ) + } + + return component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(ctx.GetColour(colour)), + Components: components, + }) +} + func BuildContainerRaw( colourHex int, title, content string, tier premium.PremiumTier, ) component.Component { @@ -66,6 +123,7 @@ func BuildContainerRaw( component.BuildTextDisplay(component.TextDisplay{ Content: fmt.Sprintf("## %s", title), }), + component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ Content: content, }), diff --git a/config/config.go b/config/config.go index 8a13f60..6badfce 100644 --- a/config/config.go +++ b/config/config.go @@ -32,6 +32,7 @@ type ( HttpAddress string `env:"HTTP_ADDR"` DashboardUrl string `env:"DASHBOARD_URL" envDefault:"https://dashboard.ticketsbot.cloud"` FrontpageUrl string `env:"FRONTPAGE_URL" envDefault:"https://ticketsbot.cloud"` + InviteUrl string `env:"INVITE_URL" envDefault:"https://discord.com/oauth2/authorize?client_id=1325579039888511056&scope=bot+applications.commands&permissions=395942816984"` VoteUrl1 string `env:"VOTE_URL_1" envDefault:"https://top.gg/bot/1325579039888511056/vote"` VoteUrl2 string `env:"VOTE_URL_2" envDefault:"https://discordbotlist.com/bots/tickets-v2/upvote"` PoweredBy string `env:"POWEREDBY" envDefault:"[ticketsbot.cloud](https://ticketsbot.cloud)"` From a2f256c232c273c7151ab836ce74ef0f492b3796 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 1 Jun 2025 10:28:26 +0100 Subject: [PATCH 03/13] cv2 Signed-off-by: Ben --- bot/command/impl/settings/autocloseconfigure.go | 9 ++++++++- bot/command/impl/settings/autocloseexclude.go | 9 ++++++++- bot/command/impl/tags/tag.go | 2 +- go.mod | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/bot/command/impl/settings/autocloseconfigure.go b/bot/command/impl/settings/autocloseconfigure.go index 7727f4d..a0908aa 100644 --- a/bot/command/impl/settings/autocloseconfigure.go +++ b/bot/command/impl/settings/autocloseconfigure.go @@ -8,6 +8,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -31,5 +32,11 @@ func (c AutoCloseConfigureCommand) GetExecutor() interface{} { } func (AutoCloseConfigureCommand) Execute(ctx registry.CommandContext) { - ctx.Reply(customisation.Green, i18n.TitleAutoclose, i18n.MessageAutoCloseConfigure, ctx.GuildId()) + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleAutoclose), ctx.GetMessage(i18n.MessageAutoCloseConfigure), ctx.PremiumTier()), + ), + ), + ) } diff --git a/bot/command/impl/settings/autocloseexclude.go b/bot/command/impl/settings/autocloseexclude.go index 7352290..98207ac 100644 --- a/bot/command/impl/settings/autocloseexclude.go +++ b/bot/command/impl/settings/autocloseexclude.go @@ -9,6 +9,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -48,5 +49,11 @@ func (AutoCloseExcludeCommand) Execute(ctx registry.CommandContext) { return } - ctx.Reply(customisation.Green, i18n.TitleAutoclose, i18n.MessageAutoCloseExclude) + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleAutoclose), ctx.GetMessage(i18n.MessageAutoCloseExclude), ctx.PremiumTier()), + ), + ), + ) } diff --git a/bot/command/impl/tags/tag.go b/bot/command/impl/tags/tag.go index 37b5027..ab3799a 100644 --- a/bot/command/impl/tags/tag.go +++ b/bot/command/impl/tags/tag.go @@ -118,7 +118,7 @@ func (TagCommand) AutoCompleteHandler(data interaction.ApplicationCommandAutoCom ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate context defer cancel() - tagIds, err := dbclient.Client.Tag.GetStartingWith(ctx, data.GuildId.Value, value, 25) + tagIds, err := dbclient.Client.Tag.GetContaining(ctx, data.GuildId.Value, value, 25) if err != nil { sentry.Error(err) // TODO: Error context return nil diff --git a/go.mod b/go.mod index 0af6544..ecab9ea 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.8 toolchain go1.24.2 -// replace github.com/TicketsBot-cloud/database => ../database +replace github.com/TicketsBot-cloud/database => ../database replace github.com/TicketsBot-cloud/gdl => ../gdl replace github.com/TicketsBot-cloud/archiverclient => ../archiverclient replace github.com/TicketsBot-cloud/logarchiver => ../logarchiver From 3bad395aa1b7956163eafa64ff57cb5a81f2eb9a Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 14 Jun 2025 12:51:28 +0100 Subject: [PATCH 04/13] update more components Signed-off-by: Ben --- bot/button/handlers/addadmin.go | 9 +- bot/button/handlers/addsupport.go | 9 +- bot/button/handlers/claim.go | 20 +- bot/button/handlers/close.go | 29 ++- bot/button/handlers/closerequestaccept.go | 5 +- bot/button/handlers/closerequestdeny.go | 4 +- bot/button/handlers/viewsurvey.go | 29 ++- .../context/applicationcommandcontext.go | 1 + bot/command/context/autoclosecontext.go | 1 + bot/command/context/buttoncontext.go | 1 + .../context/messagecomponentextensions.go | 16 +- bot/command/context/modalcontext.go | 1 + bot/command/context/panelcontext.go | 1 + bot/command/context/replyable.go | 68 ++---- bot/command/context/selectmenucontext.go | 1 + bot/command/impl/admin/admin.go | 1 + bot/command/impl/admin/admindebug.go | 203 +++++++++++++++++ .../impl/admin/adminlistguildentitlements.go | 2 +- .../impl/admin/adminlistuserentitlements.go | 2 +- bot/command/impl/admin/adminwhitelabeldata.go | 36 +++- bot/command/impl/general/invite.go | 2 +- bot/command/impl/general/jumptotop.go | 2 +- bot/command/impl/general/vote.go | 2 +- bot/command/impl/settings/addadmin.go | 11 +- bot/command/impl/settings/addsupport.go | 11 +- bot/command/impl/settings/blacklist.go | 37 +++- bot/command/impl/settings/language.go | 26 ++- bot/command/impl/settings/panel.go | 5 +- bot/command/impl/settings/premium.go | 119 +++++----- bot/command/impl/settings/removeadmin.go | 13 +- bot/command/impl/settings/removesupport.go | 13 +- bot/command/impl/settings/setup/auto.go | 21 +- bot/command/impl/statistics/statsserver.go | 204 +++++++++++------- bot/command/impl/statistics/statsuser.go | 8 +- bot/command/impl/tags/managetagsadd.go | 9 +- bot/command/impl/tags/tag.go | 44 ++-- bot/command/impl/tags/tagalias.go | 34 +-- bot/command/impl/tickets/closerequest.go | 26 +-- bot/command/impl/tickets/rename.go | 9 +- bot/command/impl/tickets/startticket.go | 18 +- bot/command/impl/tickets/switchpanel.go | 24 +-- bot/command/messageresponse.go | 34 --- bot/command/registry/commandcontext.go | 8 +- bot/errorcontext/workererrorcontext.go | 1 + bot/listeners/guildcreate.go | 32 +-- bot/logic/close.go | 17 +- bot/logic/closeembed.go | 102 +++++++++ bot/logic/open.go | 16 +- bot/logic/reopen.go | 6 +- bot/logic/welcomemessage.go | 138 ++++++++---- bot/model/model.go | 6 + bot/premium/command.go | 16 +- bot/utils/messageutils.go | 90 +++----- event/caller.go | 15 ++ event/commandexecutor.go | 4 +- event/httplisten.go | 1 - restwrapper.go | 14 -- 57 files changed, 1006 insertions(+), 571 deletions(-) create mode 100644 bot/command/impl/admin/admindebug.go create mode 100644 bot/model/model.go diff --git a/bot/button/handlers/addadmin.go b/bot/button/handlers/addadmin.go index f77b9dc..29db678 100644 --- a/bot/button/handlers/addadmin.go +++ b/bot/button/handlers/addadmin.go @@ -15,7 +15,6 @@ import ( "github.com/TicketsBot-cloud/gdl/rest/request" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" - "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" @@ -119,8 +118,12 @@ func (h *AddAdminHandler) Execute(ctx *context.ButtonContext) { return } - e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleAddAdmin, i18n.MessageAddAdminSuccess, nil) - ctx.Edit(command.NewEphemeralEmbedMessageResponse(e)) + ctx.EditWithComponentsOnly(utils.Slice(utils.BuildContainerRaw( + ctx.GetColour(customisation.Green), + ctx.GetMessage(i18n.TitleAddAdmin), + ctx.GetMessage(i18n.MessageAddAdminSuccess), + ctx.PremiumTier(), + ))) settings, err := ctx.Settings() if err != nil { diff --git a/bot/button/handlers/addsupport.go b/bot/button/handlers/addsupport.go index 65ba2a6..1983691 100644 --- a/bot/button/handlers/addsupport.go +++ b/bot/button/handlers/addsupport.go @@ -16,7 +16,6 @@ import ( "github.com/TicketsBot-cloud/gdl/rest/request" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" - "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" cmdregistry "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" @@ -121,8 +120,12 @@ func (h *AddSupportHandler) Execute(ctx *context.ButtonContext) { return } - e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleAddSupport, i18n.MessageAddSupportSuccess, nil) - ctx.Edit(command.NewEphemeralEmbedMessageResponse(e)) + ctx.EditWithComponentsOnly(utils.Slice(utils.BuildContainerRaw( + ctx.GetColour(customisation.Green), + ctx.GetMessage(i18n.TitleAddSupport), + ctx.GetMessage(i18n.MessageAddSupportSuccess), + ctx.PremiumTier(), + ))) updateChannelPermissions(ctx, id, mentionableType) } diff --git a/bot/button/handlers/claim.go b/bot/button/handlers/claim.go index 5bfb8cb..d93d408 100644 --- a/bot/button/handlers/claim.go +++ b/bot/button/handlers/claim.go @@ -63,15 +63,17 @@ func (h *ClaimHandler) Execute(ctx *context.ButtonContext) { } res := command.MessageIntoMessageResponse(ctx.Interaction.Message) - if len(res.Components) > 0 && res.Components[0].Type == component.ComponentActionRow { - row := res.Components[0].ComponentData.(component.ActionRow) - if len(row.Components) > 1 { - row.Components = row.Components[:len(row.Components)-1] - } - - res.Components[0] = component.Component{ - Type: component.ComponentActionRow, - ComponentData: row, + for i, comp := range res.Components { + if comp.Type == component.ComponentActionRow { + row := comp.ComponentData.(component.ActionRow) + if len(row.Components) > 1 { + row.Components = row.Components[:len(row.Components)-1] + } + res.Components[i] = component.Component{ + Type: component.ComponentActionRow, + ComponentData: row, + } + break } } diff --git a/bot/button/handlers/close.go b/bot/button/handlers/close.go index ac6ac32..5b8ecbc 100644 --- a/bot/button/handlers/close.go +++ b/bot/button/handlers/close.go @@ -1,7 +1,8 @@ package handlers import ( - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" + "fmt" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" @@ -56,23 +57,17 @@ func (h *CloseHandler) Execute(ctx *cmdcontext.ButtonContext) { } if closeConfirmation { - // Send confirmation message - confirmEmbed := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleCloseConfirmation, i18n.MessageCloseConfirmation, nil) - confirmEmbed.SetAuthor(ctx.InteractionUser().Username, "", utils.Ptr(ctx.InteractionUser()).AvatarUrl(256)) - - msgData := command.MessageResponse{ - Embeds: []*embed.Embed{confirmEmbed}, - Components: []component.Component{ - component.BuildActionRow(component.BuildButton(component.Button{ - Label: ctx.GetMessage(i18n.TitleClose), - CustomId: "close_confirm", - Style: component.ButtonStylePrimary, - Emoji: utils.BuildEmoji("āœ”ļø"), - })), - }, - } + container := utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleCloseConfirmation, ctx.PremiumTier(), []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("<@%d>: %s", ctx.InteractionUser().Id, ctx.GetMessage(i18n.MessageCloseConfirmation))}), + component.BuildActionRow(component.BuildButton(component.Button{ + Label: ctx.GetMessage(i18n.TitleClose), + CustomId: "close_confirm", + Style: component.ButtonStylePrimary, + Emoji: utils.BuildEmoji("āœ”ļø"), + })), + }) - if _, err := ctx.ReplyWith(msgData); err != nil { + if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(container))); err != nil { ctx.HandleError(err) return } diff --git a/bot/button/handlers/closerequestaccept.go b/bot/button/handlers/closerequestaccept.go index 8c737e8..af85ffe 100644 --- a/bot/button/handlers/closerequestaccept.go +++ b/bot/button/handlers/closerequestaccept.go @@ -1,7 +1,6 @@ package handlers import ( - "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" "github.com/TicketsBot-cloud/worker/bot/command" @@ -57,9 +56,7 @@ func (h *CloseRequestAcceptHandler) Execute(ctx *context.ButtonContext) { return } - ctx.Edit(command.MessageResponse{ - Embeds: utils.Slice(utils.BuildEmbedRaw(customisation.DefaultColours[customisation.Green], "Close Request", "Closing ticket...", nil, premium.Whitelabel)), // TODO: Translations, calculate premium level - }) + ctx.Edit(command.NewMessageResponseWithComponents(utils.Slice(utils.BuildContainerRaw(ctx.GetColour(customisation.Green), "Close Request", "Closing ticket...", ctx.PremiumTier())))) // Avoid users cant close issue // Allow members to close too, for context menu tickets diff --git a/bot/button/handlers/closerequestdeny.go b/bot/button/handlers/closerequestdeny.go index 44e49d1..962c0e3 100644 --- a/bot/button/handlers/closerequestdeny.go +++ b/bot/button/handlers/closerequestdeny.go @@ -50,7 +50,5 @@ func (h *CloseRequestDenyHandler) Execute(ctx *context.ButtonContext) { return } - ctx.Edit(command.MessageResponse{ - Embeds: utils.Embeds(utils.BuildEmbed(ctx, customisation.Red, i18n.TitleCloseRequest, i18n.MessageCloseRequestDenied, nil, ctx.UserId())), - }) + ctx.Edit(command.NewMessageResponseWithComponents(utils.Slice(utils.BuildContainer(ctx, customisation.Red, i18n.TitleCloseRequest, i18n.MessageCloseRequestDenied, ctx.UserId())))) } diff --git a/bot/button/handlers/viewsurvey.go b/bot/button/handlers/viewsurvey.go index 610b9ac..b30397f 100644 --- a/bot/button/handlers/viewsurvey.go +++ b/bot/button/handlers/viewsurvey.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "regexp" "strconv" "strings" @@ -8,10 +9,10 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" + "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" @@ -85,7 +86,7 @@ func (h *ViewSurveyHandler) Execute(ctx *context.ButtonContext) { } if len(surveyResponse.Responses) == 0 { - ctx.ReplyRaw(customisation.Red, "Error", "No survey surveyResponse has been recorded for this ticket.") // TODO: i18n + ctx.ReplyRaw(customisation.Red, "Error", "No survey responses have been recorded for this ticket.") // TODO: i18n return } @@ -95,10 +96,7 @@ func (h *ViewSurveyHandler) Execute(ctx *context.ButtonContext) { return } - e := embed.NewEmbed(). - SetTitle("Exit Survey"). // TODO: i18n - SetAuthor(opener.Username, "", opener.AvatarUrl(256)). - SetColor(ctx.GetColour(customisation.Green)) + tds := "" for _, answer := range surveyResponse.Responses { var title string @@ -115,16 +113,27 @@ func (h *ViewSurveyHandler) Execute(ctx *context.ButtonContext) { response = "No response" } - e.AddField(title, response, false) + tds += fmt.Sprintf("**%s:** %s\n", title, response) } var buttons []component.Component buttons = append(buttons, logic.TranscriptLinkElement(ticket.HasTranscript)(ctx.Worker(), ticket)...) buttons = append(buttons, logic.ThreadLinkElement(ticket.ChannelId != nil && ticket.IsThread)(ctx.Worker(), ticket)...) + comps := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("## Exit Survey for %s", opener.GlobalName)}), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: tds, + }), + } + if len(buttons) > 0 { - ctx.ReplyWithEmbedAndComponents(e, utils.Slice(component.BuildActionRow(buttons...))) - } else { - ctx.ReplyWithEmbed(e) + comps = append(comps, component.BuildActionRow(buttons...)) } + + ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), + Components: comps, + })))) } diff --git a/bot/command/context/applicationcommandcontext.go b/bot/command/context/applicationcommandcontext.go index 07e08aa..8e4835e 100644 --- a/bot/command/context/applicationcommandcontext.go +++ b/bot/command/context/applicationcommandcontext.go @@ -113,6 +113,7 @@ func (c *SlashCommandContext) ToErrorContext() errorcontext.WorkerErrorContext { Guild: c.GuildId(), User: c.Interaction.Member.User.Id, Channel: c.ChannelId(), + Command: c.Interaction.Data.Name, } } diff --git a/bot/command/context/autoclosecontext.go b/bot/command/context/autoclosecontext.go index d5cdb85..aee95e5 100644 --- a/bot/command/context/autoclosecontext.go +++ b/bot/command/context/autoclosecontext.go @@ -86,6 +86,7 @@ func (c *AutoCloseContext) ToErrorContext() errorcontext.WorkerErrorContext { Guild: c.guildId, User: c.userId, Channel: c.channelId, + Command: "autoclose", } } diff --git a/bot/command/context/buttoncontext.go b/bot/command/context/buttoncontext.go index bd7c7c2..4b60f05 100644 --- a/bot/command/context/buttoncontext.go +++ b/bot/command/context/buttoncontext.go @@ -104,6 +104,7 @@ func (c *ButtonContext) ToErrorContext() errorcontext.WorkerErrorContext { Guild: c.GuildId(), User: c.UserId(), Channel: c.ChannelId(), + Command: c.InteractionData.CustomId, } } diff --git a/bot/command/context/messagecomponentextensions.go b/bot/command/context/messagecomponentextensions.go index 1fe611d..3efb31a 100644 --- a/bot/command/context/messagecomponentextensions.go +++ b/bot/command/context/messagecomponentextensions.go @@ -66,26 +66,30 @@ func (e *MessageComponentExtensions) Edit(data command.MessageResponse) { func (e *MessageComponentExtensions) EditWith(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) { e.Edit(command.MessageResponse{ - Embeds: utils.Slice(utils.BuildEmbed(e.ctx, colour, title, content, nil, format...)), + Components: utils.Slice(utils.BuildContainer(e.ctx, colour, title, content, format...)), + }) +} + +func (e *MessageComponentExtensions) EditWithComponentsOnly(components []component.Component) { + e.Edit(command.MessageResponse{ + Components: components, }) } func (e *MessageComponentExtensions) EditWithRaw(colour customisation.Colour, title, content string) { e.Edit(command.MessageResponse{ - Embeds: utils.Slice(utils.BuildEmbedRaw(e.ctx.GetColour(colour), title, content, nil, e.ctx.PremiumTier())), + Components: utils.Slice(utils.BuildContainerRaw(e.ctx.GetColour(colour), title, content, e.ctx.PremiumTier())), }) } func (e *MessageComponentExtensions) EditWithComponents(colour customisation.Colour, title, content i18n.MessageId, components []component.Component, format ...interface{}) { e.Edit(command.MessageResponse{ - Embeds: utils.Slice(utils.BuildEmbed(e.ctx, colour, title, content, nil, format...)), - Components: components, + Components: append(utils.Slice(utils.BuildContainer(e.ctx, colour, title, content, format...)), components...), }) } func (e *MessageComponentExtensions) EditWithComponentsRaw(colour customisation.Colour, title, content string, components []component.Component) { e.Edit(command.MessageResponse{ - Embeds: utils.Slice(utils.BuildEmbedRaw(e.ctx.GetColour(colour), title, content, nil, e.ctx.PremiumTier())), - Components: components, + Components: append(utils.Slice(utils.BuildContainerRaw(e.ctx.GetColour(colour), title, content, e.ctx.PremiumTier())), components...), }) } diff --git a/bot/command/context/modalcontext.go b/bot/command/context/modalcontext.go index ee0bc8b..00b9900 100644 --- a/bot/command/context/modalcontext.go +++ b/bot/command/context/modalcontext.go @@ -129,6 +129,7 @@ func (c *ModalContext) ToErrorContext() errorcontext.WorkerErrorContext { Guild: c.GuildId(), User: c.UserId(), Channel: c.ChannelId(), + Command: c.Interaction.Data.CustomId, } } diff --git a/bot/command/context/panelcontext.go b/bot/command/context/panelcontext.go index caf5871..41bb5f7 100644 --- a/bot/command/context/panelcontext.go +++ b/bot/command/context/panelcontext.go @@ -96,6 +96,7 @@ func (c *PanelContext) ToErrorContext() errorcontext.WorkerErrorContext { Guild: c.guildId, User: c.userId, Channel: c.channelId, + Command: "panel", // This is a panel context, so we use "panel" as the command } } diff --git a/bot/command/context/replyable.go b/bot/command/context/replyable.go index 8b047ee..4b4624d 100644 --- a/bot/command/context/replyable.go +++ b/bot/command/context/replyable.go @@ -10,7 +10,6 @@ import ( permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/permission" @@ -19,6 +18,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/logic" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" @@ -54,59 +54,39 @@ func (r *Replyable) GetColour(colour customisation.Colour) int { return r.colourCodes[colour] } -func (r *Replyable) buildEmbed(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) *embed.Embed { - return utils.BuildEmbed(r.ctx, colour, title, content, fields, format...) -} - -func (r *Replyable) buildEmbedRaw(colour customisation.Colour, title, content string, fields ...embed.EmbedField) *embed.Embed { - return utils.BuildEmbedRaw(r.GetColour(colour), title, content, fields, r.ctx.PremiumTier()) -} - func (r *Replyable) Reply(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) { - embed := r.buildEmbed(colour, title, content, nil, format...) - _, _ = r.ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(embed)) + container := utils.BuildContainerRaw(r.GetColour(colour), r.GetMessage(title), r.GetMessage(content, format...), r.ctx.PremiumTier()) + _, _ = r.ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(container))) } func (r *Replyable) ReplyPermanent(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) { - embed := r.buildEmbed(colour, title, content, nil, format...) - _, _ = r.ctx.ReplyWith(command.NewEmbedMessageResponse(embed)) + container := utils.BuildContainerRaw(r.GetColour(colour), r.GetMessage(title), r.GetMessage(content, format...), r.ctx.PremiumTier()) + _, _ = r.ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(container))) } -func (r *Replyable) ReplyWithEmbed(embed *embed.Embed) { - _, _ = r.ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(embed)) +func (r *Replyable) ReplyWithFields(colour customisation.Colour, title, content i18n.MessageId, fields []model.Field, format ...interface{}) { + container := utils.BuildContainerWithFields(r.ctx, colour, title, content, fields, format...) + _, _ = r.ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(container))) } -func (r *Replyable) ReplyWithEmbedAndComponents(embed *embed.Embed, components []component.Component) { - _, _ = r.ctx.ReplyWith(command.NewEphemeralEmbedMessageResponseWithComponents(embed, components)) -} - -func (r *Replyable) ReplyWithEmbedPermanent(embed *embed.Embed) { - _, _ = r.ctx.ReplyWith(command.NewEmbedMessageResponse(embed)) -} - -func (r *Replyable) ReplyWithFields(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) { - embed := r.buildEmbed(colour, title, content, fields, format...) - _, _ = r.ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(embed)) -} - -func (r *Replyable) ReplyWithFieldsPermanent(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) { - embed := r.buildEmbed(colour, title, content, fields, format...) - _, _ = r.ctx.ReplyWith(command.NewEmbedMessageResponse(embed)) +func (r *Replyable) ReplyWithFieldsPermanent(colour customisation.Colour, title, content i18n.MessageId, fields []model.Field, format ...interface{}) { + container := utils.BuildContainerWithFields(r.ctx, colour, title, content, fields, format...) + _, _ = r.ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(container))) } func (r *Replyable) ReplyRaw(colour customisation.Colour, title, content string) { - embed := r.buildEmbedRaw(colour, title, content) - _, _ = r.ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(embed)) + container := utils.BuildContainerRaw(r.GetColour(colour), title, content, r.ctx.PremiumTier()) + _, _ = r.ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(container))) } func (r *Replyable) ReplyRawWithComponents(colour customisation.Colour, title, content string, components ...component.Component) { - embed := r.buildEmbedRaw(colour, title, content) - _, _ = r.ctx.ReplyWith(command.NewEphemeralEmbedMessageResponseWithComponents(embed, components)) + container := utils.BuildContainerRaw(r.GetColour(colour), title, content, r.ctx.PremiumTier()) + _, _ = r.ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(append(utils.Slice(container), components...))) } func (r *Replyable) ReplyRawPermanent(colour customisation.Colour, title, content string) { - embed := r.buildEmbedRaw(colour, title, content) - _, _ = r.ctx.ReplyWith(command.NewEmbedMessageResponse(embed)) + container := utils.BuildContainerRaw(r.GetColour(colour), title, content, r.ctx.PremiumTier()) + _, _ = r.ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(container))) } func (r *Replyable) ReplyPlain(content string) { @@ -171,7 +151,7 @@ func (r *Replyable) SelectValidEmoji(customEmoji customisation.CustomEmoji, fall func (r *Replyable) buildErrorResponse(err error, eventId string, includeInviteLink bool) command.MessageResponse { var message string - var imageUrl *string + // var imageUrl *string var restError request.RestError if errors.As(err, &restError) { @@ -209,15 +189,11 @@ func (r *Replyable) buildErrorResponse(err error, eventId string, includeInviteL message = fmt.Sprintf("An error occurred while performing this action.\nError ID: `%s`", eventId) } - embed := r.buildEmbedRaw(customisation.Red, r.GetMessage(i18n.Error), message) - if imageUrl != nil { - embed.SetImage(*imageUrl) - } - - res := command.NewEphemeralEmbedMessageResponse(embed) + container := utils.BuildContainerRaw(r.GetColour(customisation.Red), r.GetMessage(i18n.Error), message, r.ctx.PremiumTier()) + res := command.NewEphemeralMessageResponseWithComponents(utils.Slice(container)) if includeInviteLink { - res.Components = []component.Component{ + res.Components = append(res.Components, component.BuildActionRow( component.BuildButton(component.Button{ Label: r.GetMessage(i18n.MessageJoinSupportServer), @@ -226,7 +202,7 @@ func (r *Replyable) buildErrorResponse(err error, eventId string, includeInviteL Url: utils.Ptr(strings.ReplaceAll(config.Conf.Bot.SupportServerInvite, "\n", "")), }), ), - } + ) } return res diff --git a/bot/command/context/selectmenucontext.go b/bot/command/context/selectmenucontext.go index 8336a02..e6c111c 100644 --- a/bot/command/context/selectmenucontext.go +++ b/bot/command/context/selectmenucontext.go @@ -104,6 +104,7 @@ func (c *SelectMenuContext) ToErrorContext() errorcontext.WorkerErrorContext { Guild: c.GuildId(), User: c.UserId(), Channel: c.ChannelId(), + Command: c.InteractionData.CustomId, } } diff --git a/bot/command/impl/admin/admin.go b/bot/command/impl/admin/admin.go index a68016b..09f269c 100644 --- a/bot/command/impl/admin/admin.go +++ b/bot/command/impl/admin/admin.go @@ -22,6 +22,7 @@ func (AdminCommand) Properties() registry.Properties { AdminBlacklistCommand{}, AdminCheckBlacklistCommand{}, AdminCheckPremiumCommand{}, + AdminDebugCommand{}, AdminGenPremiumCommand{}, AdminGetOwnerCommand{}, AdminListGuildEntitlementsCommand{}, diff --git a/bot/command/impl/admin/admindebug.go b/bot/command/impl/admin/admindebug.go new file mode 100644 index 0000000..7d8b3cf --- /dev/null +++ b/bot/command/impl/admin/admindebug.go @@ -0,0 +1,203 @@ +package admin + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/TicketsBot-cloud/common/permission" + "github.com/TicketsBot-cloud/common/premium" + "github.com/TicketsBot-cloud/gdl/objects/application" + "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" + "github.com/TicketsBot-cloud/gdl/rest" + "github.com/TicketsBot-cloud/worker/bot/command" + "github.com/TicketsBot-cloud/worker/bot/command/registry" + "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/utils" + "github.com/TicketsBot-cloud/worker/i18n" +) + +type AdminDebugCommand struct{} + +func (AdminDebugCommand) Properties() registry.Properties { + return registry.Properties{ + Name: "debug", + Description: "Debug command for a guild", + Type: interaction.ApplicationCommandTypeChatInput, + PermissionLevel: permission.Everyone, + Category: command.Settings, + AdminOnly: true, + Arguments: command.Arguments( + command.NewRequiredArgument("guild_id", "ID of the guild", interaction.OptionTypeString, i18n.MessageInvalidArgument), + ), + Timeout: time.Second * 10, + } +} + +func (c AdminDebugCommand) GetExecutor() interface{} { + return c.Execute +} + +func (AdminDebugCommand) Execute(ctx registry.CommandContext, raw string) { + guildId, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + ctx.HandleError(err) + return + } + + guild, err := ctx.Worker().GetGuild(guildId) + if err != nil { + ctx.HandleError(err) + return + } + + settings, err := dbclient.Client.Settings.Get(ctx, guild.Id) + if err != nil { + ctx.HandleError(err) + return + } + + owner, err := ctx.Worker().GetUser(guild.OwnerId) + if err != nil { + ctx.HandleError(err) + return + } + + tier, source, err := utils.PremiumClient.GetTierByGuild(ctx, guild) + if err != nil { + ctx.HandleError(err) + return + } + + panels, err := dbclient.Client.Panel.GetByGuild(ctx, guild.Id) + if err != nil { + ctx.HandleError(err) + return + } + + integrations, err := dbclient.Client.CustomIntegrationGuilds.GetGuildIntegrations(ctx, guild.Id) + if err != nil { + ctx.HandleError(err) + return + } + + importLogs, err := dbclient.Client.ImportLogs.GetRuns(ctx, guild.Id) + if err != nil { + ctx.HandleError(err) + return + } + + botId, botFound, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, guild.Id) + if err != nil { + ctx.HandleError(err) + return + } + + var bInf application.Application + + if botFound { + botDbInfo, err := dbclient.Client.Whitelabel.GetByBotId(ctx, botId) + if err != nil { + ctx.HandleError(err) + return + } + + botInfo, err := rest.GetCurrentApplication(ctx, botDbInfo.Token, nil) + if err != nil { + ctx.HandleError(err) + return + } + + bInf = botInfo + } + + // Helper to get ticket notification channel info + getTicketNotifChannel := func() (string, string) { + if settings.UseThreads && settings.TicketNotificationChannel != nil { + ch, err := ctx.Worker().GetChannel(*settings.TicketNotificationChannel) + if err == nil { + return ch.Name, strconv.FormatUint(ch.Id, 10) + } + } + return "Disabled", "Disabled" + } + + ticketNotifChannelName, ticketNotifChannelId := getTicketNotifChannel() + + ownerId := strconv.FormatUint(owner.Id, 10) + ownerName := owner.Username + + panelLimit := "3" + premiumTier := "None" + premiumSource := "None" + if tier != premium.None { + premiumTier = tier.String() + premiumSource = string(source) + panelLimit = "āˆž" + } + + panelCount := len(panels) + + guildInfo := []string{ + fmt.Sprintf("ID: `%d`", guild.Id), + fmt.Sprintf("Name: `%s`", guild.Name), + fmt.Sprintf("Owner: `%s` (%s)", ownerName, ownerId), + } + if guild.VanityUrlCode != "" { + guildInfo = append(guildInfo, fmt.Sprintf("Vanity URL: `.gg/%s`", guild.VanityUrlCode)) + } + if tier != premium.None { + guildInfo = append(guildInfo, fmt.Sprintf("Premium Tier: `%s`", premiumTier)) + guildInfo = append(guildInfo, fmt.Sprintf("Premium Source: `%s`", premiumSource)) + } + + if botFound { + guildInfo = append(guildInfo, fmt.Sprintf("Whitelabel Bot: `%s` (%d)", bInf.Name, botId)) + } + + settingsInfo := []string{ + fmt.Sprintf("Transcripts Enabled: `%t`", settings.StoreTranscripts), + fmt.Sprintf("Panel Count: `%d/%s`", panelCount, panelLimit), + } + if settings.UseThreads { + settingsInfo = append(settingsInfo, fmt.Sprintf("Thread Mode: `%t`", settings.UseThreads)) + settingsInfo = append(settingsInfo, fmt.Sprintf("Thread Mode Channel: `#%s` (%s)", ticketNotifChannelName, ticketNotifChannelId)) + } + + if len(integrations) > 0 { + enabledIntegrations := make([]string, len(integrations)) + for i, integ := range integrations { + enabledIntegrations[i] = integ.Name + } + settingsInfo = append(settingsInfo, fmt.Sprintf("Enabled Integrations: %d (%s)", len(enabledIntegrations), strings.Join(enabledIntegrations, ", "))) + } + + hasDataRun, hasTranscriptRun := false, false + for _, log := range importLogs { + switch log.RunType { + case "DATA": + hasDataRun = true + case "TRANSCRIPT": + hasTranscriptRun = true + } + } + settingsInfo = append(settingsInfo, fmt.Sprintf("Data Imported: `%t`", hasDataRun)) + settingsInfo = append(settingsInfo, fmt.Sprintf("Transcripts Imported: `%t`", hasTranscriptRun)) + + debugResponse := []string{ + fmt.Sprintf("**Guild Info**\n- %s", strings.Join(guildInfo, "\n- ")), + fmt.Sprintf("**Settings**\n- %s", strings.Join(settingsInfo, "\n- ")), + } + + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainerRaw( + ctx.GetColour(customisation.Orange), + "Admin - Debug", + strings.Join(debugResponse, "\n\n"), + ctx.PremiumTier(), + ), + })) +} diff --git a/bot/command/impl/admin/adminlistguildentitlements.go b/bot/command/impl/admin/adminlistguildentitlements.go index 77dd915..2473a8d 100644 --- a/bot/command/impl/admin/adminlistguildentitlements.go +++ b/bot/command/impl/admin/adminlistguildentitlements.go @@ -114,7 +114,7 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainer( + utils.BuildContainerWithComponents( ctx, customisation.Orange, i18n.Admin, diff --git a/bot/command/impl/admin/adminlistuserentitlements.go b/bot/command/impl/admin/adminlistuserentitlements.go index 09f8f7b..0a83f21 100644 --- a/bot/command/impl/admin/adminlistuserentitlements.go +++ b/bot/command/impl/admin/adminlistuserentitlements.go @@ -68,7 +68,7 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ - utils.BuildContainer( + utils.BuildContainerWithComponents( ctx, customisation.Orange, i18n.Admin, diff --git a/bot/command/impl/admin/adminwhitelabeldata.go b/bot/command/impl/admin/adminwhitelabeldata.go index fb8dcbf..f47b949 100644 --- a/bot/command/impl/admin/adminwhitelabeldata.go +++ b/bot/command/impl/admin/adminwhitelabeldata.go @@ -7,13 +7,14 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -111,14 +112,31 @@ func (AdminWhitelabelDataCommand) Execute(ctx registry.CommandContext, userId ui guildsFormatted = strings.TrimSuffix(guildsFormatted, "\n") } - fields := []embed.EmbedField{ - utils.EmbedFieldRaw("Subscription Tier", tier.String(), true), - utils.EmbedFieldRaw("Bot ID", botIdFormatted, true), - utils.EmbedFieldRaw("Public Key", publicKeyFormatted, true), - utils.EmbedFieldRaw("Guilds", guildsFormatted, true), - utils.EmbedFieldRaw("Last 3 Errors", errorsFormatted, true), - utils.EmbedFieldRaw("Invite Link", fmt.Sprintf("[Click Here](https://discord.com/oauth2/authorize?client_id=%d&scope=bot+applications.commands&permissions=395942816984)", data.BotId), true), + tds := "" + + fields := []model.Field{ + {Name: "Subscription Tier", Value: tier.String()}, + {Name: "Bot ID", Value: botIdFormatted}, + {Name: "Public Key", Value: publicKeyFormatted}, + {Name: "Guilds", Value: guildsFormatted}, + {Name: "Last 3 Errors", Value: errorsFormatted}, + {Name: "Invite Link", Value: fmt.Sprintf("[Click Here](https://discord.com/oauth2/authorize?client_id=%d&scope=bot+applications.commands&permissions=395942816984)", data.BotId)}, + } + + for i := range fields { + tds += fmt.Sprintf("**%s:** %s\n", fields[i].Name, fields[i].Value) + } + + comps := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: "## Whitelabel"}), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: tds, + }), } - ctx.ReplyWithEmbed(utils.BuildEmbedRaw(ctx.GetColour(customisation.Green), "Whitelabel", "", fields, ctx.PremiumTier())) + ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), + Components: comps, + })))) } diff --git a/bot/command/impl/general/invite.go b/bot/command/impl/general/invite.go index 713e3ca..d712d17 100644 --- a/bot/command/impl/general/invite.go +++ b/bot/command/impl/general/invite.go @@ -36,7 +36,7 @@ func (c InviteCommand) GetExecutor() interface{} { func (InviteCommand) Execute(ctx registry.CommandContext) { ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainer(ctx, customisation.Green, i18n.TitleInvite, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleInvite, ctx.PremiumTier(), []component.Component{ component.BuildActionRow(component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.ClickHere), Style: component.ButtonStyleLink, diff --git a/bot/command/impl/general/jumptotop.go b/bot/command/impl/general/jumptotop.go index edb7b50..c0bd526 100644 --- a/bot/command/impl/general/jumptotop.go +++ b/bot/command/impl/general/jumptotop.go @@ -64,7 +64,7 @@ func (JumpToTopCommand) Execute(ctx registry.CommandContext) { } if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainer(ctx, customisation.Green, i18n.TitleJumpToTop, ctx.PremiumTier(), components), + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleJumpToTop, ctx.PremiumTier(), components), })); err != nil { ctx.HandleError(err) return diff --git a/bot/command/impl/general/vote.go b/bot/command/impl/general/vote.go index f7c45c7..db3b8d0 100644 --- a/bot/command/impl/general/vote.go +++ b/bot/command/impl/general/vote.go @@ -95,7 +95,7 @@ func (c VoteCommand) Execute(ctx registry.CommandContext) { } if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainer(ctx, customisation.Green, i18n.TitleVote, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleVote, ctx.PremiumTier(), []component.Component{ componentBody, component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), buildVoteComponent(ctx, true), diff --git a/bot/command/impl/settings/addadmin.go b/bot/command/impl/settings/addadmin.go index 670f71b..1ff4f1d 100644 --- a/bot/command/impl/settings/addadmin.go +++ b/bot/command/impl/settings/addadmin.go @@ -5,13 +5,13 @@ import ( "time" permcache "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -39,10 +39,9 @@ func (c AddAdminCommand) GetExecutor() interface{} { } func (c AddAdminCommand) Execute(ctx registry.CommandContext, id uint64) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/addadmin @User`\n`/addadmin @Role`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/addadmin @User`\n`/addadmin @Role`", } mentionableType, valid := context.DetermineMentionableType(ctx, id) @@ -63,7 +62,7 @@ func (c AddAdminCommand) Execute(ctx registry.CommandContext, id uint64) { // Send confirmation message if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainer(ctx, customisation.Green, i18n.TitleAddAdmin, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddAdmin, ctx.PremiumTier(), []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: ctx.GetMessage(i18n.MessageAddAdminConfirm, mention), }), diff --git a/bot/command/impl/settings/addsupport.go b/bot/command/impl/settings/addsupport.go index a877038..e49358e 100644 --- a/bot/command/impl/settings/addsupport.go +++ b/bot/command/impl/settings/addsupport.go @@ -5,13 +5,13 @@ import ( "time" permcache "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -40,10 +40,9 @@ func (c AddSupportCommand) GetExecutor() interface{} { } func (c AddSupportCommand) Execute(ctx registry.CommandContext, id uint64) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/addsupport @Role`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/addsupport @Role`", } mentionableType, valid := context.DetermineMentionableType(ctx, id) @@ -67,7 +66,7 @@ func (c AddSupportCommand) Execute(ctx registry.CommandContext, id uint64) { // Send confirmation message if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainer(ctx, customisation.Green, i18n.TitleAddSupport, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddSupport, ctx.PremiumTier(), []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: ctx.GetMessage(i18n.MessageAddSupportConfirm, mention), }), diff --git a/bot/command/impl/settings/blacklist.go b/bot/command/impl/settings/blacklist.go index f15dc94..f30d052 100644 --- a/bot/command/impl/settings/blacklist.go +++ b/bot/command/impl/settings/blacklist.go @@ -6,13 +6,13 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/sentry" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -41,10 +41,9 @@ func (c BlacklistCommand) GetExecutor() interface{} { } func (BlacklistCommand) Execute(ctx registry.CommandContext, id uint64) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/blacklist @User`\n`/blacklist @Role`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/blacklist @User`\n`/blacklist @Role`", } mentionableType, valid := context.DetermineMentionableType(ctx, id) @@ -82,13 +81,13 @@ func (BlacklistCommand) Execute(ctx registry.CommandContext, id uint64) { return } + blacklistMsg := i18n.MessageBlacklistRemove + if isBlacklisted { if err := dbclient.Client.Blacklist.Remove(ctx, ctx.GuildId(), id); err != nil { ctx.HandleError(err) return } - - ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistRemove, id) } else { // Limit of 250 *users* count, err := dbclient.Client.Blacklist.GetBlacklistedCount(ctx, ctx.GuildId()) @@ -106,9 +105,16 @@ func (BlacklistCommand) Execute(ctx registry.CommandContext, id uint64) { ctx.HandleError(err) return } - - ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistAdd, member.User.Id) + blacklistMsg = i18n.MessageBlacklistAdd } + + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleBlacklist), ctx.GetMessage(blacklistMsg, id), ctx.PremiumTier()), + ), + ), + ) } else if mentionableType == context.MentionableTypeRole { // Check if role is staff isSupport, err := dbclient.Client.RolePermissions.IsSupport(ctx, id) @@ -140,13 +146,13 @@ func (BlacklistCommand) Execute(ctx registry.CommandContext, id uint64) { return } + blacklistMsg := i18n.MessageBlacklistRemoveRole + if isBlacklisted { if err := dbclient.Client.RoleBlacklist.Remove(ctx, ctx.GuildId(), id); err != nil { ctx.HandleError(err) return } - - ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistRemoveRole, id) } else { // Limit of 50 *roles* count, err := dbclient.Client.Blacklist.GetBlacklistedCount(ctx, ctx.GuildId()) @@ -164,9 +170,18 @@ func (BlacklistCommand) Execute(ctx registry.CommandContext, id uint64) { ctx.HandleError(err) return } + blacklistMsg = i18n.MessageBlacklistAddRole ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistAddRole, id) } + + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleBlacklist), ctx.GetMessage(blacklistMsg, id), ctx.PremiumTier()), + ), + ), + ) } else { ctx.HandleError(fmt.Errorf("infallible")) return diff --git a/bot/command/impl/settings/language.go b/bot/command/impl/settings/language.go index cd21a4e..b4719c4 100644 --- a/bot/command/impl/settings/language.go +++ b/bot/command/impl/settings/language.go @@ -2,7 +2,7 @@ package settings import ( "fmt" - "io/ioutil" + "io" "math" "strings" "time" @@ -46,7 +46,7 @@ func (c *LanguageCommand) Execute(ctx registry.CommandContext) { } bar := progressbar.NewOptions(100, - progressbar.OptionSetWriter(ioutil.Discard), + progressbar.OptionSetWriter(io.Discard), progressbar.OptionSetWidth(15), progressbar.OptionSetPredictTime(false), progressbar.OptionSetRenderBlankState(true), @@ -65,11 +65,23 @@ func (c *LanguageCommand) Execute(ctx registry.CommandContext) { languageList = strings.TrimSuffix(languageList, "\n") - helpWanted := utils.EmbedField(ctx.GuildId(), "ā„¹ļø Help Wanted", i18n.MessageLanguageHelpWanted, true) - e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleLanguage, i18n.MessageLanguageCommand, utils.ToSlice(helpWanted), languageList) - res := command.NewEphemeralEmbedMessageResponseWithComponents(e, buildComponents(ctx)) - - _, _ = ctx.ReplyWith(res) + innerComponents := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageLanguageCommand, languageList), + }), + } + innerComponents = append(innerComponents, buildComponents(ctx)...) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{ + Content: ctx.GetMessage(i18n.MessageLanguageHelpWanted), + })) + + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(utils.BuildContainerWithComponents( + ctx, + customisation.Green, + i18n.TitleLanguage, + ctx.PremiumTier(), + innerComponents, + )))) } func buildComponents(ctx registry.CommandContext) []component.Component { diff --git a/bot/command/impl/settings/panel.go b/bot/command/impl/settings/panel.go index 63e7d14..deb644a 100644 --- a/bot/command/impl/settings/panel.go +++ b/bot/command/impl/settings/panel.go @@ -8,6 +8,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -31,5 +32,7 @@ func (c PanelCommand) GetExecutor() interface{} { } func (PanelCommand) Execute(ctx registry.CommandContext) { - ctx.Reply(customisation.Green, i18n.TitlePanel, i18n.MessagePanel, ctx.GuildId()) + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitlePanel), ctx.GetMessage(i18n.MessagePanel, ctx.GuildId()), ctx.PremiumTier()), + ))) } diff --git a/bot/command/impl/settings/premium.go b/bot/command/impl/settings/premium.go index f413bb8..f8f0c48 100644 --- a/bot/command/impl/settings/premium.go +++ b/bot/command/impl/settings/premium.go @@ -6,7 +6,6 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" @@ -84,10 +83,22 @@ func (PremiumCommand) Execute(ctx registry.CommandContext) { }, buttons...) } - ctx.ReplyWith(command.NewEphemeralEmbedMessageResponseWithComponents( - utils.BuildEmbed(ctx, customisation.Green, i18n.TitlePremium, content, nil), - utils.Slice(component.BuildActionRow(buttons...)), - )) + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice( + utils.BuildContainerWithComponents( + ctx, + customisation.Green, + i18n.TitlePremium, + ctx.PremiumTier(), + []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: ctx.GetMessage(content)}), + component.BuildActionRow(buttons...), + }, + ), + ), + ), + ) } else { var patreonEmoji, discordEmoji, keyEmoji *emoji.Emoji @@ -97,51 +108,61 @@ func (PremiumCommand) Execute(ctx registry.CommandContext) { keyEmoji = utils.BuildEmoji("šŸ”‘") } - fields := utils.Slice(embed.EmbedField{ - Name: ctx.GetMessage(i18n.MessagePremiumAlreadyPurchasedTitle), - Value: ctx.GetMessage(i18n.MessagePremiumAlreadyPurchasedDescription), - Inline: false, - }) - - ctx.ReplyWith(command.NewEphemeralEmbedMessageResponseWithComponents( - utils.BuildEmbed(ctx, customisation.Green, i18n.TitlePremium, i18n.MessagePremiumAbout, fields), - utils.Slice( - component.BuildActionRow( - component.BuildSelectMenu(component.SelectMenu{ - CustomId: "premium_purchase_method", - Options: utils.Slice( - component.SelectOption{ - Label: "Patreon", // Don't translate - Value: "patreon", - Description: ctx.GetMessage(i18n.MessagePremiumMethodSelectorPatreon), - Emoji: patreonEmoji, - }, - component.SelectOption{ - Label: "Discord", - Value: "discord", - Description: ctx.GetMessage(i18n.MessagePremiumMethodSelectorDiscord), - Emoji: discordEmoji, - }, - component.SelectOption{ - Label: ctx.GetMessage(i18n.MessagePremiumGiveawayKey), - Value: "key", - Description: ctx.GetMessage(i18n.MessagePremiumMethodSelectorKey), - Emoji: keyEmoji, - }, - ), - Placeholder: ctx.GetMessage(i18n.MessagePremiumMethodSelector), - Disabled: false, - }), - ), - component.BuildActionRow( - component.BuildButton(component.Button{ - Label: ctx.GetMessage(i18n.Website), - Style: component.ButtonStyleLink, - Emoji: utils.BuildEmoji("šŸ”—"), - Url: utils.Ptr(fmt.Sprintf("%s/premium", config.Conf.Bot.FrontpageUrl)), - }), + components := []component.Component{ + component.BuildSection(component.Section{ + Components: utils.Slice( + component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("## %s", ctx.GetMessage(i18n.TitlePremium))}), ), + Accessory: component.BuildButton(component.Button{ + Label: "Purchase", + Style: component.ButtonStyleLink, + Emoji: utils.BuildEmoji("šŸ”—"), + Url: utils.Ptr(fmt.Sprintf("%s/premium", config.Conf.Bot.FrontpageUrl)), + }), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{Content: ctx.GetMessage(i18n.MessagePremiumAbout)}), + component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("-# **%s** %s", ctx.GetMessage(i18n.MessagePremiumAlreadyPurchasedTitle), ctx.GetMessage(i18n.MessagePremiumAlreadyPurchasedDescription))}), + component.BuildActionRow( + component.BuildSelectMenu(component.SelectMenu{ + CustomId: "premium_purchase_method", + Options: utils.Slice( + component.SelectOption{ + Label: "Patreon", // Don't translate + Value: "patreon", + Description: ctx.GetMessage(i18n.MessagePremiumMethodSelectorPatreon), + Emoji: patreonEmoji, + }, + component.SelectOption{ + Label: "Discord", + Value: "discord", + Description: ctx.GetMessage(i18n.MessagePremiumMethodSelectorDiscord), + Emoji: discordEmoji, + }, + component.SelectOption{ + Label: ctx.GetMessage(i18n.MessagePremiumGiveawayKey), + Value: "key", + Description: ctx.GetMessage(i18n.MessagePremiumMethodSelectorKey), + Emoji: keyEmoji, + }, + ), + Placeholder: ctx.GetMessage(i18n.MessagePremiumMethodSelector), + Disabled: false, + }), + ), + } + + if ctx.PremiumTier() == premium.None { + components = utils.AddPremiumFooter(components) + } + + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice(component.BuildContainer(component.Container{ + Components: components, + AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), + })), ), - )) + ) } } diff --git a/bot/command/impl/settings/removeadmin.go b/bot/command/impl/settings/removeadmin.go index 3e22ab8..2898bce 100644 --- a/bot/command/impl/settings/removeadmin.go +++ b/bot/command/impl/settings/removeadmin.go @@ -6,7 +6,6 @@ import ( permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/permission" "github.com/TicketsBot-cloud/worker/bot/command" @@ -14,6 +13,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -41,10 +41,9 @@ func (c RemoveAdminCommand) GetExecutor() interface{} { // TODO: Remove from existing tickets func (c RemoveAdminCommand) Execute(ctx registry.CommandContext, id uint64) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/removeadmin @User`\n`/removeadmin @Role`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/removeadmin @User`\n`/removeadmin @Role`", } settings, err := ctx.Settings() @@ -101,7 +100,9 @@ func (c RemoveAdminCommand) Execute(ctx registry.CommandContext, id uint64) { return } - ctx.Reply(customisation.Green, i18n.TitleRemoveAdmin, i18n.MessageRemoveAdminSuccess) + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleRemoveAdmin), ctx.GetMessage(i18n.MessageRemoveAdminSuccess), ctx.PremiumTier()), + ))) // Remove user / role from thread notification channel if settings.TicketNotificationChannel != nil { diff --git a/bot/command/impl/settings/removesupport.go b/bot/command/impl/settings/removesupport.go index ffbdd7b..aab5b38 100644 --- a/bot/command/impl/settings/removesupport.go +++ b/bot/command/impl/settings/removesupport.go @@ -6,7 +6,6 @@ import ( permcache "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/permission" "github.com/TicketsBot-cloud/worker/bot/command" @@ -15,6 +14,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/logic" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -42,10 +42,9 @@ func (c RemoveSupportCommand) GetExecutor() interface{} { // TODO: Remove from existing tickets func (c RemoveSupportCommand) Execute(ctx registry.CommandContext, id uint64) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/removesupport @User`\n`/removesupport @Role`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/removesupport @User`\n`/removesupport @Role`", } settings, err := ctx.Settings() @@ -112,7 +111,9 @@ func (c RemoveSupportCommand) Execute(ctx registry.CommandContext, id uint64) { return } - ctx.Reply(customisation.Green, i18n.TitleRemoveSupport, i18n.MessageRemoveSupportSuccess) + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleRemoveSupport), ctx.GetMessage(i18n.MessageRemoveSupportSuccess), ctx.PremiumTier()), + ))) if settings.TicketNotificationChannel != nil { // Remove user / role from thread notification channel diff --git a/bot/command/impl/settings/setup/auto.go b/bot/command/impl/settings/setup/auto.go index 3162a66..69b7528 100644 --- a/bot/command/impl/settings/setup/auto.go +++ b/bot/command/impl/settings/setup/auto.go @@ -7,8 +7,8 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" channel_permissions "github.com/TicketsBot-cloud/gdl/permission" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/worker/bot/command" @@ -81,12 +81,9 @@ func (AutoSetupCommand) Execute(ctx registry.CommandContext) { messageContent = fmt.Sprintf("āŒ %s", i18n.GetMessageFromGuild(ctx.GuildId(), i18n.SetupAutoRolesFailure)) } - embed := embed.NewEmbed(). - SetTitle("Setup"). - SetColor(getColour(context.Background(), ctx.GuildId(), failed)). // TODO: Propagate context - SetDescription(messageContent) + c := utils.BuildContainerRaw(getColour(context.Background(), ctx.GuildId(), failed), "Setup", messageContent, ctx.PremiumTier()) - ctx.ReplyWithEmbed(embed) + ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(c))) // create transcripts channel switch transcriptChannel, err := ctx.Worker().CreateGuildChannel(ctx.GuildId(), getTranscriptChannelData(ctx.GuildId(), supportRoleId, adminRoleId)); err { @@ -101,10 +98,10 @@ func (AutoSetupCommand) Execute(ctx registry.CommandContext) { messageContent += fmt.Sprintf("\nāŒ %s", i18n.GetMessageFromGuild(ctx.GuildId(), i18n.SetupAutoTranscriptChannelFailure)) } - embed.SetDescription(messageContent) + c = utils.BuildContainerRaw(getColour(context.Background(), ctx.GuildId(), failed), "Setup", messageContent, ctx.PremiumTier()) shouldEdit := true - if err := edit(interaction, embed); err != nil { + if err := edit(interaction, c); err != nil { shouldEdit = false } @@ -130,17 +127,17 @@ func (AutoSetupCommand) Execute(ctx registry.CommandContext) { // update status if shouldEdit { - embed.SetDescription(messageContent) + c := utils.BuildContainerRaw(getColour(context.Background(), ctx.GuildId(), failed), "Setup", messageContent, ctx.PremiumTier()) - if err := edit(interaction, embed); err != nil { + if err := edit(interaction, c); err != nil { shouldEdit = false } } } -func edit(ctx *cmdcontext.SlashCommandContext, e *embed.Embed) error { +func edit(ctx *cmdcontext.SlashCommandContext, c component.Component) error { data := rest.WebhookEditBody{ - Embeds: utils.Slice(e), + Components: utils.Slice(c), } _, err := rest.EditOriginalInteractionResponse(context.Background(), ctx.Interaction.Token, ctx.Worker().RateLimiter, ctx.Worker().BotId, data) diff --git a/bot/command/impl/statistics/statsserver.go b/bot/command/impl/statistics/statsserver.go index 215512f..7a8dd12 100644 --- a/bot/command/impl/statistics/statsserver.go +++ b/bot/command/impl/statistics/statsserver.go @@ -9,16 +9,13 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" - "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" "github.com/getsentry/sentry-go" - "github.com/jedib0t/go-pretty/v6/table" - "github.com/jedib0t/go-pretty/v6/text" - "golang.org/x/sync/errgroup" ) type StatsServerCommand struct { @@ -31,7 +28,7 @@ func (StatsServerCommand) Properties() registry.Properties { Type: interaction.ApplicationCommandTypeChatInput, PermissionLevel: permission.Support, Category: command.Statistics, - PremiumOnly: true, + PremiumOnly: false, DefaultEphemeral: true, Timeout: time.Second * 10, } @@ -46,104 +43,104 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { span.SetTag("guild", strconv.FormatUint(ctx.GuildId(), 10)) defer span.Finish() - group, _ := errgroup.WithContext(ctx) + // group, _ := errgroup.WithContext(ctx) var totalTickets, openTickets uint64 // totalTickets - group.Go(func() (err error) { - span := sentry.StartSpan(span.Context(), "GetTotalTicketCount") - defer span.Finish() - - totalTickets, err = dbclient.Analytics.GetTotalTicketCount(ctx, ctx.GuildId()) - return - }) - - // openTickets - group.Go(func() error { - span := sentry.StartSpan(span.Context(), "GetGuildOpenTickets") - defer span.Finish() - - tickets, err := dbclient.Client.Tickets.GetGuildOpenTickets(ctx, ctx.GuildId()) - if err != nil { - return err - } + // group.Go(func() (err error) { + // span := sentry.StartSpan(span.Context(), "GetTotalTicketCount") + // defer span.Finish() + + // totalTickets, err = dbclient.Analytics.GetTotalTicketCount(ctx, ctx.GuildId()) + // return + // }) + + // // openTickets + // group.Go(func() error { + // span := sentry.StartSpan(span.Context(), "GetGuildOpenTickets") + // defer span.Finish() + + // tickets, err := dbclient.Client.Tickets.GetGuildOpenTickets(ctx, ctx.GuildId()) + // if err != nil { + // return err + // } - openTickets = uint64(len(tickets)) - return nil - }) + // openTickets = uint64(len(tickets)) + // return nil + // }) var feedbackRating float64 var feedbackCount uint64 - group.Go(func() (err error) { - span := sentry.StartSpan(span.Context(), "GetAverageFeedbackRating") - defer span.Finish() + // group.Go(func() (err error) { + // span := sentry.StartSpan(span.Context(), "GetAverageFeedbackRating") + // defer span.Finish() - feedbackRating, err = dbclient.Analytics.GetAverageFeedbackRatingGuild(ctx, ctx.GuildId()) - return - }) + // feedbackRating, err = dbclient.Analytics.GetAverageFeedbackRatingGuild(ctx, ctx.GuildId()) + // return + // }) - group.Go(func() (err error) { - span := sentry.StartSpan(span.Context(), "GetFeedbackCount") - defer span.Finish() + // group.Go(func() (err error) { + // span := sentry.StartSpan(span.Context(), "GetFeedbackCount") + // defer span.Finish() - feedbackCount, err = dbclient.Analytics.GetFeedbackCountGuild(ctx, ctx.GuildId()) - return - }) + // feedbackCount, err = dbclient.Analytics.GetFeedbackCountGuild(ctx, ctx.GuildId()) + // return + // }) // first response times var firstResponseTime analytics.TripleWindow - group.Go(func() (err error) { - span := sentry.StartSpan(span.Context(), "GetFirstResponseTimeStats") - defer span.Finish() + // group.Go(func() (err error) { + // span := sentry.StartSpan(span.Context(), "GetFirstResponseTimeStats") + // defer span.Finish() - firstResponseTime, err = dbclient.Analytics.GetFirstResponseTimeStats(ctx, ctx.GuildId()) - return - }) + // firstResponseTime, err = dbclient.Analytics.GetFirstResponseTimeStats(ctx, ctx.GuildId()) + // return + // }) // ticket duration var ticketDuration analytics.TripleWindow - group.Go(func() (err error) { - span := sentry.StartSpan(span.Context(), "GetTicketDurationStats") - defer span.Finish() + // group.Go(func() (err error) { + // span := sentry.StartSpan(span.Context(), "GetTicketDurationStats") + // defer span.Finish() - ticketDuration, err = dbclient.Analytics.GetTicketDurationStats(ctx, ctx.GuildId()) - return - }) + // ticketDuration, err = dbclient.Analytics.GetTicketDurationStats(ctx, ctx.GuildId()) + // return + // }) // tickets per day var ticketVolumeTable string - group.Go(func() error { - span := sentry.StartSpan(span.Context(), "GetLastNTicketsPerDayGuild") - defer span.Finish() + // group.Go(func() error { + // span := sentry.StartSpan(span.Context(), "GetLastNTicketsPerDayGuild") + // defer span.Finish() - counts, err := dbclient.Analytics.GetLastNTicketsPerDayGuild(ctx, ctx.GuildId(), 7) - if err != nil { - return err - } + // counts, err := dbclient.Analytics.GetLastNTicketsPerDayGuild(ctx, ctx.GuildId(), 7) + // if err != nil { + // return err + // } - tw := table.NewWriter() - tw.SetStyle(table.StyleLight) - tw.Style().Format.Header = text.FormatDefault + // tw := table.NewWriter() + // tw.SetStyle(table.StyleLight) + // tw.Style().Format.Header = text.FormatDefault - tw.AppendHeader(table.Row{"Date", "Ticket Volume"}) - for _, count := range counts { - tw.AppendRow(table.Row{count.Date.Format("2006-01-02"), count.Count}) - } + // tw.AppendHeader(table.Row{"Date", "Ticket Volume"}) + // for _, count := range counts { + // tw.AppendRow(table.Row{count.Date.Format("2006-01-02"), count.Count}) + // } - ticketVolumeTable = tw.Render() - return nil - }) + // ticketVolumeTable = tw.Render() + // return nil + // }) - if err := group.Wait(); err != nil { - ctx.HandleError(err) - return - } + // if err := group.Wait(); err != nil { + // ctx.HandleError(err) + // return + // } span = sentry.StartSpan(span.Context(), "Send Message") - msgEmbed := embed.NewEmbed(). + embed.NewEmbed(). SetTitle("Statistics"). SetColor(ctx.GetColour(customisation.Green)). AddField("Total Tickets", strconv.FormatUint(totalTickets, 10), true). @@ -160,10 +157,71 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { AddField("Average Ticket Duration (Weekly)", formatNullableTime(ticketDuration.Weekly), true). AddField("Ticket Volume", fmt.Sprintf("```\n%s\n```", ticketVolumeTable), false) - _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) + innerComponents := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: "### Statistics", + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("**Average First Response Time**\n%s", formatAlignedStats([]string{"Weekly", "Monthly", "All Time"}, []string{"1h", "1h", "1h"})), + }), + } + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + Components: innerComponents, + })))) + + // _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) span.Finish() } func formatNullableTime(duration *time.Duration) string { return utils.FormatNullableTime(duration) } + +func formatAlignedStats(header []string, values []string) string { + maxWidths := make([]int, len(header)) + for i := range header { + hLen := len(header[i]) + vLen := len(values[i]) + maxWidths[i] = max(vLen, hLen) + } + + sep := " / " + + // Build header line + headerLine := "" + for i, h := range header { + headerLine += centerText(h, maxWidths[i]) + if i < len(header)-1 { + headerLine += sep + } + } + + // Build value line + valueLine := "" + for i, v := range values { + valueLine += centerText(v, maxWidths[i]) + if i < len(values)-1 { + valueLine += sep + } + } + + return headerLine + "\n" + valueLine +} + +func centerText(s string, width int) string { + pad := width - len(s) + if pad <= 0 { + return s + } + left := pad / 2 + right := pad - left + return spaces(left) + s + spaces(right) +} + +func spaces(count int) string { + if count <= 0 { + return "" + } + return fmt.Sprintf("%*s", count, "") +} diff --git a/bot/command/impl/statistics/statsuser.go b/bot/command/impl/statistics/statsuser.go index 4e4a28d..a829c95 100644 --- a/bot/command/impl/statistics/statsuser.go +++ b/bot/command/impl/statistics/statsuser.go @@ -114,7 +114,7 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { span := sentry.StartSpan(span.Context(), "Reply") - msgEmbed := embed.NewEmbed(). + _ = embed.NewEmbed(). SetTitle("Statistics"). SetColor(ctx.GetColour(customisation.Green)). SetAuthor(member.User.Username, "", member.User.AvatarUrl(256)). @@ -124,7 +124,7 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { AddField("Total Tickets", strconv.Itoa(totalTickets), true). AddField("Open Tickets", fmt.Sprintf("%d / %d", openTickets, ticketLimit), true) - _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) + // _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) span.Finish() } else { // Support rep stats group, _ := errgroup.WithContext(ctx) @@ -275,7 +275,7 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { span := sentry.StartSpan(span.Context(), "Reply") - msgEmbed := embed.NewEmbed(). + _ = embed.NewEmbed(). SetTitle("Statistics"). SetColor(ctx.GetColour(customisation.Green)). SetAuthor(member.User.Username, "", member.User.AvatarUrl(256)). @@ -292,7 +292,7 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { AddField("Claimed Tickets (Monthly)", strconv.Itoa(monthlyClaimedTickets), true). AddField("Claimed Tickets (Total)", strconv.Itoa(totalClaimedTickets), true) - _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) + // _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) span.Finish() } } diff --git a/bot/command/impl/tags/managetagsadd.go b/bot/command/impl/tags/managetagsadd.go index 4fab99b..84c0ca5 100644 --- a/bot/command/impl/tags/managetagsadd.go +++ b/bot/command/impl/tags/managetagsadd.go @@ -5,12 +5,12 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/database" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -41,10 +41,9 @@ func (c ManageTagsAddCommand) GetExecutor() interface{} { } func (ManageTagsAddCommand) Execute(ctx registry.CommandContext, tagId, content string) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/managetags add [TagID] [Tag Contents]`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/managetags add [TagID] [Tag Contents]`", } // Limit of 200 tags diff --git a/bot/command/impl/tags/tag.go b/bot/command/impl/tags/tag.go index ab3799a..0c9b332 100644 --- a/bot/command/impl/tags/tag.go +++ b/bot/command/impl/tags/tag.go @@ -4,18 +4,18 @@ import ( "context" "time" - "github.com/TicketsBot-cloud/common/model" + model2 "github.com/TicketsBot-cloud/common/model" "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" - "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/logic" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -43,10 +43,9 @@ func (c TagCommand) GetExecutor() interface{} { } func (TagCommand) Execute(ctx registry.CommandContext, tagId string) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/tag [TagID]`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/tag [TagID]`", } tag, ok, err := dbclient.Client.Tag.Get(ctx, ctx.GuildId(), tagId) @@ -71,27 +70,24 @@ func (TagCommand) Execute(ctx registry.CommandContext, tagId string) { content = logic.DoPlaceholderSubstitutions(ctx, content, ctx.Worker(), ticket, nil) } - var embeds []*embed.Embed - if tag.Embed != nil { - embeds = []*embed.Embed{ - logic.BuildCustomEmbed(ctx, ctx.Worker(), ticket, *tag.Embed.CustomEmbed, tag.Embed.Fields, false, nil), - } - } + var components []component.Component - var allowedMentions message.AllowedMention - if ticket.Id != 0 { - allowedMentions = message.AllowedMention{ - Users: []uint64{ticket.UserId}, + if content != "" { + components = []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: content, + }), } } - data := command.MessageResponse{ - Content: content, - Embeds: embeds, - AllowedMentions: allowedMentions, + if tag.Embed != nil && tag.Embed.CustomEmbed != nil { + c := logic.BuildCustomContainer(ctx, ctx.Worker(), ticket, *tag.Embed.CustomEmbed, tag.Embed.Fields, false, nil) + if c != nil { + components = append(components, *c) + } } - if _, err := ctx.ReplyWith(data); err != nil { + if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents(components)); err != nil { ctx.HandleError(err) return } @@ -102,12 +98,12 @@ func (TagCommand) Execute(ctx registry.CommandContext, tagId string) { sentry.ErrorWithContext(err, ctx.ToErrorContext()) } - if err := dbclient.Client.Tickets.SetStatus(ctx, ctx.GuildId(), ticket.Id, model.TicketStatusPending); err != nil { + if err := dbclient.Client.Tickets.SetStatus(ctx, ctx.GuildId(), ticket.Id, model2.TicketStatusPending); err != nil { sentry.ErrorWithContext(err, ctx.ToErrorContext()) } if !ticket.IsThread && ctx.PremiumTier() > premium.None { - if err := dbclient.Client.CategoryUpdateQueue.Add(ctx, ctx.GuildId(), ticket.Id, model.TicketStatusPending); err != nil { + if err := dbclient.Client.CategoryUpdateQueue.Add(ctx, ctx.GuildId(), ticket.Id, model2.TicketStatusPending); err != nil { sentry.ErrorWithContext(err, ctx.ToErrorContext()) } } diff --git a/bot/command/impl/tags/tagalias.go b/bot/command/impl/tags/tagalias.go index 46760fb..0967ae8 100644 --- a/bot/command/impl/tags/tagalias.go +++ b/bot/command/impl/tags/tagalias.go @@ -7,9 +7,8 @@ import ( "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" - "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" @@ -70,27 +69,28 @@ func (c TagAliasCommand) Execute(ctx registry.CommandContext) { content = logic.DoPlaceholderSubstitutions(ctx, content, ctx.Worker(), ticket, nil) } - var embeds []*embed.Embed - if c.tag.Embed != nil { - embeds = []*embed.Embed{ - logic.BuildCustomEmbed(ctx, ctx.Worker(), ticket, *c.tag.Embed.CustomEmbed, c.tag.Embed.Fields, false, nil), - } - } + var components []component.Component - var allowedMentions message.AllowedMention - if ticket.Id != 0 { - allowedMentions = message.AllowedMention{ - Users: []uint64{ticket.UserId}, + if content != "" { + components = []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: content, + }), } } - data := command.MessageResponse{ - Content: content, - Embeds: embeds, - AllowedMentions: allowedMentions, + if c.tag.Embed != nil { + components = append(components, *logic.BuildCustomContainer(ctx, ctx.Worker(), ticket, *c.tag.Embed.CustomEmbed, c.tag.Embed.Fields, false, nil)) } - if _, err := ctx.ReplyWith(data); err != nil { + // var allowedMentions message.AllowedMention + // if ticket.Id != 0 { + // allowedMentions = message.AllowedMention{ + // Users: []uint64{ticket.UserId}, + // } + // } + + if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents(components)); err != nil { ctx.HandleError(err) return } diff --git a/bot/command/impl/tickets/closerequest.go b/bot/command/impl/tickets/closerequest.go index 9e80914..53ee822 100644 --- a/bot/command/impl/tickets/closerequest.go +++ b/bot/command/impl/tickets/closerequest.go @@ -9,8 +9,6 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/database" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" - "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" @@ -90,7 +88,7 @@ func (CloseRequestCommand) Execute(ctx registry.CommandContext, closeDelay *int, format = []interface{}{ctx.UserId(), strings.ReplaceAll(*reason, "`", "\\`")} } - msgEmbed := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleCloseRequest, messageId, nil, format...) + msgContainer := utils.BuildContainer(ctx, customisation.Green, i18n.TitleCloseRequest, messageId, format...) components := component.BuildActionRow( component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.MessageCloseRequestAccept), @@ -107,16 +105,20 @@ func (CloseRequestCommand) Execute(ctx registry.CommandContext, closeDelay *int, }), ) - data := command.MessageResponse{ - Content: fmt.Sprintf("<@%d>", ticket.UserId), - Embeds: []*embed.Embed{msgEmbed}, - AllowedMentions: message.AllowedMention{ - Users: []uint64{ticket.UserId}, - }, - Components: []component.Component{components}, - } + // data := command.MessageResponse{ + // AllowedMentions: message.AllowedMention{ + // Users: []uint64{ticket.UserId}, + // }, + + // } - if _, err := ctx.ReplyWith(data); err != nil { + if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("<@%d>", ticket.UserId), + }), + msgContainer, + components, + })); err != nil { ctx.HandleError(err) return } diff --git a/bot/command/impl/tickets/rename.go b/bot/command/impl/tickets/rename.go index 1c1bab1..3895fab 100644 --- a/bot/command/impl/tickets/rename.go +++ b/bot/command/impl/tickets/rename.go @@ -4,13 +4,13 @@ import ( "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/redis" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" @@ -39,10 +39,9 @@ func (c RenameCommand) GetExecutor() interface{} { } func (RenameCommand) Execute(ctx registry.CommandContext, name string) { - usageEmbed := embed.EmbedField{ - Name: "Usage", - Value: "`/rename [ticket-name]`", - Inline: false, + usageEmbed := model.Field{ + Name: "Usage", + Value: "`/rename [ticket-name]`", } ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) diff --git a/bot/command/impl/tickets/startticket.go b/bot/command/impl/tickets/startticket.go index a814c68..c8f2a55 100644 --- a/bot/command/impl/tickets/startticket.go +++ b/bot/command/impl/tickets/startticket.go @@ -9,6 +9,7 @@ import ( "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/gdl/rest/request" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/context" @@ -111,12 +112,15 @@ func sendTicketStartedFromMessage(ctx registry.CommandContext, ticket database.T messageLink := fmt.Sprintf("https://discord.com/channels/%d/%d/%d", ctx.GuildId(), ctx.ChannelId(), msg.Id) contentFormatted := strings.ReplaceAll(utils.StringMax(msg.Content, 2048, "..."), "`", "\\`") - msgEmbed := utils.BuildEmbed( - ctx, customisation.Green, i18n.Ticket, i18n.MessageTicketStartedFrom, nil, + msgEmbed := utils.BuildContainer( + ctx, customisation.Green, i18n.Ticket, i18n.MessageTicketStartedFrom, messageLink, msg.Author.Id, ctx.ChannelId(), contentFormatted, ) - if _, err := ctx.Worker().CreateMessageEmbed(*ticket.ChannelId, msgEmbed); err != nil { + if _, err := ctx.Worker().CreateMessageComplex(*ticket.ChannelId, rest.CreateMessageData{ + Components: utils.Slice(msgEmbed), + Flags: message.SumFlags(message.FlagComponentsV2), + }); err != nil { ctx.HandleError(err) return } @@ -180,9 +184,13 @@ func sendMovedMessage(ctx registry.CommandContext, ticket database.Ticket, msg m FailIfNotExists: false, } - msgEmbed := utils.BuildEmbed(ctx, customisation.Green, i18n.Ticket, i18n.MessageMovedToTicket, nil, *ticket.ChannelId) + msgEmbed := utils.BuildContainer(ctx, customisation.Green, i18n.Ticket, i18n.MessageMovedToTicket, *ticket.ChannelId) - if _, err := ctx.Worker().CreateMessageEmbedReply(msg.ChannelId, msgEmbed, reference); err != nil { + if _, err := ctx.Worker().CreateMessageComplex(msg.ChannelId, rest.CreateMessageData{ + MessageReference: reference, + Components: utils.Slice(msgEmbed), + Flags: message.SumFlags(message.FlagComponentsV2), + }); err != nil { ctx.HandleError(err) return } diff --git a/bot/command/impl/tickets/switchpanel.go b/bot/command/impl/tickets/switchpanel.go index e553e5e..7077f8f 100644 --- a/bot/command/impl/tickets/switchpanel.go +++ b/bot/command/impl/tickets/switchpanel.go @@ -10,8 +10,8 @@ import ( "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/worker/bot/command" cmdcontext "github.com/TicketsBot-cloud/worker/bot/command/context" @@ -110,29 +110,27 @@ func (SwitchPanelCommand) Execute(ctx *cmdcontext.SlashCommandContext, panelId i if err == nil { var subject string - embeds := utils.PtrElems(msg.Embeds) // TODO: Fix types - if len(embeds) == 0 { - embeds = make([]*embed.Embed, 1) + c := msg.Components + + // get the first c where its a Container + + // embeds := utils.PtrElems(msg.Embeds) // TODO: Fix types + if len(c) == 0 { + c = make([]component.Component, 1) subject = "No subject given" - } else { - subject = embeds[0].Title // TODO: Store subjects in database } - embeds[0], err = logic.BuildWelcomeMessageEmbed(ctx.Context, ctx, ticket, subject, &panel, nil) + wm, _, err := logic.BuildWelcomeMessageEmbed(ctx.Context, ctx, ticket, subject, &panel, nil) if err != nil { ctx.HandleError(err) return } - for i := 1; i < len(embeds); i++ { - embeds[i].Color = embeds[0].Color - } + c[0] = *wm editData := rest.EditMessageData{ - Content: msg.Content, - Embeds: embeds, Flags: uint(msg.Flags), - Components: msg.Components, + Components: c, } if _, err = ctx.Worker().EditMessage(ctx.ChannelId(), *ticket.WelcomeMessageId, editData); err != nil { diff --git a/bot/command/messageresponse.go b/bot/command/messageresponse.go index 9e9f8bb..81edfcf 100644 --- a/bot/command/messageresponse.go +++ b/bot/command/messageresponse.go @@ -30,34 +30,6 @@ func NewEphemeralTextMessageResponse(content string) MessageResponse { } } -func NewEmbedMessageResponse(embeds ...*embed.Embed) MessageResponse { - return MessageResponse{ - Embeds: embeds, - } -} - -func NewEmbedMessageResponseWithComponents(e *embed.Embed, components []component.Component) MessageResponse { - return MessageResponse{ - Embeds: []*embed.Embed{e}, - Components: components, - } -} - -func NewEphemeralEmbedMessageResponse(embeds ...*embed.Embed) MessageResponse { - return MessageResponse{ - Embeds: embeds, - Flags: message.SumFlags(message.FlagEphemeral), - } -} - -func NewEphemeralEmbedMessageResponseWithComponents(e *embed.Embed, components []component.Component) MessageResponse { - return MessageResponse{ - Embeds: []*embed.Embed{e}, - Flags: message.SumFlags(message.FlagEphemeral), - Components: components, - } -} - func NewEphemeralMessageResponseWithComponents(components []component.Component) MessageResponse { return MessageResponse{ Flags: message.SumFlags(message.FlagEphemeral, message.FlagComponentsV2), @@ -148,16 +120,10 @@ func (r *MessageResponse) IntoUpdateMessageResponse() (res interaction.ResponseU func MessageIntoMessageResponse(msg message.Message) MessageResponse { // TODO: Fix types - embeds := make([]*embed.Embed, len(msg.Embeds)) - for i, embed := range msg.Embeds { - embed := embed // Loop values are pointers in Go and so change with each iteration - embeds[i] = &embed - } return MessageResponse{ Tts: msg.Tts, Content: msg.Content, - Embeds: embeds, AllowedMentions: message.AllowedMention{}, Flags: uint(msg.Flags), Components: msg.Components, diff --git a/bot/command/registry/commandcontext.go b/bot/command/registry/commandcontext.go index c3a88b2..a9a09de 100644 --- a/bot/command/registry/commandcontext.go +++ b/bot/command/registry/commandcontext.go @@ -5,7 +5,6 @@ import ( "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/guild" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" @@ -16,6 +15,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/errorcontext" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/i18n" "golang.org/x/net/context" ) @@ -37,11 +37,9 @@ type CommandContext interface { Reply(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) ReplyWith(response command.MessageResponse) (message.Message, error) - ReplyWithEmbed(embed *embed.Embed) - ReplyWithEmbedPermanent(embed *embed.Embed) ReplyPermanent(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) - ReplyWithFields(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) - ReplyWithFieldsPermanent(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) + ReplyWithFields(colour customisation.Colour, title, content i18n.MessageId, fields []model.Field, format ...interface{}) + ReplyWithFieldsPermanent(colour customisation.Colour, title, content i18n.MessageId, fields []model.Field, format ...interface{}) ReplyRaw(colour customisation.Colour, title, content string) ReplyRawPermanent(colour customisation.Colour, title, content string) diff --git a/bot/errorcontext/workererrorcontext.go b/bot/errorcontext/workererrorcontext.go index 814a7a4..fe869ea 100644 --- a/bot/errorcontext/workererrorcontext.go +++ b/bot/errorcontext/workererrorcontext.go @@ -8,6 +8,7 @@ type WorkerErrorContext struct { Guild uint64 User uint64 Channel uint64 + Command string } func (w WorkerErrorContext) ToMap() map[string]string { diff --git a/bot/listeners/guildcreate.go b/bot/listeners/guildcreate.go index 14f969b..687a115 100644 --- a/bot/listeners/guildcreate.go +++ b/bot/listeners/guildcreate.go @@ -5,10 +5,11 @@ import ( "fmt" "time" + "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/gdl/gateway/payloads/events" "github.com/TicketsBot-cloud/gdl/objects/auditlog" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" + "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/guild" "github.com/TicketsBot-cloud/gdl/permission" "github.com/TicketsBot-cloud/gdl/rest" @@ -17,6 +18,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/metrics/statsd" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/config" ) @@ -71,18 +73,22 @@ func sendIntroMessage(ctx context.Context, worker *worker.Context, guild guild.G return } - msg := embed.NewEmbed(). - SetTitle("Tickets"). - SetDescription(fmt.Sprintf("Thank you for inviting Tickets to your server! Below is a quick guide on setting up the bot, please don't hesitate to contact us in our [support server](%s) if you need any assistance!", config.Conf.Bot.SupportServerInvite)). - SetColor(customisation.GetColourOrDefault(ctx, guild.Id, customisation.Green)). - AddField("Setup", fmt.Sprintf("You can setup the bot using `/setup`, or you can use the [web dashboard](%s) which has additional options", config.Conf.Bot.DashboardUrl), false). - AddField("Ticket Panels", fmt.Sprintf("Ticket panels are a commonly used feature of the bot. You can read about them [here](%s/panels), or create one on the [web dashboard](%s/manage/%d/panels)", config.Conf.Bot.FrontpageUrl, config.Conf.Bot.DashboardUrl, guild.Id), false). - AddField("Adding Staff", "To allow staff to answer tickets, you must let the bot know about them first. You can do this through\n`/addsupport [@User / @Role]` and `/addadmin [@User / @Role]`. While both Support and Admin can access the dashboard, Bot Admins can change the settings of the bot.", false). - AddField("Tags", fmt.Sprintf("Tags are predefined tickets of text which you can access through a simple command. You can learn more about them [here](%s/tags).", config.Conf.Bot.FrontpageUrl), false). - AddField("Claiming", fmt.Sprintf("Tickets can be claimed by your staff such that other staff members cannot also reply to the ticket. You can learn more about claiming [here](%s/claiming).", config.Conf.Bot.FrontpageUrl), false). - AddField("Additional Support", fmt.Sprintf("If you are still confused, we welcome you to our [support server](%s). Cheers.", config.Conf.Bot.SupportServerInvite), false) - - _, _ = worker.CreateMessageEmbed(channel.Id, msg) + // worker.CreateMessageComplex() + + content := fmt.Sprintf("Thank you for inviting Tickets to your server! Below is a quick guide on setting up the bot, please don't hesitate to contact us in our [support server](%s) if you need any assistance!\n", config.Conf.Bot.SupportServerInvite) + content += fmt.Sprintf("**Setup**:\nYou can setup the bot using `/setup`, or you can use the [web dashboard](%s) which has additional options\n", config.Conf.Bot.DashboardUrl) + content += fmt.Sprintf("**Ticket Panels**:\nTicket panels are a commonly used feature of the bot. You can read about them [here](%s/panels), or create one on the [web dashboard](%s/manage/%d/panels)\n", config.Conf.Bot.FrontpageUrl, config.Conf.Bot.DashboardUrl, guild.Id) + content += "**Adding Staff**:\nTo allow staff to answer tickets, you must let the bot know about them first. You can do this through\n`/addsupport [@User / @Role]` and `/addadmin [@User / @Role]`. While both Support and Admin can access the dashboard, Bot Admins can change the settings of the bot.\n" + content += fmt.Sprintf("**Tags**:\nTags are predefined tickets of text which you can access through a simple command. You can learn more about them [here](%s/tags).\n", config.Conf.Bot.FrontpageUrl) + content += fmt.Sprintf("**Claiming**:\nTickets can be claimed by your staff such that other staff members cannot also reply to the ticket. You can learn more about claiming [here](%s/claiming).\n", config.Conf.Bot.FrontpageUrl) + content += fmt.Sprintf("**Additional Support**:\nIf you are still confused, we welcome you to our [support server](%s). Cheers.", config.Conf.Bot.SupportServerInvite) + + container := utils.BuildContainerRaw(customisation.GetColourOrDefault(ctx, guild.Id, customisation.Green), "Tickets", content, premium.Premium) + + _, _ = worker.CreateMessageComplex(channel.Id, rest.CreateMessageData{ + Components: utils.Slice(container), + Flags: message.SumFlags(message.FlagComponentsV2), + }) } func getInviter(worker *worker.Context, guildId uint64) (userId uint64) { diff --git a/bot/logic/close.go b/bot/logic/close.go index 8fecb76..71b482f 100644 --- a/bot/logic/close.go +++ b/bot/logic/close.go @@ -11,7 +11,6 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/member" "github.com/TicketsBot-cloud/gdl/rest" @@ -20,6 +19,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/metrics/statsd" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/bot/redis" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" @@ -175,11 +175,10 @@ func CloseTicket(ctx context.Context, cmd registry.CommandContext, reason *strin if reason == nil { cmd.ReplyPermanent(customisation.Green, i18n.TitleTicketClosed, i18n.MessageCloseSuccess, cmd.UserId()) } else { - fields := []embed.EmbedField{ + fields := []model.Field{ { - Name: cmd.GetMessage(i18n.Reason), - Value: fmt.Sprintf("```%s```", *reason), - Inline: false, + Name: cmd.GetMessage(i18n.Reason), + Value: fmt.Sprintf("```%s```", *reason), }, } @@ -257,11 +256,13 @@ func sendCloseEmbed(ctx context.Context, cmd registry.CommandContext, errorConte }, } - closeEmbed, closeComponents := BuildCloseEmbed(ctx, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) + // closeEmbed, closeComponents := BuildCloseEmbed(ctx, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) + closeContainer := BuildCloseContainer(ctx, cmd, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) data := rest.CreateMessageData{ - Embeds: utils.Slice(closeEmbed), - Components: closeComponents, + // Embeds: utils.Slice(closeEmbed), + Flags: uint(message.FlagComponentsV2), + Components: utils.Slice(*closeContainer), } msg, err := cmd.Worker().CreateMessageComplex(*archiveChannelId, data) diff --git a/bot/logic/closeembed.go b/bot/logic/closeembed.go index 6dd854f..de4f1f6 100644 --- a/bot/logic/closeembed.go +++ b/bot/logic/closeembed.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "strconv" + "strings" + "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" @@ -13,6 +15,7 @@ import ( "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/worker" + "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/utils" @@ -196,6 +199,105 @@ func BuildCloseEmbed( return closeEmbed, rows } +func BuildCloseContainer( + ctx context.Context, + cmd registry.CommandContext, + worker *worker.Context, + ticket database.Ticket, + closedBy uint64, + reason *string, + rating *uint8, + components [][]CloseEmbedElement, +) *component.Component { + var formattedReason string + if reason == nil { + formattedReason = "No reason specified" + } else { + formattedReason = *reason + if len(formattedReason) > 1024 { + formattedReason = formattedReason[:1024] + } + } + + var transcriptEmoji *emoji.Emoji + if !worker.IsWhitelabel { + transcriptEmoji = customisation.EmojiTranscript.BuildEmoji() + } + + var claimedBy string + { + claimUserId, err := dbclient.Client.TicketClaims.Get(ctx, ticket.GuildId, ticket.Id) + if err != nil { + sentry.Error(err) + } + + if claimUserId == 0 { + claimedBy = "" + } else { + claimedBy = fmt.Sprintf("<@%d>", claimUserId) + } + } + + section1Text := []string{ + formatRow("Ticket ID", strconv.Itoa(ticket.Id)), + formatRow("Opened By", fmt.Sprintf("<@%d>", ticket.UserId)), + formatRow("Closed By", fmt.Sprintf("<@%d>", closedBy)), + } + + section2Text := []string{ + formatRow("Open Time", message.BuildTimestamp(ticket.OpenTime, message.TimestampStyleShortDateTime)), + } + + if ticket.CloseTime != nil { + section2Text = append(section2Text, formatRow("Close Time", message.BuildTimestamp(*ticket.CloseTime, message.TimestampStyleShortDateTime))) + } + + if claimedBy != "" { + section2Text = append(section2Text, formatRow("Claimed By", claimedBy)) + } + + if rating != nil { + section2Text = append(section2Text, formatRow("Rating", strings.Repeat("⭐", int(*rating)))) + } + + if reason != nil { + section2Text = append(section2Text, formatRow("Reason", formattedReason)) + } + + transcriptLink := fmt.Sprintf("%s/manage/%d/transcripts/view/%d", config.Conf.Bot.DashboardUrl, ticket.GuildId, ticket.Id) + cc := []component.Component{ + component.BuildSection(component.Section{ + Accessory: component.BuildButton(component.Button{ + Label: "View Transcript", + Style: component.ButtonStyleLink, + Emoji: transcriptEmoji, + Url: utils.Ptr(transcriptLink), + }), + Components: utils.Slice(component.BuildTextDisplay(component.TextDisplay{ + Content: "## Ticket Closed", + })), + }), + component.BuildTextDisplay(component.TextDisplay{Content: strings.Join(section1Text, "\n")}), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{Content: strings.Join(section2Text, "\n")}), + } + + if cmd.PremiumTier() == premium.None { + cc = utils.AddPremiumFooter(cc) + } + + container := component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(cmd.GetColour(customisation.Green)), + Components: cc, + }) + + return &container +} + +func formatRow(title, content string) string { + return fmt.Sprintf("` ā ` **%s:** %s", title, content) +} + func formatTitle(s string, emoji customisation.CustomEmoji, isWhitelabel bool) string { if !isWhitelabel { return fmt.Sprintf("%s %s", emoji, s) diff --git a/bot/logic/open.go b/bot/logic/open.go index cf13641..a805ad3 100644 --- a/bot/logic/open.go +++ b/bot/logic/open.go @@ -1081,10 +1081,12 @@ func buildJoinThreadMessage( title = "Ticket Reopened" } - e := utils.BuildEmbedRaw(customisation.GetColourOrDefault(ctx, guildId, colour), title, fmt.Sprintf("%s with ID: %d has been opened. Press the button below to join it.", name, ticketId), nil, premiumTier) - e.AddField(customisation.PrefixWithEmoji("Opened By", customisation.EmojiOpen, !worker.IsWhitelabel), customisation.PrefixWithEmoji(fmt.Sprintf("<@%d>", openerId), customisation.EmojiBulletLine, !worker.IsWhitelabel), true) - e.AddField(customisation.PrefixWithEmoji("Panel", customisation.EmojiPanel, !worker.IsWhitelabel), customisation.PrefixWithEmoji(panelName, customisation.EmojiBulletLine, !worker.IsWhitelabel), true) - e.AddField(customisation.PrefixWithEmoji("Staff In Ticket", customisation.EmojiStaff, !worker.IsWhitelabel), customisation.PrefixWithEmoji(strconv.Itoa(len(staffMembers)), customisation.EmojiBulletLine, !worker.IsWhitelabel), true) + content := fmt.Sprintf("%s with ID: %d has been opened. Press the button below to join it.\n\n", name, ticketId) + content += fmt.Sprintf("**%s**: %s\n", customisation.PrefixWithEmoji("Opened By", customisation.EmojiOpen, !worker.IsWhitelabel), customisation.PrefixWithEmoji(fmt.Sprintf("<@%d>", openerId), customisation.EmojiBulletLine, !worker.IsWhitelabel)) + content += fmt.Sprintf("**%s**: %s\n", customisation.PrefixWithEmoji("Panel", customisation.EmojiPanel, !worker.IsWhitelabel), customisation.PrefixWithEmoji(panelName, customisation.EmojiBulletLine, !worker.IsWhitelabel)) + content += fmt.Sprintf("**%s**: %s\n", customisation.PrefixWithEmoji("Staff In Ticket", customisation.EmojiStaff, !worker.IsWhitelabel), customisation.PrefixWithEmoji(strconv.Itoa(len(staffMembers)), customisation.EmojiBulletLine, !worker.IsWhitelabel)) + + e := utils.BuildContainerRaw(customisation.GetColourOrDefault(ctx, guildId, colour), title, content, premiumTier) if len(staffMembers) > 0 { var mentions []string // dynamic length @@ -1100,12 +1102,12 @@ func buildJoinThreadMessage( charCount += len(mention) + 1 // +1 for space } - e.AddField(customisation.PrefixWithEmoji("Staff Members", customisation.EmojiStaff, !worker.IsWhitelabel), customisation.PrefixWithEmoji(strings.Join(mentions, " "), customisation.EmojiBulletLine, !worker.IsWhitelabel), false) + content += fmt.Sprintf("**%s**: %s\n", customisation.PrefixWithEmoji("Staff Members", customisation.EmojiStaff, !worker.IsWhitelabel), customisation.PrefixWithEmoji(strings.Join(mentions, " "), customisation.EmojiBulletLine, !worker.IsWhitelabel)) } return command.MessageResponse{ - Embeds: utils.Slice(e), - Components: utils.Slice(component.BuildActionRow( + // Embeds: utils.Slice(e), + Components: append(utils.Slice(e), component.BuildActionRow( component.BuildButton(component.Button{ Label: "Join Ticket", CustomId: fmt.Sprintf("join_thread_%d", ticketId), diff --git a/bot/logic/reopen.go b/bot/logic/reopen.go index 44a1e4f..63a1529 100644 --- a/bot/logic/reopen.go +++ b/bot/logic/reopen.go @@ -114,8 +114,10 @@ func ReopenTicket(ctx context.Context, cmd registry.CommandContext, ticketId int cmd.Reply(customisation.Green, i18n.Success, i18n.MessageReopenSuccess, ticket.Id, *ticket.ChannelId) - embedData := utils.BuildEmbed(cmd, customisation.Green, i18n.TitleReopened, i18n.MessageReopenedTicket, nil, cmd.UserId()) - if _, err := cmd.Worker().CreateMessageEmbed(*ticket.ChannelId, embedData); err != nil { + containerData := utils.BuildContainer(cmd, customisation.Green, i18n.TitleReopened, i18n.MessageReopenedTicket, cmd.UserId()) + if _, err := cmd.Worker().CreateMessageComplex(*ticket.ChannelId, rest.CreateMessageData{ + Components: utils.Slice(containerData), + }); err != nil { cmd.HandleError(err) return } diff --git a/bot/logic/welcomemessage.go b/bot/logic/welcomemessage.go index d7c2225..810095c 100644 --- a/bot/logic/welcomemessage.go +++ b/bot/logic/welcomemessage.go @@ -14,6 +14,7 @@ import ( "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" + "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/rest" @@ -23,7 +24,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/integrations" "github.com/TicketsBot-cloud/worker/bot/utils" - "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" "golang.org/x/sync/errgroup" ) @@ -45,28 +45,38 @@ func SendWelcomeMessage( } // Build embeds - welcomeMessageEmbed, err := BuildWelcomeMessageEmbed(ctx, cmd, ticket, subject, panel, additionalPlaceholders) + welcomeMessageEmbed, embedColor, err := BuildWelcomeMessageEmbed(ctx, cmd, ticket, subject, panel, additionalPlaceholders) if err != nil { return 0, err } - embeds := utils.Slice(welcomeMessageEmbed) + embeds := utils.Slice(*welcomeMessageEmbed) // Put form fields in a separate embed fields := getFormDataFields(formData) if len(fields) > 0 { - formAnswersEmbed := embed.NewEmbed(). - SetColor(welcomeMessageEmbed.Color) + fieldStr := "" for _, field := range fields { - formAnswersEmbed.AddField(field.Name, utils.EscapeMarkdown(field.Value), field.Inline) + fieldStr += fmt.Sprintf("**%s**\n%s\n", field.Name, utils.EscapeMarkdown(field.Value)) + } + + components := []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fieldStr, + }), } if cmd.PremiumTier() == premium.None { - formAnswersEmbed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) + components = utils.AddPremiumFooter(components) } - embeds = append(embeds, formAnswersEmbed) + embeds = append(embeds, component.BuildContainer(component.Container{ + AccentColor: &embedColor, + Components: components, + })) + + // embeds = append(embeds, formAnswersEmbed) } buttons := []component.Component{ @@ -94,10 +104,8 @@ func SendWelcomeMessage( } data := rest.CreateMessageData{ - Embeds: embeds, - Components: []component.Component{ - component.BuildActionRow(buttons...), - }, + Components: append(embeds, component.BuildActionRow(buttons...)), + Flags: message.SumFlags(message.FlagComponentsV2), } // Should never happen @@ -121,11 +129,11 @@ func BuildWelcomeMessageEmbed( panel *database.Panel, // Only custom integration placeholders for now - prevent making duplicate requests additionalPlaceholders map[string]string, -) (*embed.Embed, error) { +) (*component.Component, int, error) { if panel == nil || panel.WelcomeMessageEmbed == nil { welcomeMessage, err := dbclient.Client.WelcomeMessages.Get(ctx, ticket.GuildId) if err != nil { - return nil, err + return nil, 0, err } if len(welcomeMessage) == 0 { @@ -134,21 +142,21 @@ func BuildWelcomeMessageEmbed( // Replace variables welcomeMessage = DoPlaceholderSubstitutions(ctx, welcomeMessage, cmd.Worker(), ticket, additionalPlaceholders) - - return utils.BuildEmbedRaw(cmd.GetColour(customisation.Green), subject, welcomeMessage, nil, cmd.PremiumTier()), nil + container := utils.BuildContainerRaw(cmd.GetColour(customisation.Green), subject, welcomeMessage, cmd.PremiumTier()) + return &container, cmd.GetColour(customisation.Green), nil } else { data, err := dbclient.Client.Embeds.GetEmbed(ctx, *panel.WelcomeMessageEmbed) if err != nil { - return nil, err + return nil, 0, err } fields, err := dbclient.Client.EmbedFields.GetFieldsForEmbed(ctx, *panel.WelcomeMessageEmbed) if err != nil { - return nil, err + return nil, 0, err } - e := BuildCustomEmbed(ctx, cmd.Worker(), ticket, data, fields, cmd.PremiumTier() == premium.None, additionalPlaceholders) - return e, nil + e := BuildCustomContainer(ctx, cmd.Worker(), ticket, data, fields, cmd.PremiumTier() == premium.None, additionalPlaceholders) + return e, int(data.Colour), nil } } @@ -563,7 +571,7 @@ func getFormDataFields(formData map[database.FormInput]string) []embed.EmbedFiel return fields } -func BuildCustomEmbed( +func BuildCustomContainer( ctx context.Context, worker *worker.Context, ticket database.Ticket, customEmbed database.CustomEmbed, @@ -571,39 +579,62 @@ func BuildCustomEmbed( branding bool, // Only custom integration placeholders for now - prevent making duplicate requests additionalPlaceholders map[string]string, -) *embed.Embed { - description := utils.ValueOrZero(customEmbed.Description) - if ticket.Id != 0 { - description = DoPlaceholderSubstitutions(ctx, description, worker, ticket, additionalPlaceholders) - } +) *component.Component { + + innerComponents := []component.Component{} - e := &embed.Embed{ - Title: utils.ValueOrZero(customEmbed.Title), - Description: description, - Url: utils.ValueOrZero(customEmbed.Url), - Timestamp: customEmbed.Timestamp, - Color: int(customEmbed.Colour), + if customEmbed.Title != nil { + innerComponents = append(innerComponents, + component.BuildTextDisplay(component.TextDisplay{ + Content: *customEmbed.Title, + }), + component.BuildSeparator(component.Separator{}), + ) } - if branding { - e.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) - } else if customEmbed.FooterText != nil { - e.SetFooter(*customEmbed.FooterText, utils.ValueOrZero(customEmbed.FooterIconUrl)) + if customEmbed.Description != nil { + description := *customEmbed.Description + if ticket.Id != 0 { + description = DoPlaceholderSubstitutions(ctx, description, worker, ticket, additionalPlaceholders) + } + innerComponents = append(innerComponents, + component.BuildTextDisplay(component.TextDisplay{ + Content: description, + }), + ) } if customEmbed.ImageUrl != nil { imageUrl := replaceImagePlaceholder(worker, ticket, *customEmbed.ImageUrl) - e.SetImage(imageUrl) + innerComponents = append(innerComponents, + component.BuildMediaGallery(component.MediaGallery{ + Items: []component.MediaGalleryItem{ + { + Media: component.UnfurledMediaItem{ + Url: imageUrl, + }, + }, + }, + }), + ) } if customEmbed.ThumbnailUrl != nil { imageUrl := replaceImagePlaceholder(worker, ticket, *customEmbed.ThumbnailUrl) - e.SetThumbnail(imageUrl) + innerComponents = append(innerComponents, + component.BuildMediaGallery(component.MediaGallery{ + Items: []component.MediaGalleryItem{ + { + Media: component.UnfurledMediaItem{ + Url: imageUrl, + }, + }, + }, + }), + ) } - if customEmbed.AuthorName != nil { - e.SetAuthor(*customEmbed.AuthorName, utils.ValueOrZero(customEmbed.AuthorUrl), utils.ValueOrZero(customEmbed.AuthorIconUrl)) - } + fieldStr := "" for _, field := range fields { value := field.Value @@ -611,10 +642,31 @@ func BuildCustomEmbed( value = DoPlaceholderSubstitutions(ctx, value, worker, ticket, additionalPlaceholders) } - e.AddField(field.Name, value, field.Inline) + fieldStr += fmt.Sprintf("**%s**\n%s\n", field.Name, value) + } + + if branding { + innerComponents = utils.AddPremiumFooter(innerComponents) + } else if customEmbed.FooterText != nil { + innerComponents = append(innerComponents, + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: *customEmbed.FooterText, + }), + ) + } + + container := component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(int(customEmbed.Colour)), + Components: innerComponents, + }) + + if len(innerComponents) == 0 { + // If there are no components, we should return nil + return nil } - return e + return &container } func replaceImagePlaceholder(worker *worker.Context, ticket database.Ticket, imageUrl string) string { diff --git a/bot/model/model.go b/bot/model/model.go new file mode 100644 index 0000000..6581c6a --- /dev/null +++ b/bot/model/model.go @@ -0,0 +1,6 @@ +package model + +type Field struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/bot/premium/command.go b/bot/premium/command.go index e03db0e..44d6f3a 100644 --- a/bot/premium/command.go +++ b/bot/premium/command.go @@ -81,8 +81,8 @@ func BuildPatreonSubscriptionFoundMessage(ctx registry.CommandContext, legacyEnt }), )) - embed := utils.BuildEmbed(ctx, customisation.Red, i18n.MessagePremiumSubscriptionFound, i18n.MessagePremiumSubscriptionFoundContent, nil, guild.OwnerId, commands["addadmin"], commands["viewstaff"]) - return command.NewEphemeralEmbedMessageResponseWithComponents(embed, components), nil + embed := utils.BuildContainer(ctx, customisation.Red, i18n.MessagePremiumSubscriptionFound, i18n.MessagePremiumSubscriptionFoundContent, guild.OwnerId, commands["addadmin"], commands["viewstaff"]) + return command.NewEphemeralMessageResponseWithComponents(append(utils.Slice(embed), components...)), nil } else { // Modern entitlements components := utils.Slice(component.BuildActionRow( component.BuildButton(component.Button{ @@ -99,8 +99,8 @@ func BuildPatreonSubscriptionFoundMessage(ctx registry.CommandContext, legacyEnt }), )) - embed := utils.BuildEmbed(ctx, customisation.Red, i18n.MessagePremiumSubscriptionFound, i18n.MessagePremiumSubscriptionFoundContentModern, nil) - return command.NewEphemeralEmbedMessageResponseWithComponents(embed, components), nil + embed := utils.BuildContainer(ctx, customisation.Red, i18n.MessagePremiumSubscriptionFound, i18n.MessagePremiumSubscriptionFoundContentModern) + return command.NewEphemeralMessageResponseWithComponents(append(utils.Slice(embed), components...)), nil } } @@ -126,14 +126,14 @@ func BuildPatreonNotLinkedMessage(ctx registry.CommandContext) command.MessageRe }), )) - embed := utils.BuildEmbed(ctx, customisation.Red, i18n.TitlePremium, i18n.MessagePremiumNoSubscription, nil) - return command.NewEphemeralEmbedMessageResponseWithComponents(embed, components) + embed := utils.BuildContainer(ctx, customisation.Red, i18n.TitlePremium, i18n.MessagePremiumNoSubscription) + return command.NewEphemeralMessageResponseWithComponents(append(utils.Slice(embed), components...)) } func BuildDiscordNotFoundMessage(ctx registry.CommandContext) command.MessageResponse { - embed := utils.BuildEmbed(ctx, customisation.Red, i18n.TitlePremium, i18n.MessagePremiumDiscordNoSubscription, nil) + embed := utils.BuildContainer(ctx, customisation.Red, i18n.TitlePremium, i18n.MessagePremiumDiscordNoSubscription) - return command.NewEphemeralEmbedMessageResponseWithComponents(embed, utils.Slice(component.BuildActionRow( + return command.NewEphemeralMessageResponseWithComponents(append(utils.Slice(embed), component.BuildActionRow( component.BuildButton(component.Button{ Style: component.ButtonStylePremium, SkuId: utils.Ptr(PremiumStoreSku), diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index 3f8c33a..f4bc01d 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -5,62 +5,44 @@ import ( "fmt" "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" "github.com/TicketsBot/common/utils" ) -func BuildEmbed( - ctx registry.CommandContext, - colour customisation.Colour, titleId, contentId i18n.MessageId, fields []embed.EmbedField, - format ...interface{}, -) *embed.Embed { - title := i18n.GetMessageFromGuild(ctx.GuildId(), titleId) - content := i18n.GetMessageFromGuild(ctx.GuildId(), contentId, format...) - - msgEmbed := embed.NewEmbed(). - SetColor(ctx.GetColour(colour)). - SetTitle(title). - SetDescription(content) - - for _, field := range fields { - msgEmbed.AddField(field.Name, field.Value, field.Inline) - } - - if ctx.PremiumTier() == premium.None { - msgEmbed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) - } - - return msgEmbed +func BuildContainer(ctx registry.CommandContext, colour customisation.Colour, titleId, contentId i18n.MessageId, format ...interface{}) component.Component { + return BuildContainerWithComponents(ctx, colour, titleId, ctx.PremiumTier(), []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: i18n.GetMessageFromGuild(ctx.GuildId(), contentId, format...), + })}) } -func BuildEmbedRaw( - colourHex int, title, content string, fields []embed.EmbedField, tier premium.PremiumTier, -) *embed.Embed { - msgEmbed := embed.NewEmbed(). - SetColor(colourHex). - SetTitle(title). - SetDescription(content) +func BuildContainerWithFields(ctx registry.CommandContext, colour customisation.Colour, titleId, content i18n.MessageId, fields []model.Field, format ...interface{}) component.Component { - for _, field := range fields { - msgEmbed.AddField(field.Name, field.Value, field.Inline) - } + components := make([]component.Component, 0, len(fields)+2) - if tier == premium.None { - msgEmbed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) + for _, field := range fields { + components = append(components, component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("**%s**\n%s", field.Name, field.Value), + })) } - return msgEmbed + return BuildContainerWithComponents(ctx, colour, titleId, ctx.PremiumTier(), append([]component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: i18n.GetMessageFromGuild(ctx.GuildId(), content, format...), + }), + component.BuildSeparator(component.Separator{}), + }, components...)) } -func BuildContainer( +func BuildContainerWithComponents( ctx registry.CommandContext, colour customisation.Colour, title i18n.MessageId, tier premium.PremiumTier, innerComponents []component.Component, ) component.Component { components := append(Slice( @@ -144,6 +126,20 @@ func BuildContainerRaw( }) } +func AddPremiumFooter(components []component.Component) []component.Component { + if len(components) == 0 || components[len(components)-1].Type != component.ComponentSeparator { + components = append(components, component.BuildSeparator(component.Separator{})) + } + + components = append(components, + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), + }), + ) + + return components +} + func GetColourForGuild(ctx context.Context, worker *worker.Context, colour customisation.Colour, guildId uint64) (int, error) { premiumTier, err := PremiumClient.GetTierByGuildId(ctx, guildId, true, worker.Token, worker.RateLimiter) if err != nil { @@ -164,28 +160,8 @@ func GetColourForGuild(ctx context.Context, worker *worker.Context, colour custo } } -func EmbedFieldRaw(name, value string, inline bool) embed.EmbedField { - return embed.EmbedField{ - Name: name, - Value: value, - Inline: inline, - } -} - -func EmbedField(guildId uint64, name string, value i18n.MessageId, inline bool, format ...interface{}) embed.EmbedField { - return embed.EmbedField{ - Name: name, - Value: i18n.GetMessageFromGuild(guildId, value, format...), - Inline: inline, - } -} - func BuildEmoji(emote string) *emoji.Emoji { return &emoji.Emoji{ Name: emote, } } - -func Embeds(embeds ...*embed.Embed) []*embed.Embed { - return embeds -} diff --git a/event/caller.go b/event/caller.go index a03666f..c98e346 100644 --- a/event/caller.go +++ b/event/caller.go @@ -85,6 +85,21 @@ func callCommand( arg0 = argValue } + v.Execute(ctx, arg0) + case admin.AdminDebugCommand: + var arg0 string + + opt0, ok0 := findOption(cmd.Properties().Arguments[0], options) + if !ok0 { + return ErrArgumentNotFound + } else { + argValue, ok := opt0.Value.(string) + if !ok { + return fmt.Errorf("option %s was not a string", opt0.Name) + } + arg0 = argValue + } + v.Execute(ctx, arg0) case admin.AdminCommand: diff --git a/event/commandexecutor.go b/event/commandexecutor.go index a482322..9693a71 100644 --- a/event/commandexecutor.go +++ b/event/commandexecutor.go @@ -192,8 +192,8 @@ func executeCommand( if errors.Is(err, ErrArgumentNotFound) { if worker.IsWhitelabel { content := `This command registration is outdated. Please ask the server administrators to visit the whitelabel dashboard and press "Create Slash Commands" again.` - embed := utils.BuildEmbedRaw(customisation.GetDefaultColour(customisation.Red), "Outdated Command", content, nil, premium.Whitelabel) - res := command.NewEphemeralEmbedMessageResponse(embed) + container := utils.BuildContainerRaw(customisation.GetDefaultColour(customisation.Red), "Outdated Command", content, premium.Whitelabel) + res := command.NewEphemeralMessageResponseWithComponents(utils.Slice(container)) responseCh <- res.IntoApplicationCommandData() return diff --git a/event/httplisten.go b/event/httplisten.go index a88fae1..84625e2 100644 --- a/event/httplisten.go +++ b/event/httplisten.go @@ -121,7 +121,6 @@ func interactionHandler(redis *redis.Client, cache *cache.PgCache) func(*gin.Con Cache: cache, RateLimiter: nil, // Use http-proxy ratelimit functionality } - fmt.Println("Handling interaction", payload.InteractionType) switch payload.InteractionType { case interaction.InteractionTypeApplicationCommand: diff --git a/restwrapper.go b/restwrapper.go index 5218cab..567d8c9 100644 --- a/restwrapper.go +++ b/restwrapper.go @@ -7,7 +7,6 @@ import ( "github.com/TicketsBot-cloud/gdl/cache" "github.com/TicketsBot-cloud/gdl/objects/auditlog" "github.com/TicketsBot-cloud/gdl/objects/channel" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/guild" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" @@ -74,19 +73,6 @@ func (ctx *Context) CreateMessageReply(channelId uint64, content string, referen }) } -func (ctx *Context) CreateMessageEmbed(channelId uint64, embed ...*embed.Embed) (message.Message, error) { - return ctx.CreateMessageComplex(channelId, rest.CreateMessageData{ - Embeds: embed, - }) -} - -func (ctx *Context) CreateMessageEmbedReply(channelId uint64, e *embed.Embed, reference *message.MessageReference) (message.Message, error) { - return ctx.CreateMessageComplex(channelId, rest.CreateMessageData{ - Embeds: []*embed.Embed{e}, - MessageReference: reference, - }) -} - func (ctx *Context) CreateMessageComplex(channelId uint64, data rest.CreateMessageData) (message.Message, error) { return rest.CreateMessage(context.Background(), ctx.Token, ctx.RateLimiter, channelId, data) } From 1d9ed3cbaee6cddad0859d49a165be962ae2c151 Mon Sep 17 00:00:00 2001 From: Tobias <53872542+biast12@users.noreply.github.com> Date: Sun, 15 Jun 2025 16:00:55 +0200 Subject: [PATCH 05/13] feat: slight rewrite of /viewstaff with added pagination (#14) * feat: slight rewrite of /viewstaff with added pagination * oops * Add branding footer * Only show Support users if more than 1 exist * Switched role/user order * Move view staff message formats to the worker --- bot/button/handlers/viewstaff.go | 72 ++------ bot/command/impl/settings/viewstaff.go | 28 +-- bot/logic/viewstaff.go | 240 +++++++++++++------------ i18n/messages.go | 10 ++ 4 files changed, 152 insertions(+), 198 deletions(-) diff --git a/bot/button/handlers/viewstaff.go b/bot/button/handlers/viewstaff.go index 75b3643..af42122 100644 --- a/bot/button/handlers/viewstaff.go +++ b/bot/button/handlers/viewstaff.go @@ -1,15 +1,12 @@ package handlers import ( - "fmt" "regexp" "strconv" "strings" "time" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" - "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" - "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" "github.com/TicketsBot-cloud/worker/bot/command" @@ -51,64 +48,21 @@ func (h *ViewStaffHandler) Execute(ctx *context.ButtonContext) { return } - msgEmbed, isBlank := logic.BuildViewStaffMessage(ctx.Context, ctx, page) - if !isBlank { + msgEmbed, totalPages := logic.BuildViewStaffMessage(ctx.Context, ctx, page) + if totalPages <= 1 { ctx.Edit(command.MessageResponse{ - Embeds: []*embed.Embed{msgEmbed}, - Components: []component.Component{ - component.BuildActionRow( - component.BuildButton(component.Button{ - CustomId: fmt.Sprintf("viewstaff_%d", page-1), - Style: component.ButtonStylePrimary, - Emoji: &emoji.Emoji{ - Name: "ā—€ļø", - }, - Disabled: page <= 0, - }), - component.BuildButton(component.Button{ - CustomId: fmt.Sprintf("viewstaff_%d", page+1), - Style: component.ButtonStylePrimary, - Emoji: &emoji.Emoji{ - Name: "ā–¶ļø", - }, - Disabled: false, - }), - ), - }, + Embeds: []*embed.Embed{msgEmbed}, + Components: nil, }) - } else { - components := ctx.Interaction.Message.Components - if len(components) == 0 { // Impossible unless whitelabel - return - } - - actionRow, ok := components[0].ComponentData.(component.ActionRow) - if !ok { - return - } - - if len(actionRow.Components) < 2 { - return - } - - nextButton := actionRow.Components[1].ComponentData.(component.Button) - if !ok { - return - } - - nextButton.Disabled = true - actionRow.Components[1].ComponentData = nextButton - components[0].ComponentData = actionRow - - // v hacky - embeds := make([]*embed.Embed, len(ctx.Interaction.Message.Embeds)) - for i, e := range ctx.Interaction.Message.Embeds { - embeds[i] = &e - } + return + } - ctx.Edit(command.MessageResponse{ - Embeds: embeds, - Components: components, - }) + if page >= totalPages { + page = totalPages - 1 } + + ctx.Edit(command.MessageResponse{ + Embeds: []*embed.Embed{msgEmbed}, + Components: logic.BuildViewStaffComponents(page, totalPages), + }) } diff --git a/bot/command/impl/settings/viewstaff.go b/bot/command/impl/settings/viewstaff.go index 2c4836f..66839a6 100644 --- a/bot/command/impl/settings/viewstaff.go +++ b/bot/command/impl/settings/viewstaff.go @@ -6,9 +6,7 @@ import ( "github.com/TicketsBot-cloud/common/permission" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/channel/message" - "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction" - "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/logic" @@ -35,31 +33,15 @@ func (c ViewStaffCommand) GetExecutor() interface{} { } func (ViewStaffCommand) Execute(ctx registry.CommandContext) { - msgEmbed, _ := logic.BuildViewStaffMessage(ctx, ctx, 0) + msgEmbed, totalPages := logic.BuildViewStaffMessage(ctx, ctx, 0) res := command.MessageResponse{ Embeds: []*embed.Embed{msgEmbed}, Flags: message.SumFlags(message.FlagEphemeral), - Components: []component.Component{ - component.BuildActionRow( - component.BuildButton(component.Button{ - CustomId: "disabled", - Style: component.ButtonStylePrimary, - Emoji: &emoji.Emoji{ - Name: "ā—€ļø", - }, - Disabled: true, - }), - component.BuildButton(component.Button{ - CustomId: "viewstaff_1", - Style: component.ButtonStylePrimary, - Emoji: &emoji.Emoji{ - Name: "ā–¶ļø", - }, - Disabled: false, - }), - ), - }, + } + + if totalPages > 1 { + res.Components = logic.BuildViewStaffComponents(0, totalPages) } _, _ = ctx.ReplyWith(res) diff --git a/bot/logic/viewstaff.go b/bot/logic/viewstaff.go index dab6763..d5ece9d 100644 --- a/bot/logic/viewstaff.go +++ b/bot/logic/viewstaff.go @@ -5,142 +5,150 @@ import ( "fmt" "strings" - "github.com/TicketsBot-cloud/common/sentry" + "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/gdl/objects/channel/embed" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/config" + "github.com/TicketsBot-cloud/worker/i18n" ) -// each msg is const perField = 8 +const viewStaffUserFormat = "- <@%d> (`%d`)\n" +const viewStaffRoleFormat = "- <@&%d> (`%d`)\n" -func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (*embed.Embed, bool) { - isBlank := true +func BuildViewStaffComponents(page, totalPages int) []component.Component { + if totalPages <= 1 { + return nil + } + return []component.Component{ + component.BuildActionRow( + component.BuildButton(component.Button{ + CustomId: fmt.Sprintf("viewstaff_%d", page-1), + Style: component.ButtonStyleDanger, + Label: "<", + Disabled: page <= 0, + }), + component.BuildButton(component.Button{ + CustomId: "viewstaff_page_count", + Style: component.ButtonStyleSecondary, + Label: fmt.Sprintf("%d/%d", page+1, totalPages), + Disabled: true, + }), + component.BuildButton(component.Button{ + CustomId: fmt.Sprintf("viewstaff_%d", page+1), + Style: component.ButtonStyleSuccess, + Label: ">", + Disabled: page >= totalPages-1, + }), + ), + } +} + +func buildPaginatedField(cmd registry.CommandContext, entries []uint64, page int, labelId, emptyId i18n.MessageId, format string, prefix string) (string, string) { + lower := perField * page + upper := perField * (page + 1) + if upper > len(entries) { + upper = len(entries) + } + label := cmd.GetMessage(labelId) + if len(entries) == 0 || lower >= len(entries) { + return label, cmd.GetMessage(emptyId) + } + var content strings.Builder + if prefix != "" { + content.WriteString(prefix) + content.WriteString("\n\n") + } + for i := lower; i < upper; i++ { + content.WriteString(fmt.Sprintf(format, entries[i], entries[i])) + } + return label, strings.TrimSuffix(content.String(), "\n") +} - self, _ := cmd.Worker().Self() +func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (*embed.Embed, int) { embed := embed.NewEmbed(). SetColor(cmd.GetColour(customisation.Green)). - SetTitle("Staff"). - SetFooter(fmt.Sprintf("Page %d", page+1), self.AvatarUrl(256)) - - // Add field for admin users - { - adminUsers, err := dbclient.Client.Permissions.GetAdmins(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } + SetTitle(cmd.GetMessage(i18n.MessageViewStaffTitle)) - lower := perField * page - upper := perField * (page + 1) - - if lower >= len(adminUsers) { - embed.AddField("Admin Users", "No admin users", true) - } else { - if upper >= len(adminUsers) { - upper = len(adminUsers) - } - - var content string - for i := lower; i < upper; i++ { - userId := adminUsers[i] - content += fmt.Sprintf("• <@%d> (`%d`)\n", userId, userId) - } - content = strings.TrimSuffix(content, "\n") - - embed.AddField("Admin Users", content, true) - isBlank = false - } - } + adminUsers, _ := dbclient.Client.Permissions.GetAdmins(ctx, cmd.GuildId()) + adminRoles, _ := dbclient.Client.RolePermissions.GetAdminRoles(ctx, cmd.GuildId()) + supportUsers, _ := dbclient.Client.Permissions.GetSupportOnly(ctx, cmd.GuildId()) + supportRoles, _ := dbclient.Client.RolePermissions.GetSupportRolesOnly(ctx, cmd.GuildId()) - // Add field for admin roles - { - adminRoles, err := dbclient.Client.RolePermissions.GetAdminRoles(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } - - lower := perField * page - upper := perField * (page + 1) - - if lower >= len(adminRoles) { - embed.AddField("Admin Roles", "No admin roles", true) - } else { - if upper >= len(adminRoles) { - upper = len(adminRoles) - } - - var content string - for i := lower; i < upper; i++ { - roleId := adminRoles[i] - content += fmt.Sprintf("• <@&%d> (`%d`)\n", roleId, roleId) - } - content = strings.TrimSuffix(content, "\n") - - embed.AddField("Admin Roles", content, true) - isBlank = false - } + maxLen := max(len(adminUsers), len(adminRoles), len(supportUsers), len(supportRoles)) + totalPages := (maxLen + perField - 1) / perField + if totalPages == 0 { + totalPages = 1 } - embed.AddBlankField(false) // Add spacer between admin & support reps + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 + } - // Add field for support representatives - { - supportUsers, err := dbclient.Client.Permissions.GetSupportOnly(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } + // Admin roles + label, value := buildPaginatedField( + cmd, adminRoles, page, + i18n.MessageViewStaffAdminRoles, + i18n.MessageViewStaffNoAdminRoles, + viewStaffRoleFormat, + "", + ) + embed.AddField(label, value, true) + + // Admin users + label, value = buildPaginatedField( + cmd, adminUsers, page, + i18n.MessageViewStaffAdminUsers, + i18n.MessageViewStaffNoAdminUsers, + viewStaffUserFormat, + "", + ) + embed.AddField(label, value, true) + + embed.AddBlankField(false) + + // Support roles + label, value = buildPaginatedField( + cmd, supportRoles, page, + i18n.MessageViewStaffSupportRoles, + i18n.MessageViewStaffNoSupportRoles, + viewStaffRoleFormat, + "", + ) + embed.AddField(label, value, true) + + // Support users + if len(supportUsers) > 0 { + label, value = buildPaginatedField( + cmd, supportUsers, page, + i18n.MessageViewStaffSupportUsers, + "", + viewStaffUserFormat, + cmd.GetMessage(i18n.MessageViewStaffSupportUsersWarn), + ) + embed.AddField(label, value, true) + } - lower := perField * page - upper := perField * (page + 1) - - if lower >= len(supportUsers) { - embed.AddField("Support Representatives", "No support representatives", true) - } else { - if upper >= len(supportUsers) { - upper = len(supportUsers) - } - - content := "**Warning:** Users in support teams are now deprecated. Please migrate to roles.\n\n" - for i := lower; i < upper; i++ { - userId := supportUsers[i] - content += fmt.Sprintf("• <@%d> (`%d`)\n", userId, userId) - } - content = strings.TrimSuffix(content, "\n") - - embed.AddField("Support Representatives", content, true) - isBlank = false - } + // Add premium branding footer if not premium + if cmd.PremiumTier() == premium.None { + embed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) } - // Add field for support roles - { - supportRoles, err := dbclient.Client.RolePermissions.GetSupportRolesOnly(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } + return embed, totalPages +} - lower := perField * page - upper := perField * (page + 1) - - if lower >= len(supportRoles) { - embed.AddField("Support Roles", "No support roles", true) - } else { - if upper >= len(supportRoles) { - upper = len(supportRoles) - } - - var content string - for i := lower; i < upper; i++ { - roleId := supportRoles[i] - content += fmt.Sprintf("• <@&%d> (`%d`)\n", roleId, roleId) - } - content = strings.TrimSuffix(content, "\n") - - embed.AddField("Support Roles", content, true) - isBlank = false +func max(nums ...int) int { + maxVal := 0 + for _, n := range nums { + if n > maxVal { + maxVal = n } } - - return embed, isBlank + return maxVal } diff --git a/i18n/messages.go b/i18n/messages.go index 5429f7d..025158e 100644 --- a/i18n/messages.go +++ b/i18n/messages.go @@ -231,6 +231,16 @@ var ( MessageNotesAddedToExisting MessageId = "commands.notes.added_to_existing" MessageNotesCreated MessageId = "commands.notes.created" + MessageViewStaffTitle MessageId = "commands.viewstaff.title" + MessageViewStaffAdminUsers MessageId = "commands.viewstaff.admin.users" + MessageViewStaffNoAdminUsers MessageId = "commands.viewstaff.admin.no_users" + MessageViewStaffAdminRoles MessageId = "commands.viewstaff.admin.roles" + MessageViewStaffNoAdminRoles MessageId = "commands.viewstaff.admin.no_roles" + MessageViewStaffSupportUsers MessageId = "commands.viewstaff.support.users" + MessageViewStaffSupportUsersWarn MessageId = "commands.viewstaff.support.users_warning" + MessageViewStaffSupportRoles MessageId = "commands.viewstaff.support.roles" + MessageViewStaffNoSupportRoles MessageId = "commands.viewstaff.support.no_roles" + MessageJoinClosedTicket MessageId = "button.join_thread.closed_ticket" MessageJoinThreadNoPermission MessageId = "button.join_thread.no_permission" MessageAlreadyJoinedThread MessageId = "button.join_thread.already_joined" From f79f2f6dd2f6abb4bc809e92ccebe4fbcf12b9dc Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 21 Jun 2025 22:07:07 +0100 Subject: [PATCH 06/13] update more components Signed-off-by: Ben --- bot/button/handlers/viewstaff.go | 18 ++---- bot/command/impl/general/help.go | 2 +- bot/command/impl/settings/viewstaff.go | 19 ++----- bot/command/impl/tickets/startticket.go | 2 +- bot/logic/help.go | 1 - bot/logic/viewstaff.go | 75 +++++++++++-------------- locale | 2 +- 7 files changed, 47 insertions(+), 72 deletions(-) diff --git a/bot/button/handlers/viewstaff.go b/bot/button/handlers/viewstaff.go index af42122..faf10f3 100644 --- a/bot/button/handlers/viewstaff.go +++ b/bot/button/handlers/viewstaff.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/button/registry" "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" "github.com/TicketsBot-cloud/worker/bot/command" @@ -48,21 +48,15 @@ func (h *ViewStaffHandler) Execute(ctx *context.ButtonContext) { return } - msgEmbed, totalPages := logic.BuildViewStaffMessage(ctx.Context, ctx, page) - if totalPages <= 1 { - ctx.Edit(command.MessageResponse{ - Embeds: []*embed.Embed{msgEmbed}, - Components: nil, - }) - return - } - + comp, totalPages := logic.BuildViewStaffMessage(ctx.Context, ctx, page) if page >= totalPages { page = totalPages - 1 } ctx.Edit(command.MessageResponse{ - Embeds: []*embed.Embed{msgEmbed}, - Components: logic.BuildViewStaffComponents(page, totalPages), + Components: []component.Component{ + comp, + logic.BuildViewStaffButtons(page, totalPages), + }, }) } diff --git a/bot/command/impl/general/help.go b/bot/command/impl/general/help.go index 2372544..8f69870 100644 --- a/bot/command/impl/general/help.go +++ b/bot/command/impl/general/help.go @@ -43,8 +43,8 @@ func (c HelpCommand) Execute(ctx registry.CommandContext) { _, err = ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ *container, - *logic.BuildHelpMessagePaginationButtons(command.General.ToRawString(), 1, 1), *logic.BuildHelpMessageCategorySelector(c.Registry, ctx, command.General), + *logic.BuildHelpMessagePaginationButtons(command.General.ToRawString(), 1, 1), })) if err != nil { diff --git a/bot/command/impl/settings/viewstaff.go b/bot/command/impl/settings/viewstaff.go index 66839a6..415e513 100644 --- a/bot/command/impl/settings/viewstaff.go +++ b/bot/command/impl/settings/viewstaff.go @@ -4,9 +4,8 @@ import ( "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" - "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/logic" @@ -33,16 +32,10 @@ func (c ViewStaffCommand) GetExecutor() interface{} { } func (ViewStaffCommand) Execute(ctx registry.CommandContext) { - msgEmbed, totalPages := logic.BuildViewStaffMessage(ctx, ctx, 0) + comp, totalPages := logic.BuildViewStaffMessage(ctx, ctx, 0) - res := command.MessageResponse{ - Embeds: []*embed.Embed{msgEmbed}, - Flags: message.SumFlags(message.FlagEphemeral), - } - - if totalPages > 1 { - res.Components = logic.BuildViewStaffComponents(0, totalPages) - } - - _, _ = ctx.ReplyWith(res) + _, _ = ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + comp, + logic.BuildViewStaffButtons(0, totalPages), + })) } diff --git a/bot/command/impl/tickets/startticket.go b/bot/command/impl/tickets/startticket.go index c8f2a55..f4db1e8 100644 --- a/bot/command/impl/tickets/startticket.go +++ b/bot/command/impl/tickets/startticket.go @@ -67,7 +67,7 @@ func (StartTicketCommand) Execute(ctx registry.CommandContext) { messageId := interaction.Interaction.Data.TargetId msg, ok := interaction.ResolvedMessage(messageId) - if err != nil { + if !ok { ctx.HandleError(errors.New("Message missing from resolved data")) return } diff --git a/bot/logic/help.go b/bot/logic/help.go index fbdf540..b93954c 100644 --- a/bot/logic/help.go +++ b/bot/logic/help.go @@ -155,7 +155,6 @@ func BuildHelpMessageCategorySelector(r registry.Registry, ctx registry.CommandC } permLevel, _ := ctx.UserPermissionLevel(ctx) - // permLevel := permission.Admin for _, cmd := range r { properties := cmd.Properties() diff --git a/bot/logic/viewstaff.go b/bot/logic/viewstaff.go index d5ece9d..bb0edf5 100644 --- a/bot/logic/viewstaff.go +++ b/bot/logic/viewstaff.go @@ -5,13 +5,11 @@ import ( "fmt" "strings" - "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" - "github.com/TicketsBot-cloud/worker/config" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -19,32 +17,27 @@ const perField = 8 const viewStaffUserFormat = "- <@%d> (`%d`)\n" const viewStaffRoleFormat = "- <@&%d> (`%d`)\n" -func BuildViewStaffComponents(page, totalPages int) []component.Component { - if totalPages <= 1 { - return nil - } - return []component.Component{ - component.BuildActionRow( - component.BuildButton(component.Button{ - CustomId: fmt.Sprintf("viewstaff_%d", page-1), - Style: component.ButtonStyleDanger, - Label: "<", - Disabled: page <= 0, - }), - component.BuildButton(component.Button{ - CustomId: "viewstaff_page_count", - Style: component.ButtonStyleSecondary, - Label: fmt.Sprintf("%d/%d", page+1, totalPages), - Disabled: true, - }), - component.BuildButton(component.Button{ - CustomId: fmt.Sprintf("viewstaff_%d", page+1), - Style: component.ButtonStyleSuccess, - Label: ">", - Disabled: page >= totalPages-1, - }), - ), - } +func BuildViewStaffButtons(page, totalPages int) component.Component { + return component.BuildActionRow( + component.BuildButton(component.Button{ + CustomId: fmt.Sprintf("viewstaff_%d", page-1), + Style: component.ButtonStyleDanger, + Label: "<", + Disabled: page <= 0, + }), + component.BuildButton(component.Button{ + CustomId: "viewstaff_page_count", + Style: component.ButtonStyleSecondary, + Label: fmt.Sprintf("%d/%d", page+1, totalPages), + Disabled: true, + }), + component.BuildButton(component.Button{ + CustomId: fmt.Sprintf("viewstaff_%d", page+1), + Style: component.ButtonStyleSuccess, + Label: ">", + Disabled: page >= totalPages-1, + }), + ) } func buildPaginatedField(cmd registry.CommandContext, entries []uint64, page int, labelId, emptyId i18n.MessageId, format string, prefix string) (string, string) { @@ -68,10 +61,8 @@ func buildPaginatedField(cmd registry.CommandContext, entries []uint64, page int return label, strings.TrimSuffix(content.String(), "\n") } -func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (*embed.Embed, int) { - embed := embed.NewEmbed(). - SetColor(cmd.GetColour(customisation.Green)). - SetTitle(cmd.GetMessage(i18n.MessageViewStaffTitle)) +func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (component.Component, int) { + comps := []component.Component{} adminUsers, _ := dbclient.Client.Permissions.GetAdmins(ctx, cmd.GuildId()) adminRoles, _ := dbclient.Client.RolePermissions.GetAdminRoles(ctx, cmd.GuildId()) @@ -99,7 +90,8 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffRoleFormat, "", ) - embed.AddField(label, value, true) + comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) + comps = append(comps, component.BuildSeparator(component.Separator{Divider: utils.Ptr(true), Spacing: utils.Ptr(1)})) // Admin users label, value = buildPaginatedField( @@ -109,9 +101,9 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffUserFormat, "", ) - embed.AddField(label, value, true) + comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) - embed.AddBlankField(false) + comps = append(comps, component.BuildSeparator(component.Separator{Divider: utils.Ptr(true), Spacing: utils.Ptr(1)})) // Support roles label, value = buildPaginatedField( @@ -121,7 +113,7 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffRoleFormat, "", ) - embed.AddField(label, value, true) + comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) // Support users if len(supportUsers) > 0 { @@ -132,15 +124,12 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffUserFormat, cmd.GetMessage(i18n.MessageViewStaffSupportUsersWarn), ) - embed.AddField(label, value, true) + comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) } - // Add premium branding footer if not premium - if cmd.PremiumTier() == premium.None { - embed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) - } + container := utils.BuildContainerWithComponents(cmd, customisation.Green, i18n.MessageViewStaffTitle, cmd.PremiumTier(), comps) - return embed, totalPages + return container, totalPages } func max(nums ...int) int { diff --git a/locale b/locale index 1e90c3e..50a5bc9 160000 --- a/locale +++ b/locale @@ -1 +1 @@ -Subproject commit 1e90c3e69feb02f5502774aaa227ce56888a432a +Subproject commit 50a5bc9f7ef1af46627b28593c8f18cd1237be6a From 0ce9f2a3dc99a85a0cf1e0958aadd20ea7a0cc9d Mon Sep 17 00:00:00 2001 From: Tobias <53872542+biast12@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:55:02 +0200 Subject: [PATCH 07/13] Cleanup for cv2 (#20) Cleanup --- bot/button/handlers/close.go | 2 +- .../impl/admin/adminlistguildentitlements.go | 1 - .../impl/admin/adminlistuserentitlements.go | 1 - bot/command/impl/admin/adminrecache.go | 1 - bot/command/impl/general/invite.go | 2 +- bot/command/impl/general/jumptotop.go | 2 +- bot/command/impl/general/vote.go | 2 +- bot/command/impl/settings/addadmin.go | 2 +- bot/command/impl/settings/addsupport.go | 2 +- bot/command/impl/settings/language.go | 1 - bot/command/impl/settings/premium.go | 1 - bot/command/impl/tags/tagalias.go | 7 ---- bot/command/impl/tickets/closerequest.go | 7 ---- bot/command/impl/tickets/switchpanel.go | 2 - bot/customisation/emoji.go | 31 +++++++------- bot/logic/close.go | 2 - bot/logic/help.go | 1 - bot/logic/viewstaff.go | 2 +- bot/logic/welcomemessage.go | 2 - bot/utils/messageutils.go | 41 +++++-------------- config/config.go | 17 ++++---- 21 files changed, 39 insertions(+), 90 deletions(-) diff --git a/bot/button/handlers/close.go b/bot/button/handlers/close.go index 5b8ecbc..170bb8d 100644 --- a/bot/button/handlers/close.go +++ b/bot/button/handlers/close.go @@ -57,7 +57,7 @@ func (h *CloseHandler) Execute(ctx *cmdcontext.ButtonContext) { } if closeConfirmation { - container := utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleCloseConfirmation, ctx.PremiumTier(), []component.Component{ + container := utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleCloseConfirmation, []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("<@%d>: %s", ctx.InteractionUser().Id, ctx.GetMessage(i18n.MessageCloseConfirmation))}), component.BuildActionRow(component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.TitleClose), diff --git a/bot/command/impl/admin/adminlistguildentitlements.go b/bot/command/impl/admin/adminlistguildentitlements.go index 2473a8d..767b282 100644 --- a/bot/command/impl/admin/adminlistguildentitlements.go +++ b/bot/command/impl/admin/adminlistguildentitlements.go @@ -118,7 +118,6 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu ctx, customisation.Orange, i18n.Admin, - ctx.PremiumTier(), values, ), })) diff --git a/bot/command/impl/admin/adminlistuserentitlements.go b/bot/command/impl/admin/adminlistuserentitlements.go index 0a83f21..12885d4 100644 --- a/bot/command/impl/admin/adminlistuserentitlements.go +++ b/bot/command/impl/admin/adminlistuserentitlements.go @@ -72,7 +72,6 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use ctx, customisation.Orange, i18n.Admin, - ctx.PremiumTier(), values, ), })) diff --git a/bot/command/impl/admin/adminrecache.go b/bot/command/impl/admin/adminrecache.go index 46f794a..16e6e65 100644 --- a/bot/command/impl/admin/adminrecache.go +++ b/bot/command/impl/admin/adminrecache.go @@ -109,7 +109,6 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, providedGuildId ctx, customisation.Orange, "Admin - Recache", - ctx.PremiumTier(), []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: fmt.Sprintf("**%s** has been recached successfully.\n\n**Guild ID:** %d\n**Time Taken:** %s", guild.Name, guildId, time.Since(currentTime).Round(time.Millisecond)), diff --git a/bot/command/impl/general/invite.go b/bot/command/impl/general/invite.go index d712d17..2ec3899 100644 --- a/bot/command/impl/general/invite.go +++ b/bot/command/impl/general/invite.go @@ -36,7 +36,7 @@ func (c InviteCommand) GetExecutor() interface{} { func (InviteCommand) Execute(ctx registry.CommandContext) { ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleInvite, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleInvite, []component.Component{ component.BuildActionRow(component.BuildButton(component.Button{ Label: ctx.GetMessage(i18n.ClickHere), Style: component.ButtonStyleLink, diff --git a/bot/command/impl/general/jumptotop.go b/bot/command/impl/general/jumptotop.go index c0bd526..8c5a5b8 100644 --- a/bot/command/impl/general/jumptotop.go +++ b/bot/command/impl/general/jumptotop.go @@ -64,7 +64,7 @@ func (JumpToTopCommand) Execute(ctx registry.CommandContext) { } if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleJumpToTop, ctx.PremiumTier(), components), + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleJumpToTop, components), })); err != nil { ctx.HandleError(err) return diff --git a/bot/command/impl/general/vote.go b/bot/command/impl/general/vote.go index db3b8d0..caa1362 100644 --- a/bot/command/impl/general/vote.go +++ b/bot/command/impl/general/vote.go @@ -95,7 +95,7 @@ func (c VoteCommand) Execute(ctx registry.CommandContext) { } if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleVote, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleVote, []component.Component{ componentBody, component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), buildVoteComponent(ctx, true), diff --git a/bot/command/impl/settings/addadmin.go b/bot/command/impl/settings/addadmin.go index 1ff4f1d..92237b8 100644 --- a/bot/command/impl/settings/addadmin.go +++ b/bot/command/impl/settings/addadmin.go @@ -62,7 +62,7 @@ func (c AddAdminCommand) Execute(ctx registry.CommandContext, id uint64) { // Send confirmation message if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddAdmin, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddAdmin, []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: ctx.GetMessage(i18n.MessageAddAdminConfirm, mention), }), diff --git a/bot/command/impl/settings/addsupport.go b/bot/command/impl/settings/addsupport.go index e49358e..8244930 100644 --- a/bot/command/impl/settings/addsupport.go +++ b/bot/command/impl/settings/addsupport.go @@ -66,7 +66,7 @@ func (c AddSupportCommand) Execute(ctx registry.CommandContext, id uint64) { // Send confirmation message if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddSupport, ctx.PremiumTier(), []component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddSupport, []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: ctx.GetMessage(i18n.MessageAddSupportConfirm, mention), }), diff --git a/bot/command/impl/settings/language.go b/bot/command/impl/settings/language.go index b4719c4..b780331 100644 --- a/bot/command/impl/settings/language.go +++ b/bot/command/impl/settings/language.go @@ -79,7 +79,6 @@ func (c *LanguageCommand) Execute(ctx registry.CommandContext) { ctx, customisation.Green, i18n.TitleLanguage, - ctx.PremiumTier(), innerComponents, )))) } diff --git a/bot/command/impl/settings/premium.go b/bot/command/impl/settings/premium.go index f8f0c48..c764d96 100644 --- a/bot/command/impl/settings/premium.go +++ b/bot/command/impl/settings/premium.go @@ -90,7 +90,6 @@ func (PremiumCommand) Execute(ctx registry.CommandContext) { ctx, customisation.Green, i18n.TitlePremium, - ctx.PremiumTier(), []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: ctx.GetMessage(content)}), component.BuildActionRow(buttons...), diff --git a/bot/command/impl/tags/tagalias.go b/bot/command/impl/tags/tagalias.go index 0967ae8..a601427 100644 --- a/bot/command/impl/tags/tagalias.go +++ b/bot/command/impl/tags/tagalias.go @@ -83,13 +83,6 @@ func (c TagAliasCommand) Execute(ctx registry.CommandContext) { components = append(components, *logic.BuildCustomContainer(ctx, ctx.Worker(), ticket, *c.tag.Embed.CustomEmbed, c.tag.Embed.Fields, false, nil)) } - // var allowedMentions message.AllowedMention - // if ticket.Id != 0 { - // allowedMentions = message.AllowedMention{ - // Users: []uint64{ticket.UserId}, - // } - // } - if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents(components)); err != nil { ctx.HandleError(err) return diff --git a/bot/command/impl/tickets/closerequest.go b/bot/command/impl/tickets/closerequest.go index 53ee822..f68fbf4 100644 --- a/bot/command/impl/tickets/closerequest.go +++ b/bot/command/impl/tickets/closerequest.go @@ -105,13 +105,6 @@ func (CloseRequestCommand) Execute(ctx registry.CommandContext, closeDelay *int, }), ) - // data := command.MessageResponse{ - // AllowedMentions: message.AllowedMention{ - // Users: []uint64{ticket.UserId}, - // }, - - // } - if _, err := ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: fmt.Sprintf("<@%d>", ticket.UserId), diff --git a/bot/command/impl/tickets/switchpanel.go b/bot/command/impl/tickets/switchpanel.go index 7077f8f..7eae731 100644 --- a/bot/command/impl/tickets/switchpanel.go +++ b/bot/command/impl/tickets/switchpanel.go @@ -113,8 +113,6 @@ func (SwitchPanelCommand) Execute(ctx *cmdcontext.SlashCommandContext, panelId i c := msg.Components // get the first c where its a Container - - // embeds := utils.PtrElems(msg.Embeds) // TODO: Fix types if len(c) == 0 { c = make([]component.Component, 1) subject = "No subject given" diff --git a/bot/customisation/emoji.go b/bot/customisation/emoji.go index 180590a..b50a8a8 100644 --- a/bot/customisation/emoji.go +++ b/bot/customisation/emoji.go @@ -3,9 +3,9 @@ package customisation import ( "fmt" - "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/gdl/objects" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" + "github.com/TicketsBot-cloud/worker/config" ) type CustomEmoji struct { @@ -16,17 +16,17 @@ type CustomEmoji struct { func NewCustomEmoji(name string, id uint64, animated bool) CustomEmoji { return CustomEmoji{ - Name: name, - Id: id, + Name: name, + Id: id, + Animated: animated, } } func (e CustomEmoji) String() string { if e.Animated { return fmt.Sprintf("", e.Name, e.Id) - } else { - return fmt.Sprintf("<:%s:%d>", e.Name, e.Id) } + return fmt.Sprintf("<:%s:%d>", e.Name, e.Id) } func (e CustomEmoji) BuildEmoji() *emoji.Emoji { @@ -38,30 +38,27 @@ func (e CustomEmoji) BuildEmoji() *emoji.Emoji { } var ( + EmojiBulletLine = NewCustomEmoji("bulletline", config.Conf.Emojis.BulletLine, false) + EmojiClaim = NewCustomEmoji("claim", config.Conf.Emojis.Claim, false) + EmojiClose = NewCustomEmoji("close", config.Conf.Emojis.Close, false) + EmojiDiscord = NewCustomEmoji("discord", config.Conf.Emojis.Discord, false) EmojiId = NewCustomEmoji("id", config.Conf.Emojis.Id, false) + EmojiLogo = NewCustomEmoji("logo", config.Conf.Emojis.Logo, false) EmojiOpen = NewCustomEmoji("open", config.Conf.Emojis.Open, false) EmojiOpenTime = NewCustomEmoji("opentime", config.Conf.Emojis.OpenTime, false) - EmojiClose = NewCustomEmoji("close", config.Conf.Emojis.Close, false) - EmojiCloseTime = NewCustomEmoji("closetime", config.Conf.Emojis.CloseTime, false) - EmojiReason = NewCustomEmoji("reason", config.Conf.Emojis.Reason, false) - EmojiSubject = NewCustomEmoji("subject", config.Conf.Emojis.Subject, false) - EmojiTranscript = NewCustomEmoji("transcript", config.Conf.Emojis.Transcript, false) - EmojiClaim = NewCustomEmoji("claim", config.Conf.Emojis.Claim, false) EmojiPanel = NewCustomEmoji("panel", config.Conf.Emojis.Panel, false) + EmojiPatreon = NewCustomEmoji("patreon", config.Conf.Emojis.Patreon, false) EmojiRating = NewCustomEmoji("rating", config.Conf.Emojis.Rating, false) + EmojiReason = NewCustomEmoji("reason", config.Conf.Emojis.Reason, false) EmojiStaff = NewCustomEmoji("staff", config.Conf.Emojis.Staff, false) EmojiThread = NewCustomEmoji("thread", config.Conf.Emojis.Thread, false) - EmojiBulletLine = NewCustomEmoji("bulletline", config.Conf.Emojis.BulletLine, false) - EmojiPatreon = NewCustomEmoji("patreon", config.Conf.Emojis.Patreon, false) - EmojiDiscord = NewCustomEmoji("discord", config.Conf.Emojis.Discord, false) - //EmojiTime = NewCustomEmoji("time", 974006684622159952, false) + EmojiTranscript = NewCustomEmoji("transcript", config.Conf.Emojis.Transcript, false) ) // PrefixWithEmoji Useful for whitelabel bots func PrefixWithEmoji(s string, emoji CustomEmoji, includeEmoji bool) string { if includeEmoji { return fmt.Sprintf("%s %s", emoji, s) - } else { - return s } + return s } diff --git a/bot/logic/close.go b/bot/logic/close.go index 71b482f..8393258 100644 --- a/bot/logic/close.go +++ b/bot/logic/close.go @@ -256,11 +256,9 @@ func sendCloseEmbed(ctx context.Context, cmd registry.CommandContext, errorConte }, } - // closeEmbed, closeComponents := BuildCloseEmbed(ctx, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) closeContainer := BuildCloseContainer(ctx, cmd, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) data := rest.CreateMessageData{ - // Embeds: utils.Slice(closeEmbed), Flags: uint(message.FlagComponentsV2), Components: utils.Slice(*closeContainer), } diff --git a/bot/logic/help.go b/bot/logic/help.go index b93954c..8724668 100644 --- a/bot/logic/help.go +++ b/bot/logic/help.go @@ -21,7 +21,6 @@ func BuildHelpMessage(category command.Category, page int, ctx registry.CommandC componentList := []component.Component{} permLevel, _ := ctx.UserPermissionLevel(ctx) - // permLevel := permission.Admin commandIds, err := command.LoadCommandIds(ctx.Worker(), ctx.Worker().BotId) if err != nil { diff --git a/bot/logic/viewstaff.go b/bot/logic/viewstaff.go index bb0edf5..c2bc712 100644 --- a/bot/logic/viewstaff.go +++ b/bot/logic/viewstaff.go @@ -127,7 +127,7 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) } - container := utils.BuildContainerWithComponents(cmd, customisation.Green, i18n.MessageViewStaffTitle, cmd.PremiumTier(), comps) + container := utils.BuildContainerWithComponents(cmd, customisation.Green, i18n.MessageViewStaffTitle, comps) return container, totalPages } diff --git a/bot/logic/welcomemessage.go b/bot/logic/welcomemessage.go index 810095c..9762d05 100644 --- a/bot/logic/welcomemessage.go +++ b/bot/logic/welcomemessage.go @@ -75,8 +75,6 @@ func SendWelcomeMessage( AccentColor: &embedColor, Components: components, })) - - // embeds = append(embeds, formAnswersEmbed) } buttons := []component.Component{ diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index f4bc01d..a11882f 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -18,7 +18,7 @@ import ( ) func BuildContainer(ctx registry.CommandContext, colour customisation.Colour, titleId, contentId i18n.MessageId, format ...interface{}) component.Component { - return BuildContainerWithComponents(ctx, colour, titleId, ctx.PremiumTier(), []component.Component{ + return BuildContainerWithComponents(ctx, colour, titleId, []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: i18n.GetMessageFromGuild(ctx.GuildId(), contentId, format...), })}) @@ -34,7 +34,7 @@ func BuildContainerWithFields(ctx registry.CommandContext, colour customisation. })) } - return BuildContainerWithComponents(ctx, colour, titleId, ctx.PremiumTier(), append([]component.Component{ + return BuildContainerWithComponents(ctx, colour, titleId, append([]component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: i18n.GetMessageFromGuild(ctx.GuildId(), content, format...), }), @@ -43,7 +43,7 @@ func BuildContainerWithFields(ctx registry.CommandContext, colour customisation. } func BuildContainerWithComponents( - ctx registry.CommandContext, colour customisation.Colour, title i18n.MessageId, tier premium.PremiumTier, innerComponents []component.Component, + ctx registry.CommandContext, colour customisation.Colour, title i18n.MessageId, innerComponents []component.Component, ) component.Component { components := append(Slice( component.BuildTextDisplay(component.TextDisplay{ @@ -52,16 +52,8 @@ func BuildContainerWithComponents( component.BuildSeparator(component.Separator{}), ), innerComponents...) - if tier == premium.None { - // check if last component is a separator, if not add one - if len(components) == 0 || components[len(components)-1].Type != component.ComponentSeparator { - components = append(components, component.BuildSeparator(component.Separator{})) - } - components = append(components, - component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), - }), - ) + if ctx.PremiumTier() == premium.None { + components = AddPremiumFooter(components) } return component.BuildContainer(component.Container{ @@ -71,7 +63,7 @@ func BuildContainerWithComponents( } func BuildContainerNoLocale( - ctx registry.CommandContext, colour customisation.Colour, title string, tier premium.PremiumTier, innerComponents []component.Component, + ctx registry.CommandContext, colour customisation.Colour, title string, innerComponents []component.Component, ) component.Component { components := append(Slice( component.BuildTextDisplay(component.TextDisplay{ @@ -80,16 +72,8 @@ func BuildContainerNoLocale( component.BuildSeparator(component.Separator{}), ), innerComponents...) - if tier == premium.None { - // check if last component is a separator, if not add one - if len(components) == 0 || components[len(components)-1].Type != component.ComponentSeparator { - components = append(components, component.BuildSeparator(component.Separator{})) - } - components = append(components, - component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), - }), - ) + if ctx.PremiumTier() == premium.None { + components = AddPremiumFooter(components) } return component.BuildContainer(component.Container{ @@ -112,12 +96,7 @@ func BuildContainerRaw( ) if tier == premium.None { - components = append(components, - component.BuildSeparator(component.Separator{}), - component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), - }), - ) + components = AddPremiumFooter(components) } return component.BuildContainer(component.Container{ @@ -133,7 +112,7 @@ func AddPremiumFooter(components []component.Component) []component.Component { components = append(components, component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("-# <:tkts_circle:1373407290912276642> Powered by %s", config.Conf.Bot.PoweredBy), + Content: fmt.Sprintf("-# %s Powered by %s", customisation.EmojiLogo, config.Conf.Bot.PoweredBy), }), ) diff --git a/config/config.go b/config/config.go index fa5e10e..8fdaf6f 100644 --- a/config/config.go +++ b/config/config.go @@ -122,22 +122,21 @@ type ( } `envPrefix:"WORKER_CLOUD_PROFILER_"` Emojis struct { + BulletLine uint64 `env:"BULLETLINE" envDefault:"1327350311110574201"` + Claim uint64 `env:"CLAIM" envDefault:"1327350259965235233"` + Close uint64 `env:"CLOSE" envDefault:"1327350171121614870"` + Discord uint64 `env:"DISCORD" envDefault:"1327350329381228544"` Id uint64 `env:"ID" envDefault:"1327350136170479638"` + Logo uint64 `env:"LOGO" envDefault:"1373407290912276642"` Open uint64 `env:"OPEN" envDefault:"1327350149684400268"` OpenTime uint64 `env:"OPENTIME" envDefault:"1327350161206153227"` - Close uint64 `env:"CLOSE" envDefault:"1327350171121614870"` - CloseTime uint64 `env:"CLOSETIME" envDefault:"1327350182806949948"` - Reason uint64 `env:"REASON" envDefault:"1327350192801972224"` - Subject uint64 `env:"SUBJECT" envDefault:"1327350205896458251"` - Transcript uint64 `env:"TRANSCRIPT" envDefault:"1327350249450111068"` - Claim uint64 `env:"CLAIM" envDefault:"1327350259965235233"` Panel uint64 `env:"PANEL" envDefault:"1327350268974600263"` + Patreon uint64 `env:"PATREON" envDefault:"1327350319612690563"` Rating uint64 `env:"RATING" envDefault:"1327350278973952045"` + Reason uint64 `env:"REASON" envDefault:"1327350192801972224"` Staff uint64 `env:"STAFF" envDefault:"1327350290558746674"` Thread uint64 `env:"THREAD" envDefault:"1327350300717355079"` - BulletLine uint64 `env:"BULLETLINE" envDefault:"1327350311110574201"` - Patreon uint64 `env:"PATREON" envDefault:"1327350319612690563"` - Discord uint64 `env:"DISCORD" envDefault:"1327350329381228544"` + Transcript uint64 `env:"TRANSCRIPT" envDefault:"1327350249450111068"` } `envPrefix:"EMOJI_"` VoteSkuId uuid.UUID `env:"VOTE_SKU_ID"` From 00268c577c0f7b8f17d390916d03b83d2db45550 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 22 Jun 2025 21:59:51 +0100 Subject: [PATCH 08/13] update stats commands Signed-off-by: Ben --- bot/command/impl/statistics/statsserver.go | 270 ++++++++++----------- bot/command/impl/statistics/statsuser.go | 141 ++++++++--- bot/utils/messageutils.go | 2 +- go.mod | 5 +- go.sum | 11 - 5 files changed, 244 insertions(+), 185 deletions(-) diff --git a/bot/command/impl/statistics/statsserver.go b/bot/command/impl/statistics/statsserver.go index 7a8dd12..a87727f 100644 --- a/bot/command/impl/statistics/statsserver.go +++ b/bot/command/impl/statistics/statsserver.go @@ -3,19 +3,20 @@ package statistics import ( "fmt" "strconv" + "strings" "time" "github.com/TicketsBot-cloud/analytics-client" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" - "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" "github.com/getsentry/sentry-go" + "golang.org/x/sync/errgroup" ) type StatsServerCommand struct { @@ -43,185 +44,174 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { span.SetTag("guild", strconv.FormatUint(ctx.GuildId(), 10)) defer span.Finish() - // group, _ := errgroup.WithContext(ctx) + group, _ := errgroup.WithContext(ctx) var totalTickets, openTickets uint64 - // totalTickets - // group.Go(func() (err error) { - // span := sentry.StartSpan(span.Context(), "GetTotalTicketCount") - // defer span.Finish() + group.Go(func() (err error) { + span := sentry.StartSpan(span.Context(), "GetTotalTicketCount") + defer span.Finish() - // totalTickets, err = dbclient.Analytics.GetTotalTicketCount(ctx, ctx.GuildId()) - // return - // }) + totalTickets, err = dbclient.Analytics.GetTotalTicketCount(ctx, ctx.GuildId()) + return + }) - // // openTickets - // group.Go(func() error { - // span := sentry.StartSpan(span.Context(), "GetGuildOpenTickets") - // defer span.Finish() + // openTickets + group.Go(func() error { + span := sentry.StartSpan(span.Context(), "GetGuildOpenTickets") + defer span.Finish() - // tickets, err := dbclient.Client.Tickets.GetGuildOpenTickets(ctx, ctx.GuildId()) - // if err != nil { - // return err - // } + tickets, err := dbclient.Client.Tickets.GetGuildOpenTickets(ctx, ctx.GuildId()) + if err != nil { + return err + } - // openTickets = uint64(len(tickets)) - // return nil - // }) + openTickets = uint64(len(tickets)) + return nil + }) var feedbackRating float64 var feedbackCount uint64 - // group.Go(func() (err error) { - // span := sentry.StartSpan(span.Context(), "GetAverageFeedbackRating") - // defer span.Finish() + group.Go(func() (err error) { + span := sentry.StartSpan(span.Context(), "GetAverageFeedbackRating") + defer span.Finish() - // feedbackRating, err = dbclient.Analytics.GetAverageFeedbackRatingGuild(ctx, ctx.GuildId()) - // return - // }) + feedbackRating, err = dbclient.Analytics.GetAverageFeedbackRatingGuild(ctx, ctx.GuildId()) + return + }) - // group.Go(func() (err error) { - // span := sentry.StartSpan(span.Context(), "GetFeedbackCount") - // defer span.Finish() + group.Go(func() (err error) { + span := sentry.StartSpan(span.Context(), "GetFeedbackCount") + defer span.Finish() - // feedbackCount, err = dbclient.Analytics.GetFeedbackCountGuild(ctx, ctx.GuildId()) - // return - // }) + feedbackCount, err = dbclient.Analytics.GetFeedbackCountGuild(ctx, ctx.GuildId()) + return + }) // first response times var firstResponseTime analytics.TripleWindow - // group.Go(func() (err error) { - // span := sentry.StartSpan(span.Context(), "GetFirstResponseTimeStats") - // defer span.Finish() + group.Go(func() (err error) { + span := sentry.StartSpan(span.Context(), "GetFirstResponseTimeStats") + defer span.Finish() - // firstResponseTime, err = dbclient.Analytics.GetFirstResponseTimeStats(ctx, ctx.GuildId()) - // return - // }) + firstResponseTime, err = dbclient.Analytics.GetFirstResponseTimeStats(ctx, ctx.GuildId()) + return + }) - // ticket duration + // // ticket duration var ticketDuration analytics.TripleWindow - // group.Go(func() (err error) { - // span := sentry.StartSpan(span.Context(), "GetTicketDurationStats") - // defer span.Finish() + group.Go(func() (err error) { + span := sentry.StartSpan(span.Context(), "GetTicketDurationStats") + defer span.Finish() - // ticketDuration, err = dbclient.Analytics.GetTicketDurationStats(ctx, ctx.GuildId()) - // return - // }) + ticketDuration, err = dbclient.Analytics.GetTicketDurationStats(ctx, ctx.GuildId()) + return + }) // tickets per day - var ticketVolumeTable string - // group.Go(func() error { - // span := sentry.StartSpan(span.Context(), "GetLastNTicketsPerDayGuild") - // defer span.Finish() - - // counts, err := dbclient.Analytics.GetLastNTicketsPerDayGuild(ctx, ctx.GuildId(), 7) - // if err != nil { - // return err - // } - - // tw := table.NewWriter() - // tw.SetStyle(table.StyleLight) - // tw.Style().Format.Header = text.FormatDefault - - // tw.AppendHeader(table.Row{"Date", "Ticket Volume"}) - // for _, count := range counts { - // tw.AppendRow(table.Row{count.Date.Format("2006-01-02"), count.Count}) - // } - - // ticketVolumeTable = tw.Render() - // return nil - // }) - - // if err := group.Wait(); err != nil { - // ctx.HandleError(err) - // return - // } + var ticketVolumeData []analytics.CountOnDate + group.Go(func() error { + span := sentry.StartSpan(span.Context(), "GetLastNTicketsPerDayGuild") + defer span.Finish() + + counts, err := dbclient.Analytics.GetLastNTicketsPerDayGuild(ctx, ctx.GuildId(), 7) + if err != nil { + return err + } + + ticketVolumeData = counts + return nil + }) + + if err := group.Wait(); err != nil { + ctx.HandleError(err) + return + } span = sentry.StartSpan(span.Context(), "Send Message") - embed.NewEmbed(). - SetTitle("Statistics"). - SetColor(ctx.GetColour(customisation.Green)). - AddField("Total Tickets", strconv.FormatUint(totalTickets, 10), true). - AddField("Open Tickets", strconv.FormatUint(openTickets, 10), true). - AddBlankField(true). - AddField("Feedback Rating", fmt.Sprintf("%.1f / 5 ⭐", feedbackRating), true). - AddField("Feedback Count", strconv.FormatUint(feedbackCount, 10), true). - AddBlankField(true). - AddField("Average First Response Time (Total)", formatNullableTime(firstResponseTime.AllTime), true). - AddField("Average First Response Time (Monthly)", formatNullableTime(firstResponseTime.Monthly), true). - AddField("Average First Response Time (Weekly)", formatNullableTime(firstResponseTime.Weekly), true). - AddField("Average Ticket Duration (Total)", formatNullableTime(ticketDuration.AllTime), true). - AddField("Average Ticket Duration (Monthly)", formatNullableTime(ticketDuration.Monthly), true). - AddField("Average Ticket Duration (Weekly)", formatNullableTime(ticketDuration.Weekly), true). - AddField("Ticket Volume", fmt.Sprintf("```\n%s\n```", ticketVolumeTable), false) + guildData, err := ctx.Guild() + if err != nil { + ctx.HandleError(err) + return + } + + mainStats := []string{ + fmt.Sprintf("**Total Tickets**: %d", totalTickets), + fmt.Sprintf("**Open Tickets**: %d", openTickets), + fmt.Sprintf("**Feedback Rating**: %.1f / 5 ā˜…", feedbackRating), + fmt.Sprintf("**Feedback Count**: %d", feedbackCount), + } + + responseTimeStats := []string{ + fmt.Sprintf("**Total**: %s", formatNullableTime(firstResponseTime.AllTime)), + fmt.Sprintf("**Monthly**: %s", formatNullableTime(firstResponseTime.Monthly)), + fmt.Sprintf("**Weekly**: %s", formatNullableTime(firstResponseTime.Weekly)), + } + + ticketDurationStats := []string{ + fmt.Sprintf("**Total**: %s", formatNullableTime(ticketDuration.AllTime)), + fmt.Sprintf("**Monthly**: %s", formatNullableTime(ticketDuration.Monthly)), + fmt.Sprintf("**Weekly**: %s", formatNullableTime(ticketDuration.Weekly)), + } + + spacers := "\u200e \u200e \u200e \u200e \u200e \u200e \u200e \u200e" + last7DaysStats := make([]string, 0, len(ticketVolumeData)) + + for _, tv := range ticketVolumeData { + date := tv.Date.Format("2006-01-02") + count := fmt.Sprintf("%d", tv.Count) + extraPadding := "" + switch { + case tv.Count < 10: + extraPadding = " \u200e \u200e" + case tv.Count < 100: + extraPadding = " \u200e" + } + last7DaysStats = append(last7DaysStats, + fmt.Sprintf("`%s %s %s\u200e \u200e \u200e \u200e \u200e \u200e \u200e%s`", date, spacers, count, extraPadding), + ) + } innerComponents := []component.Component{ + component.BuildSection(component.Section{ + Accessory: component.BuildThumbnail(component.Thumbnail{ + Media: component.UnfurledMediaItem{ + Url: guildData.IconUrl(), + }, + }), + Components: []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: "## Server Ticket Statistics"}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("` ā— ` %s", strings.Join(mainStats, "\n` ā— ` ")), + }), + }, + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### Average Response Time\n` ā— ` %s", strings.Join(responseTimeStats, "\n` ā— ` ")), + }), + component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ - Content: "### Statistics", + Content: fmt.Sprintf("### Average Ticket Duration\n` ā— ` %s", strings.Join(ticketDurationStats, "\n` ā— ` ")), }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("**Average First Response Time**\n%s", formatAlignedStats([]string{"Weekly", "Monthly", "All Time"}, []string{"1h", "1h", "1h"})), + Content: fmt.Sprintf( + "### Ticket Volume\n__**\u200e \u200e \u200e \u200e \u200e \u200e \u200e Date \u200e \u200e \u200e \u200e \u200e \u200e \u200e | \u200e \u200e \u200e \u200e \u200eTicket Volume\u200e \u200e \u200e \u200e \u200e\u200e**__\n%s", + strings.Join(last7DaysStats, "\n"), + ), }), } + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ Components: innerComponents, })))) - // _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) span.Finish() } func formatNullableTime(duration *time.Duration) string { return utils.FormatNullableTime(duration) } - -func formatAlignedStats(header []string, values []string) string { - maxWidths := make([]int, len(header)) - for i := range header { - hLen := len(header[i]) - vLen := len(values[i]) - maxWidths[i] = max(vLen, hLen) - } - - sep := " / " - - // Build header line - headerLine := "" - for i, h := range header { - headerLine += centerText(h, maxWidths[i]) - if i < len(header)-1 { - headerLine += sep - } - } - - // Build value line - valueLine := "" - for i, v := range values { - valueLine += centerText(v, maxWidths[i]) - if i < len(values)-1 { - valueLine += sep - } - } - - return headerLine + "\n" + valueLine -} - -func centerText(s string, width int) string { - pad := width - len(s) - if pad <= 0 { - return s - } - left := pad / 2 - right := pad - left - return spaces(left) + s + spaces(right) -} - -func spaces(count int) string { - if count <= 0 { - return "" - } - return fmt.Sprintf("%*s", count, "") -} diff --git a/bot/command/impl/statistics/statsuser.go b/bot/command/impl/statistics/statsuser.go index a829c95..4eccfd7 100644 --- a/bot/command/impl/statistics/statsuser.go +++ b/bot/command/impl/statistics/statsuser.go @@ -3,14 +3,14 @@ package statistics import ( "fmt" "strconv" + "strings" "time" "github.com/TicketsBot-cloud/common/permission" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" - "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" @@ -114,17 +114,46 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { span := sentry.StartSpan(span.Context(), "Reply") - _ = embed.NewEmbed(). - SetTitle("Statistics"). - SetColor(ctx.GetColour(customisation.Green)). - SetAuthor(member.User.Username, "", member.User.AvatarUrl(256)). - AddField("Permission Level", "Regular", true). - AddField("Is Blacklisted", strconv.FormatBool(isBlacklisted), true). - AddBlankField(true). - AddField("Total Tickets", strconv.Itoa(totalTickets), true). - AddField("Open Tickets", fmt.Sprintf("%d / %d", openTickets, ticketLimit), true) - - // _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) + userData, err := ctx.Worker().GetUser(userId) + if err != nil { + ctx.HandleError(err) + return + } + + mainStats := []string{ + fmt.Sprintf("**Username**: %s", userData.Username), + fmt.Sprintf("**Permission Level**: Regular"), + fmt.Sprintf("**Is Blacklisted**: %d", strconv.FormatBool(isBlacklisted)), + } + + ticketCountStats := []string{ + fmt.Sprintf("**Total Tickets**: %d", totalTickets), + fmt.Sprintf("**Open Tickets**: %d / %d", openTickets, ticketLimit), + } + + innerComponents := []component.Component{ + component.BuildSection(component.Section{ + Accessory: component.BuildThumbnail(component.Thumbnail{ + Media: component.UnfurledMediaItem{ + Url: userData.AvatarUrl(256), + }, + }), + Components: []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: "## Ticket User Statistics"}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("` ā— ` %s", strings.Join(mainStats, "\n` ā— ` ")), + }), + }, + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### Ticket Count\n` ā— ` %s", strings.Join(ticketCountStats, "\n` ā— ` ")), + }), + } + + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + Components: innerComponents, + })))) span.Finish() } else { // Support rep stats group, _ := errgroup.WithContext(ctx) @@ -275,24 +304,74 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { span := sentry.StartSpan(span.Context(), "Reply") - _ = embed.NewEmbed(). - SetTitle("Statistics"). - SetColor(ctx.GetColour(customisation.Green)). - SetAuthor(member.User.Username, "", member.User.AvatarUrl(256)). - AddField("Permission Level", permissionLevel, true). - AddField("Feedback Rating", fmt.Sprintf("%.1f / 5 ⭐ (%d ratings)", feedbackRating, feedbackCount), true). - AddBlankField(true). - AddField("Average First Response Time (Weekly)", formatNullableTime(weeklyAR), true). - AddField("Average First Response Time (Monthly)", formatNullableTime(monthlyAR), true). - AddField("Average First Response Time (Total)", formatNullableTime(totalAR), true). - AddField("Tickets Answered (Weekly)", fmt.Sprintf("%d / %d", weeklyAnsweredTickets, weeklyTotalTickets), true). - AddField("Tickets Answered (Monthly)", fmt.Sprintf("%d / %d", monthlyAnsweredTickets, monthlyTotalTickets), true). - AddField("Tickets Answered (Total)", fmt.Sprintf("%d / %d", totalAnsweredTickets, totalTotalTickets), true). - AddField("Claimed Tickets (Weekly)", strconv.Itoa(weeklyClaimedTickets), true). - AddField("Claimed Tickets (Monthly)", strconv.Itoa(monthlyClaimedTickets), true). - AddField("Claimed Tickets (Total)", strconv.Itoa(totalClaimedTickets), true) - - // _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) + userData, err := ctx.Worker().GetUser(userId) + if err != nil { + ctx.HandleError(err) + return + } + + mainStats := []string{ + fmt.Sprintf("**Username**: %s", userData.Username), + fmt.Sprintf("**Permission Level**: %s", permissionLevel), + fmt.Sprintf("**Feedback Rating**: %.1f / 5 ā˜…", feedbackRating), + fmt.Sprintf("**Feedback Count**: %d", feedbackCount), + } + + responseTimeStats := []string{ + fmt.Sprintf("**Total**: %s", formatNullableTime(totalAR)), + fmt.Sprintf("**Monthly**: %s", formatNullableTime(monthlyAR)), + fmt.Sprintf("**Weekly**: %s", formatNullableTime(weeklyAR)), + } + + ticketsAnsweredStats := []string{ + fmt.Sprintf("**Total**: %d/%d", totalAnsweredTickets, totalTotalTickets), + fmt.Sprintf("**Monthly**: %d/%d", monthlyAnsweredTickets, monthlyTotalTickets), + fmt.Sprintf("**Weekly**: %d/%d", weeklyAnsweredTickets, weeklyTotalTickets), + } + + claimedStats := []string{ + fmt.Sprintf("**Total**: %d", totalClaimedTickets), + fmt.Sprintf("**Monthly**: %d", monthlyClaimedTickets), + fmt.Sprintf("**Weekly**: %d", weeklyClaimedTickets), + } + + innerComponents := []component.Component{ + component.BuildSection(component.Section{ + Accessory: component.BuildThumbnail(component.Thumbnail{ + Media: component.UnfurledMediaItem{ + Url: userData.AvatarUrl(256), + }, + }), + Components: []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: "## Ticket User Statistics"}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("` ā— ` %s", strings.Join(mainStats, "\n` ā— ` ")), + }), + }, + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### Average Response Time\n` ā— ` %s", strings.Join(responseTimeStats, "\n` ā— ` ")), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf( + "### Tickets Answered\n` ā— ` %s", + strings.Join(ticketsAnsweredStats, "\n` ā— ` "), + ), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf( + "### Claimed Tickets\n` ā— ` %s", + strings.Join(claimedStats, "\n` ā— ` "), + ), + }), + } + + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + Components: innerComponents, + })))) span.Finish() } } diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index f4bc01d..201b9c6 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/TicketsBot-cloud/common/premium" + "github.com/TicketsBot-cloud/common/utils" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker" @@ -14,7 +15,6 @@ import ( "github.com/TicketsBot-cloud/worker/bot/model" "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" - "github.com/TicketsBot/common/utils" ) func BuildContainer(ctx registry.CommandContext, colour customisation.Colour, titleId, contentId i18n.MessageId, format ...interface{}) component.Component { diff --git a/go.mod b/go.mod index 02327d6..ebeaebc 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,11 @@ go 1.23.8 toolchain go1.24.2 replace github.com/TicketsBot-cloud/database => ../database + replace github.com/TicketsBot-cloud/gdl => ../gdl + replace github.com/TicketsBot-cloud/archiverclient => ../archiverclient + replace github.com/TicketsBot-cloud/logarchiver => ../logarchiver require ( @@ -24,7 +27,6 @@ require ( github.com/go-redsync/redsync/v4 v4.13.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v4 v4.18.3 - github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/joho/godotenv v1.5.1 github.com/json-iterator/go v1.1.12 github.com/pkg/errors v0.9.1 @@ -98,7 +100,6 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/crc64nvme v1.0.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/minio-go/v7 v7.0.91 // indirect diff --git a/go.sum b/go.sum index 8584d30..8282e8f 100644 --- a/go.sum +++ b/go.sum @@ -31,16 +31,8 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/ReneKroon/ttlcache v1.6.0/go.mod h1:DG6nbhXKUQhrExfwwLuZUdH7UnRDDRA1IW+nBuCssvs= github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c h1:0pKR6drN8yc7dSQJcoNop9G5ywuzTZzLgD9Ktp7AvJo= github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c/go.mod h1:zecIz09jVDSHyhV6NYgTko0NEN0QJGiZbzcxHRjQLzc= -github.com/TicketsBot-cloud/archiverclient v0.0.0-20250514201416-cf23f65eb3fc h1:2EFgb6gQMedsO7YkqUPH1zY8+L6i3sGuV/YAv0gPqhQ= -github.com/TicketsBot-cloud/archiverclient v0.0.0-20250514201416-cf23f65eb3fc/go.mod h1:Mux1bEPpOHwRw1wo6Fa6qJLJH9Erk9qv1yAIfLi1Wmw= github.com/TicketsBot-cloud/common v0.0.0-20250509064208-a2d357175463 h1:ZO2kw9lpsy4umoWFmAt9WKVFb3WvU5oSif2Yw4K7Lj4= github.com/TicketsBot-cloud/common v0.0.0-20250509064208-a2d357175463/go.mod h1:PL5j/omFvU0NeyTKCESmOF+3GscaEuM0aqmI4yFcCFY= -github.com/TicketsBot-cloud/database v0.0.0-20250601094419-c168a11bd60a h1:UrqHaVS3SkzoKd19xLFdJZlLv3kZ1T3IR22shlRx43s= -github.com/TicketsBot-cloud/database v0.0.0-20250601094419-c168a11bd60a/go.mod h1:LPDEn9e5wccH7rq/pUlVcL3UhyLnnwdM2dhj0tp/ljo= -github.com/TicketsBot-cloud/gdl v0.0.0-20250509054940-2045fbe19c06 h1:PzziB2S58d9agJtpaPVrYMTuBiJICr2QIGQoqL6l3z0= -github.com/TicketsBot-cloud/gdl v0.0.0-20250509054940-2045fbe19c06/go.mod h1:CdwBR2egPtxUXjD2CgC9ZwfuB8dz9HPePM8nuG6dt7Y= -github.com/TicketsBot-cloud/logarchiver v0.0.0-20250514201320-d5141071a6eb h1:BapforRlvTfWP8MX8DTsxVM40oDgQorJVo/cnNGTaKU= -github.com/TicketsBot-cloud/logarchiver v0.0.0-20250514201320-d5141071a6eb/go.mod h1:pZqkzPNNTqnwKZvCT8kCaTHxrG7HJbxZV83S0p7mmzM= github.com/TicketsBot/common v0.0.0-20241117150316-ff54c97b45c1 h1:FqC1KGOsmB+ikvbmDkyNQU6bGUWyfYq8Ip9r4KxTveY= github.com/TicketsBot/common v0.0.0-20241117150316-ff54c97b45c1/go.mod h1:N7zwetwx8B3RK/ZajWwMroJSyv2ZJ+bIOZWv/z8DhaM= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM= @@ -226,8 +218,6 @@ github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= -github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -326,7 +316,6 @@ github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLB github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= From 2c1f144b05a6a68639b027cb9f3d9ee5f792ff4f Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 22 Jun 2025 22:03:21 +0100 Subject: [PATCH 09/13] update stats commands Signed-off-by: Ben --- bot/command/impl/statistics/statsuser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/command/impl/statistics/statsuser.go b/bot/command/impl/statistics/statsuser.go index 4eccfd7..adea996 100644 --- a/bot/command/impl/statistics/statsuser.go +++ b/bot/command/impl/statistics/statsuser.go @@ -122,8 +122,8 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { mainStats := []string{ fmt.Sprintf("**Username**: %s", userData.Username), - fmt.Sprintf("**Permission Level**: Regular"), - fmt.Sprintf("**Is Blacklisted**: %d", strconv.FormatBool(isBlacklisted)), + "**Permission Level**: Regular", + fmt.Sprintf("**Is Blacklisted**: %s", strconv.FormatBool(isBlacklisted)), } ticketCountStats := []string{ From 13596665a6433eda24f77370323e78d5e2f171f2 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 22 Jun 2025 22:46:50 +0100 Subject: [PATCH 10/13] add recache button Signed-off-by: Ben --- bot/button/handlers/recache.go | 128 +++++++++++++++++++++++++ bot/button/manager/manager.go | 1 + bot/command/impl/admin/admindebug.go | 5 + bot/command/impl/admin/adminrecache.go | 21 ++++ bot/redis/recache_ratelimit.go | 46 +++++++++ 5 files changed, 201 insertions(+) create mode 100644 bot/button/handlers/recache.go create mode 100644 bot/redis/recache_ratelimit.go diff --git a/bot/button/handlers/recache.go b/bot/button/handlers/recache.go new file mode 100644 index 0000000..4f60ecf --- /dev/null +++ b/bot/button/handlers/recache.go @@ -0,0 +1,128 @@ +package handlers + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + permcache "github.com/TicketsBot-cloud/common/permission" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" + w "github.com/TicketsBot-cloud/worker" + "github.com/TicketsBot-cloud/worker/bot/button/registry" + "github.com/TicketsBot-cloud/worker/bot/button/registry/matcher" + "github.com/TicketsBot-cloud/worker/bot/command" + "github.com/TicketsBot-cloud/worker/bot/command/context" + "github.com/TicketsBot-cloud/worker/bot/customisation" + "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/redis" + "github.com/TicketsBot-cloud/worker/bot/utils" +) + +type RecacheHandler struct{} + +func (h *RecacheHandler) Matcher() matcher.Matcher { + return matcher.NewFuncMatcher(func(customId string) bool { + return strings.HasPrefix(customId, "admin_debug_recache") + }) +} + +func (h *RecacheHandler) Properties() registry.Properties { + return registry.Properties{ + Flags: registry.SumFlags(registry.GuildAllowed, registry.CanEdit), + Timeout: time.Second * 30, + PermissionLevel: permcache.Support, + } +} + +func (h *RecacheHandler) Execute(ctx *context.ButtonContext) { + guildId, err := strconv.ParseUint(strings.Replace(ctx.InteractionData.CustomId, "admin_debug_recache_", "", -1), 10, 64) + + if onCooldown, cooldownTime := redis.GetRecacheCooldown(guildId); onCooldown { + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerNoLocale( + ctx, + customisation.Red, + "Admin - Recache", + []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("Recache for this guild is on cooldown. Please wait until it is available again.\n\n**Cooldown ends** ", cooldownTime.Unix()), + }), + }, + )})) + return + } + currentTime := time.Now() + + // purge cache + ctx.Worker().Cache.DeleteGuild(ctx, guildId) + ctx.Worker().Cache.DeleteGuildChannels(ctx, guildId) + ctx.Worker().Cache.DeleteGuildRoles(ctx, guildId) + + botId, isWhitelabel, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, guildId) + if err != nil { + ctx.HandleError(err) + return + } + + var worker *w.Context + if isWhitelabel { + bot, err := dbclient.Client.Whitelabel.GetByBotId(ctx, botId) + if err != nil { + ctx.HandleError(err) + return + } + + if bot.BotId == 0 { + ctx.HandleError(errors.New("bot not found")) + return + } + + worker = &w.Context{ + Token: bot.Token, + BotId: bot.BotId, + IsWhitelabel: true, + ShardId: 0, + Cache: ctx.Worker().Cache, + RateLimiter: nil, // Use http-proxy ratelimit functionality + } + } else { + worker = ctx.Worker() + } + + guild, err := worker.GetGuild(guildId) + if err != nil { + ctx.HandleError(err) + return + } + + guildChannels, err := worker.GetGuildChannels(guildId) + if err != nil { + ctx.HandleError(err) + return + } + + // Set the recache cooldown + if err := redis.SetRecacheCooldown(guildId, time.Second*30); err != nil { + ctx.HandleError(err) + return + } + + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerNoLocale( + ctx, + customisation.Orange, + "Admin - Recache", + []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("**%s** has been recached successfully.\n\n**Guild ID:** %d\n**Time Taken:** %s", guild.Name, guildId, time.Since(currentTime).Round(time.Millisecond)), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### Cache Stats\n**Channels:** `%d`\n**Roles:** `%d`", len(guildChannels), len(guild.Roles)), + }), + }, + ), + })) +} diff --git a/bot/button/manager/manager.go b/bot/button/manager/manager.go index b894c6a..8febb78 100644 --- a/bot/button/manager/manager.go +++ b/bot/button/manager/manager.go @@ -67,6 +67,7 @@ func (m *ComponentInteractionManager) RegisterCommands() { new(handlers.ViewStaffHandler), new(handlers.ViewSurveyHandler), new(handlers.HelpPageHandler), + new(handlers.RecacheHandler), ) m.selectRegistry = append(m.selectRegistry, diff --git a/bot/command/impl/admin/admindebug.go b/bot/command/impl/admin/admindebug.go index 7d8b3cf..347005b 100644 --- a/bot/command/impl/admin/admindebug.go +++ b/bot/command/impl/admin/admindebug.go @@ -199,5 +199,10 @@ func (AdminDebugCommand) Execute(ctx registry.CommandContext, raw string) { strings.Join(debugResponse, "\n\n"), ctx.PremiumTier(), ), + component.BuildActionRow(component.BuildButton(component.Button{ + Label: "Recache Guild", + Style: component.ButtonStylePrimary, + CustomId: fmt.Sprintf("admin_debug_recache_%d", guild.Id), + })), })) } diff --git a/bot/command/impl/admin/adminrecache.go b/bot/command/impl/admin/adminrecache.go index 16e6e65..67a169a 100644 --- a/bot/command/impl/admin/adminrecache.go +++ b/bot/command/impl/admin/adminrecache.go @@ -14,6 +14,7 @@ import ( "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/dbclient" + "github.com/TicketsBot-cloud/worker/bot/redis" "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -53,6 +54,21 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, providedGuildId guildId = ctx.GuildId() } + if onCooldown, cooldownTime := redis.GetRecacheCooldown(guildId); onCooldown { + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerNoLocale( + ctx, + customisation.Red, + "Admin - Recache", + []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("Recache for this guild is on cooldown. Please wait until it is available again.\n\n**Cooldown ends** ", cooldownTime.Unix()), + }), + }, + )})) + return + } + currentTime := time.Now() // purge cache @@ -104,6 +120,11 @@ func (AdminRecacheCommand) Execute(ctx registry.CommandContext, providedGuildId return } + if err := redis.SetRecacheCooldown(guildId, time.Second*30); err != nil { + ctx.HandleError(err) + return + } + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ utils.BuildContainerNoLocale( ctx, diff --git a/bot/redis/recache_ratelimit.go b/bot/redis/recache_ratelimit.go new file mode 100644 index 0000000..57cea50 --- /dev/null +++ b/bot/redis/recache_ratelimit.go @@ -0,0 +1,46 @@ +package redis + +import ( + "fmt" + "time" + + "github.com/TicketsBot-cloud/common/utils" + "github.com/go-redis/redis/v8" +) + +// Returns nil if we cannot create a channel +// Returns ErrNotCached if not cached +func GetRecacheCooldown(guildId uint64) (bool, time.Time) { + key := fmt.Sprintf("admin:recache:%d", guildId) + + isOnCooldown, err := Client.Get(utils.DefaultContext(), key).Bool() + if err != nil { + if err == redis.Nil { + return false, time.Time{} + } + + return false, time.Time{} + } + + if isOnCooldown { + res, err := Client.TTL(utils.DefaultContext(), key).Result() + if err != nil { + return false, time.Time{} + } + + if res < 0 { + return false, time.Time{} + } + + return true, time.Now().Add(res) + } + + return false, time.Time{} +} + +func SetRecacheCooldown(guildId uint64, duration time.Duration) error { + key := fmt.Sprintf("admin:recache:%d", guildId) + + // Set the cooldown to true and set the expiration time + return Client.Set(utils.DefaultContext(), key, true, duration).Err() +} From bec9946e5e718081bc7b6db25cb5ec63a52dc57e Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 28 Jun 2025 21:38:25 +0100 Subject: [PATCH 11/13] update stats commands Signed-off-by: Ben --- bot/command/impl/statistics/statsserver.go | 57 ++++++++++------------ bot/command/impl/statistics/statsuser.go | 16 +++--- go.mod | 4 ++ go.sum | 5 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/bot/command/impl/statistics/statsserver.go b/bot/command/impl/statistics/statsserver.go index a87727f..7734bc5 100644 --- a/bot/command/impl/statistics/statsserver.go +++ b/bot/command/impl/statistics/statsserver.go @@ -16,6 +16,8 @@ import ( "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" "github.com/getsentry/sentry-go" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" "golang.org/x/sync/errgroup" ) @@ -44,9 +46,16 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { span.SetTag("guild", strconv.FormatUint(ctx.GuildId(), 10)) defer span.Finish() - group, _ := errgroup.WithContext(ctx) + var ( + totalTickets, openTickets uint64 + feedbackRating float64 + feedbackCount uint64 + firstResponseTime analytics.TripleWindow + ticketDuration analytics.TripleWindow + ticketVolumeData []analytics.CountOnDate + ) - var totalTickets, openTickets uint64 + group, _ := errgroup.WithContext(ctx) group.Go(func() (err error) { span := sentry.StartSpan(span.Context(), "GetTotalTicketCount") @@ -56,7 +65,7 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { return }) - // openTickets + // open tickets group.Go(func() error { span := sentry.StartSpan(span.Context(), "GetGuildOpenTickets") defer span.Finish() @@ -70,9 +79,6 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { return nil }) - var feedbackRating float64 - var feedbackCount uint64 - group.Go(func() (err error) { span := sentry.StartSpan(span.Context(), "GetAverageFeedbackRating") defer span.Finish() @@ -90,7 +96,6 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { }) // first response times - var firstResponseTime analytics.TripleWindow group.Go(func() (err error) { span := sentry.StartSpan(span.Context(), "GetFirstResponseTimeStats") defer span.Finish() @@ -99,8 +104,7 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { return }) - // // ticket duration - var ticketDuration analytics.TripleWindow + // ticket duration group.Go(func() (err error) { span := sentry.StartSpan(span.Context(), "GetTicketDurationStats") defer span.Finish() @@ -110,7 +114,6 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { }) // tickets per day - var ticketVolumeData []analytics.CountOnDate group.Go(func() error { span := sentry.StartSpan(span.Context(), "GetLastNTicketsPerDayGuild") defer span.Finish() @@ -155,25 +158,17 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { fmt.Sprintf("**Monthly**: %s", formatNullableTime(ticketDuration.Monthly)), fmt.Sprintf("**Weekly**: %s", formatNullableTime(ticketDuration.Weekly)), } + tw := table.NewWriter() + tw.SetStyle(table.StyleLight) + tw.Style().Format.Header = text.FormatDefault - spacers := "\u200e \u200e \u200e \u200e \u200e \u200e \u200e \u200e" - last7DaysStats := make([]string, 0, len(ticketVolumeData)) - - for _, tv := range ticketVolumeData { - date := tv.Date.Format("2006-01-02") - count := fmt.Sprintf("%d", tv.Count) - extraPadding := "" - switch { - case tv.Count < 10: - extraPadding = " \u200e \u200e" - case tv.Count < 100: - extraPadding = " \u200e" - } - last7DaysStats = append(last7DaysStats, - fmt.Sprintf("`%s %s %s\u200e \u200e \u200e \u200e \u200e \u200e \u200e%s`", date, spacers, count, extraPadding), - ) + tw.AppendHeader(table.Row{"Date", "Ticket Volume"}) + for _, count := range ticketVolumeData { + tw.AppendRow(table.Row{count.Date.Format("2006-01-02"), count.Count}) } + ticketVolumeTable := tw.Render() + innerComponents := []component.Component{ component.BuildSection(component.Section{ Accessory: component.BuildThumbnail(component.Thumbnail{ @@ -184,23 +179,23 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { Components: []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: "## Server Ticket Statistics"}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("` ā— ` %s", strings.Join(mainStats, "\n` ā— ` ")), + Content: fmt.Sprintf("ā— %s", strings.Join(mainStats, "\nā— ")), }), }, }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("### Average Response Time\n` ā— ` %s", strings.Join(responseTimeStats, "\n` ā— ` ")), + Content: fmt.Sprintf("### Average Response Time\nā— %s", strings.Join(responseTimeStats, "\nā— ")), }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("### Average Ticket Duration\n` ā— ` %s", strings.Join(ticketDurationStats, "\n` ā— ` ")), + Content: fmt.Sprintf("### Average Ticket Duration\nā— %s", strings.Join(ticketDurationStats, "\nā— ")), }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ Content: fmt.Sprintf( - "### Ticket Volume\n__**\u200e \u200e \u200e \u200e \u200e \u200e \u200e Date \u200e \u200e \u200e \u200e \u200e \u200e \u200e | \u200e \u200e \u200e \u200e \u200eTicket Volume\u200e \u200e \u200e \u200e \u200e\u200e**__\n%s", - strings.Join(last7DaysStats, "\n"), + "### Ticket Volume\n```\n%s\n```", + ticketVolumeTable, ), }), } diff --git a/bot/command/impl/statistics/statsuser.go b/bot/command/impl/statistics/statsuser.go index adea996..2590806 100644 --- a/bot/command/impl/statistics/statsuser.go +++ b/bot/command/impl/statistics/statsuser.go @@ -141,13 +141,13 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { Components: []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: "## Ticket User Statistics"}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("` ā— ` %s", strings.Join(mainStats, "\n` ā— ` ")), + Content: fmt.Sprintf("ā— %s", strings.Join(mainStats, "\nā— ")), }), }, }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("### Ticket Count\n` ā— ` %s", strings.Join(ticketCountStats, "\n` ā— ` ")), + Content: fmt.Sprintf("### Ticket Count\nā— %s", strings.Join(ticketCountStats, "\nā— ")), }), } @@ -345,26 +345,26 @@ func (StatsUserCommand) Execute(ctx registry.CommandContext, userId uint64) { Components: []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: "## Ticket User Statistics"}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("` ā— ` %s", strings.Join(mainStats, "\n` ā— ` ")), + Content: fmt.Sprintf("ā— %s", strings.Join(mainStats, "\nā— ")), }), }, }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("### Average Response Time\n` ā— ` %s", strings.Join(responseTimeStats, "\n` ā— ` ")), + Content: fmt.Sprintf("### Average Response Time\nā— %s", strings.Join(responseTimeStats, "\nā— ")), }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ Content: fmt.Sprintf( - "### Tickets Answered\n` ā— ` %s", - strings.Join(ticketsAnsweredStats, "\n` ā— ` "), + "### Tickets Answered\nā— %s", + strings.Join(ticketsAnsweredStats, "\nā— "), ), }), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ Content: fmt.Sprintf( - "### Claimed Tickets\n` ā— ` %s", - strings.Join(claimedStats, "\n` ā— ` "), + "### Claimed Tickets\nā— %s", + strings.Join(claimedStats, "\nā— "), ), }), } diff --git a/go.mod b/go.mod index ebeaebc..21edd2e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ replace github.com/TicketsBot-cloud/archiverclient => ../archiverclient replace github.com/TicketsBot-cloud/logarchiver => ../logarchiver +replace github.com/TicketsBot-cloud/common => ../common + require ( cloud.google.com/go/profiler v0.4.2 github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c @@ -27,6 +29,7 @@ require ( github.com/go-redsync/redsync/v4 v4.13.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v4 v4.18.3 + github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/joho/godotenv v1.5.1 github.com/json-iterator/go v1.1.12 github.com/pkg/errors v0.9.1 @@ -100,6 +103,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/crc64nvme v1.0.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/minio-go/v7 v7.0.91 // indirect diff --git a/go.sum b/go.sum index 8282e8f..2ebd3a6 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,6 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/ReneKroon/ttlcache v1.6.0/go.mod h1:DG6nbhXKUQhrExfwwLuZUdH7UnRDDRA1IW+nBuCssvs= github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c h1:0pKR6drN8yc7dSQJcoNop9G5ywuzTZzLgD9Ktp7AvJo= github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c/go.mod h1:zecIz09jVDSHyhV6NYgTko0NEN0QJGiZbzcxHRjQLzc= -github.com/TicketsBot-cloud/common v0.0.0-20250509064208-a2d357175463 h1:ZO2kw9lpsy4umoWFmAt9WKVFb3WvU5oSif2Yw4K7Lj4= -github.com/TicketsBot-cloud/common v0.0.0-20250509064208-a2d357175463/go.mod h1:PL5j/omFvU0NeyTKCESmOF+3GscaEuM0aqmI4yFcCFY= github.com/TicketsBot/common v0.0.0-20241117150316-ff54c97b45c1 h1:FqC1KGOsmB+ikvbmDkyNQU6bGUWyfYq8Ip9r4KxTveY= github.com/TicketsBot/common v0.0.0-20241117150316-ff54c97b45c1/go.mod h1:N7zwetwx8B3RK/ZajWwMroJSyv2ZJ+bIOZWv/z8DhaM= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM= @@ -218,6 +216,8 @@ github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= +github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -316,6 +316,7 @@ github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLB github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= From 7d66d3a87e0c76f12b705f6f6f3ab7cab9f6e1ee Mon Sep 17 00:00:00 2001 From: Tobias <53872542+biast12@users.noreply.github.com> Date: Fri, 4 Jul 2025 22:24:44 +0200 Subject: [PATCH 12/13] CV2: refactor component variable names and improve blacklist logic (#21) Refactor component variable names and improve blacklist logic Renames local variables for component slices to 'innerComponents' across multiple files for clarity and consistency. Refactors the blacklist command to separate user and role blacklist logic into dedicated handler functions, unifies response formatting, and fixes role blacklist count checks. Updates help and welcome message logic to use new container utilities. Adds a new i18n message for tickets started from messages with no content and improves the premium footer link formatting. --- bot/button/handlers/viewsurvey.go | 6 +- .../impl/admin/adminlistguildentitlements.go | 6 +- .../impl/admin/adminlistuserentitlements.go | 6 +- bot/command/impl/admin/adminwhitelabeldata.go | 4 +- bot/command/impl/general/jumptotop.go | 4 +- bot/command/impl/settings/blacklist.go | 212 +++++++++--------- bot/command/impl/settings/premium.go | 6 +- bot/command/impl/tickets/startticket.go | 18 +- bot/logic/closeembed.go | 6 +- bot/logic/help.go | 39 ++-- bot/logic/viewstaff.go | 16 +- bot/logic/welcomemessage.go | 6 +- bot/utils/messageutils.go | 2 +- i18n/messages.go | 37 ++- 14 files changed, 186 insertions(+), 182 deletions(-) diff --git a/bot/button/handlers/viewsurvey.go b/bot/button/handlers/viewsurvey.go index b30397f..e90a206 100644 --- a/bot/button/handlers/viewsurvey.go +++ b/bot/button/handlers/viewsurvey.go @@ -120,7 +120,7 @@ func (h *ViewSurveyHandler) Execute(ctx *context.ButtonContext) { buttons = append(buttons, logic.TranscriptLinkElement(ticket.HasTranscript)(ctx.Worker(), ticket)...) buttons = append(buttons, logic.ThreadLinkElement(ticket.ChannelId != nil && ticket.IsThread)(ctx.Worker(), ticket)...) - comps := []component.Component{ + innerComponents := []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("## Exit Survey for %s", opener.GlobalName)}), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ @@ -129,11 +129,11 @@ func (h *ViewSurveyHandler) Execute(ctx *context.ButtonContext) { } if len(buttons) > 0 { - comps = append(comps, component.BuildActionRow(buttons...)) + innerComponents = append(innerComponents, component.BuildActionRow(buttons...)) } ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), - Components: comps, + Components: innerComponents, })))) } diff --git a/bot/command/impl/admin/adminlistguildentitlements.go b/bot/command/impl/admin/adminlistguildentitlements.go index 767b282..0bb4dc7 100644 --- a/bot/command/impl/admin/adminlistguildentitlements.go +++ b/bot/command/impl/admin/adminlistguildentitlements.go @@ -97,7 +97,7 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu return } - values := []component.Component{} + innerComponents := []component.Component{} for _, entitlement := range entitlements { value := fmt.Sprintf( @@ -110,7 +110,7 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu entitlement.SkuPriority, ) - values = append(values, component.BuildTextDisplay(component.TextDisplay{Content: value})) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: value})) } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ @@ -118,7 +118,7 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu ctx, customisation.Orange, i18n.Admin, - values, + innerComponents, ), })) } diff --git a/bot/command/impl/admin/adminlistuserentitlements.go b/bot/command/impl/admin/adminlistuserentitlements.go index 12885d4..13b5e24 100644 --- a/bot/command/impl/admin/adminlistuserentitlements.go +++ b/bot/command/impl/admin/adminlistuserentitlements.go @@ -50,7 +50,7 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use return } - values := []component.Component{} + innerComponents := []component.Component{} for _, entitlement := range entitlements { @@ -64,7 +64,7 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use entitlement.SkuPriority, ) - values = append(values, component.BuildTextDisplay(component.TextDisplay{Content: value})) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: value})) } ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ @@ -72,7 +72,7 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use ctx, customisation.Orange, i18n.Admin, - values, + innerComponents, ), })) } diff --git a/bot/command/impl/admin/adminwhitelabeldata.go b/bot/command/impl/admin/adminwhitelabeldata.go index f47b949..7d16832 100644 --- a/bot/command/impl/admin/adminwhitelabeldata.go +++ b/bot/command/impl/admin/adminwhitelabeldata.go @@ -127,7 +127,7 @@ func (AdminWhitelabelDataCommand) Execute(ctx registry.CommandContext, userId ui tds += fmt.Sprintf("**%s:** %s\n", fields[i].Name, fields[i].Value) } - comps := []component.Component{ + innerComponents := []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: "## Whitelabel"}), component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{ @@ -137,6 +137,6 @@ func (AdminWhitelabelDataCommand) Execute(ctx registry.CommandContext, userId ui ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), - Components: comps, + Components: innerComponents, })))) } diff --git a/bot/command/impl/general/jumptotop.go b/bot/command/impl/general/jumptotop.go index 8c5a5b8..336c961 100644 --- a/bot/command/impl/general/jumptotop.go +++ b/bot/command/impl/general/jumptotop.go @@ -52,7 +52,7 @@ func (JumpToTopCommand) Execute(ctx registry.CommandContext) { } messageLink := fmt.Sprintf("https://discord.com/channels/%d/%d/%d", ctx.GuildId(), ctx.ChannelId(), *ticket.WelcomeMessageId) - components := []component.Component{ + innerComponents := []component.Component{ component.BuildTextDisplay(component.TextDisplay{Content: ctx.GetMessage(i18n.MessageJumpToTopContent)}), component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), component.BuildActionRow(component.BuildButton(component.Button{ @@ -64,7 +64,7 @@ func (JumpToTopCommand) Execute(ctx registry.CommandContext) { } if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ - utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleJumpToTop, components), + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleJumpToTop, innerComponents), })); err != nil { ctx.HandleError(err) return diff --git a/bot/command/impl/settings/blacklist.go b/bot/command/impl/settings/blacklist.go index f30d052..81f29ff 100644 --- a/bot/command/impl/settings/blacklist.go +++ b/bot/command/impl/settings/blacklist.go @@ -52,138 +52,148 @@ func (BlacklistCommand) Execute(ctx registry.CommandContext, id uint64) { return } - if mentionableType == context.MentionableTypeUser { - member, err := ctx.Worker().GetGuildMember(ctx.GuildId(), id) - if err != nil { - ctx.HandleError(err) - return - } + switch mentionableType { + case context.MentionableTypeUser: + BlacklistCommand{}.handleUserBlacklist(ctx, id, usageEmbed) + case context.MentionableTypeRole: + BlacklistCommand{}.handleRoleBlacklist(ctx, id, usageEmbed) + default: + ctx.HandleError(fmt.Errorf("invalid mentionable type")) + } +} + +func (BlacklistCommand) handleUserBlacklist(ctx registry.CommandContext, id uint64, usageEmbed model.Field) { + member, err := ctx.Worker().GetGuildMember(ctx.GuildId(), id) + if err != nil { + ctx.HandleError(err) + return + } + + if ctx.UserId() == id { + ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistSelf, utils.ToSlice(usageEmbed)) + return + } - if ctx.UserId() == id { - ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistSelf, utils.ToSlice(usageEmbed)) + permLevel, err := permission.GetPermissionLevel(ctx, utils.ToRetriever(ctx.Worker()), member, ctx.GuildId()) + if err != nil { + ctx.HandleError(err) + return + } + + if permLevel > permission.Everyone { + ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) + return + } + + isBlacklisted, err := dbclient.Client.Blacklist.IsBlacklisted(ctx, ctx.GuildId(), id) + if err != nil { + sentry.ErrorWithContext(err, ctx.ToErrorContext()) + return + } + + if isBlacklisted { + if err := dbclient.Client.Blacklist.Remove(ctx, ctx.GuildId(), id); err != nil { + ctx.HandleError(err) return } - - permLevel, err := permission.GetPermissionLevel(ctx, utils.ToRetriever(ctx.Worker()), member, ctx.GuildId()) + } else { + // Check user blacklist limit + count, err := dbclient.Client.Blacklist.GetBlacklistedCount(ctx, ctx.GuildId()) if err != nil { ctx.HandleError(err) return } - if permLevel > permission.Everyone { - ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) + if count >= 250 { + ctx.Reply(customisation.Red, i18n.Error, i18n.MessageBlacklistLimit, 250) return } - isBlacklisted, err := dbclient.Client.Blacklist.IsBlacklisted(ctx, ctx.GuildId(), id) - if err != nil { - sentry.ErrorWithContext(err, ctx.ToErrorContext()) + if err := dbclient.Client.Blacklist.Add(ctx, ctx.GuildId(), member.User.Id); err != nil { + ctx.HandleError(err) return } + } - blacklistMsg := i18n.MessageBlacklistRemove - - if isBlacklisted { - if err := dbclient.Client.Blacklist.Remove(ctx, ctx.GuildId(), id); err != nil { - ctx.HandleError(err) - return - } - } else { - // Limit of 250 *users* - count, err := dbclient.Client.Blacklist.GetBlacklistedCount(ctx, ctx.GuildId()) - if err != nil { - ctx.HandleError(err) - return - } - - if count >= 250 { - ctx.Reply(customisation.Red, i18n.Error, i18n.MessageBlacklistLimit, 250) - return - } - - if err := dbclient.Client.Blacklist.Add(ctx, ctx.GuildId(), member.User.Id); err != nil { - ctx.HandleError(err) - return - } - blacklistMsg = i18n.MessageBlacklistAdd - } + BlacklistCommand{}.sendBlacklistResponse(ctx, id, isBlacklisted, true) +} - ctx.ReplyWith( - command.NewEphemeralMessageResponseWithComponents( - utils.Slice( - utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleBlacklist), ctx.GetMessage(blacklistMsg, id), ctx.PremiumTier()), - ), - ), - ) - } else if mentionableType == context.MentionableTypeRole { - // Check if role is staff - isSupport, err := dbclient.Client.RolePermissions.IsSupport(ctx, id) - if err != nil { - ctx.HandleError(err) - return - } +func (BlacklistCommand) handleRoleBlacklist(ctx registry.CommandContext, id uint64, usageEmbed model.Field) { + // Check if role is support role + isSupport, err := dbclient.Client.RolePermissions.IsSupport(ctx, id) + if err != nil { + ctx.HandleError(err) + return + } + + if isSupport { + ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) + return + } - if isSupport { - ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) // TODO: Does this need a new message? + // Check if role is part of any support team + isSupport, err = dbclient.Client.SupportTeamRoles.IsSupport(ctx, ctx.GuildId(), id) + if err != nil { + ctx.HandleError(err) + return + } + + if isSupport { + ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) + return + } + + isBlacklisted, err := dbclient.Client.RoleBlacklist.IsBlacklisted(ctx, ctx.GuildId(), id) + if err != nil { + ctx.HandleError(err) + return + } + + if isBlacklisted { + if err := dbclient.Client.RoleBlacklist.Remove(ctx, ctx.GuildId(), id); err != nil { + ctx.HandleError(err) return } - - // Check if staff is part of any team - isSupport, err = dbclient.Client.SupportTeamRoles.IsSupport(ctx, ctx.GuildId(), id) + } else { + // Check role blacklist limit - use RoleBlacklist instead of Blacklist + count, err := dbclient.Client.RoleBlacklist.GetBlacklistedCount(ctx, ctx.GuildId()) if err != nil { ctx.HandleError(err) return } - if isSupport { - ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) // TODO: Does this need a new message? + if count >= 50 { + ctx.Reply(customisation.Red, i18n.Error, i18n.MessageBlacklistRoleLimit, 50) return } - isBlacklisted, err := dbclient.Client.RoleBlacklist.IsBlacklisted(ctx, ctx.GuildId(), id) - if err != nil { + if err := dbclient.Client.RoleBlacklist.Add(ctx, ctx.GuildId(), id); err != nil { ctx.HandleError(err) return } + } - blacklistMsg := i18n.MessageBlacklistRemoveRole - - if isBlacklisted { - if err := dbclient.Client.RoleBlacklist.Remove(ctx, ctx.GuildId(), id); err != nil { - ctx.HandleError(err) - return - } - } else { - // Limit of 50 *roles* - count, err := dbclient.Client.Blacklist.GetBlacklistedCount(ctx, ctx.GuildId()) - if err != nil { - ctx.HandleError(err) - return - } - - if count >= 50 { - ctx.Reply(customisation.Red, i18n.Error, i18n.MessageBlacklistRoleLimit, 50) - return - } - - if err := dbclient.Client.RoleBlacklist.Add(ctx, ctx.GuildId(), id); err != nil { - ctx.HandleError(err) - return - } - blacklistMsg = i18n.MessageBlacklistAddRole - - ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistAddRole, id) - } + BlacklistCommand{}.sendBlacklistResponse(ctx, id, isBlacklisted, false) +} - ctx.ReplyWith( - command.NewEphemeralMessageResponseWithComponents( - utils.Slice( - utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleBlacklist), ctx.GetMessage(blacklistMsg, id), ctx.PremiumTier()), - ), - ), - ) +func (BlacklistCommand) sendBlacklistResponse(ctx registry.CommandContext, id uint64, wasBlacklisted, isUser bool) { + blacklistMsg := i18n.MessageBlacklistAdd + if wasBlacklisted { + blacklistMsg = i18n.MessageBlacklistRemove + } + + var message string + if isUser { + message = fmt.Sprintf("<@%d> %s", id, ctx.GetMessage(blacklistMsg)) } else { - ctx.HandleError(fmt.Errorf("infallible")) - return + message = fmt.Sprintf("<@&%d> %s", id, ctx.GetMessage(blacklistMsg)) } + + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice( + utils.BuildContainerRaw(ctx.GetColour(customisation.Green), ctx.GetMessage(i18n.TitleBlacklist), message, ctx.PremiumTier()), + ), + ), + ) } diff --git a/bot/command/impl/settings/premium.go b/bot/command/impl/settings/premium.go index c764d96..a4634ab 100644 --- a/bot/command/impl/settings/premium.go +++ b/bot/command/impl/settings/premium.go @@ -107,7 +107,7 @@ func (PremiumCommand) Execute(ctx registry.CommandContext) { keyEmoji = utils.BuildEmoji("šŸ”‘") } - components := []component.Component{ + innerComponents := []component.Component{ component.BuildSection(component.Section{ Components: utils.Slice( component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("## %s", ctx.GetMessage(i18n.TitlePremium))}), @@ -152,13 +152,13 @@ func (PremiumCommand) Execute(ctx registry.CommandContext) { } if ctx.PremiumTier() == premium.None { - components = utils.AddPremiumFooter(components) + innerComponents = utils.AddPremiumFooter(innerComponents) } ctx.ReplyWith( command.NewEphemeralMessageResponseWithComponents( utils.Slice(component.BuildContainer(component.Container{ - Components: components, + Components: innerComponents, AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), })), ), diff --git a/bot/command/impl/tickets/startticket.go b/bot/command/impl/tickets/startticket.go index f4db1e8..e77e4e3 100644 --- a/bot/command/impl/tickets/startticket.go +++ b/bot/command/impl/tickets/startticket.go @@ -9,6 +9,7 @@ import ( "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel/message" "github.com/TicketsBot-cloud/gdl/objects/interaction" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/gdl/rest/request" "github.com/TicketsBot-cloud/worker/bot/command" @@ -110,12 +111,19 @@ func (StartTicketCommand) Execute(ctx registry.CommandContext) { func sendTicketStartedFromMessage(ctx registry.CommandContext, ticket database.Ticket, msg message.Message) { // format messageLink := fmt.Sprintf("https://discord.com/channels/%d/%d/%d", ctx.GuildId(), ctx.ChannelId(), msg.Id) - contentFormatted := strings.ReplaceAll(utils.StringMax(msg.Content, 2048, "..."), "`", "\\`") - msgEmbed := utils.BuildContainer( - ctx, customisation.Green, i18n.Ticket, i18n.MessageTicketStartedFrom, - messageLink, msg.Author.Id, ctx.ChannelId(), contentFormatted, - ) + var msgEmbed component.Component + if msg.Content == "" { + msgEmbed = utils.BuildContainer( + ctx, customisation.Green, i18n.Ticket, i18n.MessageTicketStartedFromNoContent, + messageLink, + ) + } else { + msgEmbed = utils.BuildContainer( + ctx, customisation.Green, i18n.Ticket, i18n.MessageTicketStartedFrom, + messageLink, msg.Author.Id, ctx.ChannelId(), strings.ReplaceAll(utils.StringMax(msg.Content, 2048, "..."), "`", "\\`"), + ) + } if _, err := ctx.Worker().CreateMessageComplex(*ticket.ChannelId, rest.CreateMessageData{ Components: utils.Slice(msgEmbed), diff --git a/bot/logic/closeembed.go b/bot/logic/closeembed.go index de4f1f6..1dd7e02 100644 --- a/bot/logic/closeembed.go +++ b/bot/logic/closeembed.go @@ -265,7 +265,7 @@ func BuildCloseContainer( } transcriptLink := fmt.Sprintf("%s/manage/%d/transcripts/view/%d", config.Conf.Bot.DashboardUrl, ticket.GuildId, ticket.Id) - cc := []component.Component{ + innerComponents := []component.Component{ component.BuildSection(component.Section{ Accessory: component.BuildButton(component.Button{ Label: "View Transcript", @@ -283,12 +283,12 @@ func BuildCloseContainer( } if cmd.PremiumTier() == premium.None { - cc = utils.AddPremiumFooter(cc) + innerComponents = utils.AddPremiumFooter(innerComponents) } container := component.BuildContainer(component.Container{ AccentColor: utils.Ptr(cmd.GetColour(customisation.Green)), - Components: cc, + Components: innerComponents, }) return &container diff --git a/bot/logic/help.go b/bot/logic/help.go index 8724668..eaac32a 100644 --- a/bot/logic/help.go +++ b/bot/logic/help.go @@ -5,20 +5,18 @@ import ( "fmt" "sort" - "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" "github.com/TicketsBot-cloud/worker/bot/command/registry" "github.com/TicketsBot-cloud/worker/bot/customisation" "github.com/TicketsBot-cloud/worker/bot/utils" - "github.com/TicketsBot-cloud/worker/config" "github.com/TicketsBot-cloud/worker/i18n" "github.com/elliotchance/orderedmap" ) func BuildHelpMessage(category command.Category, page int, ctx registry.CommandContext, cmds map[string]registry.Command) (*component.Component, error) { - componentList := []component.Component{} + innerComponents := []component.Component{} permLevel, _ := ctx.UserPermissionLevel(ctx) @@ -69,7 +67,7 @@ func BuildHelpMessage(category command.Category, page int, ctx registry.CommandC continue } - componentList = append(componentList, + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{ Content: registry.FormatHelp2(cmd, ctx.GuildId(), &commandId), }), @@ -80,38 +78,27 @@ func BuildHelpMessage(category command.Category, page int, ctx registry.CommandC // get certain commands for pagination componentsPerPage := 10 - if len(componentList) > componentsPerPage { + if len(innerComponents) > componentsPerPage { startIndex := (page - 1) * componentsPerPage endIndex := startIndex + componentsPerPage - if startIndex > len(componentList) { + if startIndex > len(innerComponents) { return nil, fmt.Errorf("page %d is out of range", page) } - if endIndex > len(componentList) { - endIndex = len(componentList) + if endIndex > len(innerComponents) { + endIndex = len(innerComponents) } - componentList = componentList[startIndex:endIndex] + innerComponents = innerComponents[startIndex:endIndex] } - if ctx.PremiumTier() == premium.None { - componentList = append(componentList, - component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), - }), - ) - } - - container := component.BuildContainer(component.Container{ - Components: append([]component.Component{ - component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("## %s\n-# %s", ctx.GetMessage(i18n.TitleHelp), category), - }), - component.BuildSeparator(component.Separator{}), - }, componentList...), - AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), - }) + container := utils.BuildContainerNoLocale( + ctx, + customisation.Green, + fmt.Sprintf("%s\n-# %s", ctx.GetMessage(i18n.TitleHelp), category), + innerComponents, + ) return &container, nil } diff --git a/bot/logic/viewstaff.go b/bot/logic/viewstaff.go index c2bc712..26c1776 100644 --- a/bot/logic/viewstaff.go +++ b/bot/logic/viewstaff.go @@ -62,7 +62,7 @@ func buildPaginatedField(cmd registry.CommandContext, entries []uint64, page int } func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (component.Component, int) { - comps := []component.Component{} + innerComponents := []component.Component{} adminUsers, _ := dbclient.Client.Permissions.GetAdmins(ctx, cmd.GuildId()) adminRoles, _ := dbclient.Client.RolePermissions.GetAdminRoles(ctx, cmd.GuildId()) @@ -90,8 +90,8 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffRoleFormat, "", ) - comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) - comps = append(comps, component.BuildSeparator(component.Separator{Divider: utils.Ptr(true), Spacing: utils.Ptr(1)})) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) + innerComponents = append(innerComponents, component.BuildSeparator(component.Separator{Divider: utils.Ptr(true), Spacing: utils.Ptr(1)})) // Admin users label, value = buildPaginatedField( @@ -101,9 +101,9 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffUserFormat, "", ) - comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) - comps = append(comps, component.BuildSeparator(component.Separator{Divider: utils.Ptr(true), Spacing: utils.Ptr(1)})) + innerComponents = append(innerComponents, component.BuildSeparator(component.Separator{Divider: utils.Ptr(true), Spacing: utils.Ptr(1)})) // Support roles label, value = buildPaginatedField( @@ -113,7 +113,7 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffRoleFormat, "", ) - comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) // Support users if len(supportUsers) > 0 { @@ -124,10 +124,10 @@ func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, pag viewStaffUserFormat, cmd.GetMessage(i18n.MessageViewStaffSupportUsersWarn), ) - comps = append(comps, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) } - container := utils.BuildContainerWithComponents(cmd, customisation.Green, i18n.MessageViewStaffTitle, comps) + container := utils.BuildContainerWithComponents(cmd, customisation.Green, i18n.MessageViewStaffTitle, innerComponents) return container, totalPages } diff --git a/bot/logic/welcomemessage.go b/bot/logic/welcomemessage.go index 9762d05..1b5432a 100644 --- a/bot/logic/welcomemessage.go +++ b/bot/logic/welcomemessage.go @@ -61,19 +61,19 @@ func SendWelcomeMessage( fieldStr += fmt.Sprintf("**%s**\n%s\n", field.Name, utils.EscapeMarkdown(field.Value)) } - components := []component.Component{ + innerComponents := []component.Component{ component.BuildTextDisplay(component.TextDisplay{ Content: fieldStr, }), } if cmd.PremiumTier() == premium.None { - components = utils.AddPremiumFooter(components) + innerComponents = utils.AddPremiumFooter(innerComponents) } embeds = append(embeds, component.BuildContainer(component.Container{ AccentColor: &embedColor, - Components: components, + Components: innerComponents, })) } diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index 8c68211..5623374 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -112,7 +112,7 @@ func AddPremiumFooter(components []component.Component) []component.Component { components = append(components, component.BuildTextDisplay(component.TextDisplay{ - Content: fmt.Sprintf("-# %s Powered by %s", customisation.EmojiLogo, config.Conf.Bot.PoweredBy), + Content: fmt.Sprintf("-# %s Powered by [%s](https://%s)", customisation.EmojiLogo, config.Conf.Bot.PoweredBy, config.Conf.Bot.PoweredBy), }), ) diff --git a/i18n/messages.go b/i18n/messages.go index 025158e..b14e31e 100644 --- a/i18n/messages.go +++ b/i18n/messages.go @@ -112,16 +112,14 @@ var ( MessageAddNoPermission MessageId = "commands.add.no_permission" MessageAddSuccess MessageId = "commands.add.success" - MessageBlacklisted MessageId = "generic.error.blacklisted" - MessageBlacklistNoMembers MessageId = "commands.blacklist.no_members" - MessageBlacklistSelf MessageId = "commands.blacklist.self" - MessageBlacklistStaff MessageId = "commands.blacklist.staff" - MessageBlacklistLimit MessageId = "commands.blacklist.add.limit" - MessageBlacklistAdd MessageId = "commands.blacklist.add.success" - MessageBlacklistRoleLimit MessageId = "commands.blacklist.add_role.limit" - MessageBlacklistAddRole MessageId = "commands.blacklist.add_role.success" - MessageBlacklistRemove MessageId = "commands.blacklist.remove.success" - MessageBlacklistRemoveRole MessageId = "commands.blacklist.remove_role.success" + MessageBlacklisted MessageId = "generic.error.blacklisted" + MessageBlacklistNoMembers MessageId = "commands.blacklist.no_members" + MessageBlacklistSelf MessageId = "commands.blacklist.self" + MessageBlacklistStaff MessageId = "commands.blacklist.staff" + MessageBlacklistAdd MessageId = "commands.blacklist.add.success" + MessageBlacklistRemove MessageId = "commands.blacklist.remove.success" + MessageBlacklistLimit MessageId = "commands.blacklist.add.limit" + MessageBlacklistRoleLimit MessageId = "commands.blacklist.add_role.limit" MessageClaimed MessageId = "commands.claim.success" MessageClaimNoPermission MessageId = "commands.claim.no_permission" @@ -183,15 +181,16 @@ var ( MessageNotATicketChannel MessageId = "generic.not_ticket" MessageInvalidUser MessageId = "generic.invalid_user" - MessageTicketLimitReached MessageId = "commands.open.ticket_limit" - MessageTooManyTickets MessageId = "commands.open.too_many_tickets" - MessageGuildChannelLimitReached MessageId = "commands.open.guild_channel_limit" - MessageTicketStartedFrom MessageId = "commands.open.from" - MessageMovedToTicket MessageId = "commands.open.from.moved" - MessageFormMissingInput MessageId = "commands.open.missing_form_answer" - MessageOpenCommandDisabled MessageId = "commands.open.disabled" - MessageOpenCantSeeParentChannel MessageId = "commands.open.threads.cant_see_parent_channel" - MessageOpenCantMessageInThreads MessageId = "commands.open.threads.cant_message_in_threads" + MessageTicketLimitReached MessageId = "commands.open.ticket_limit" + MessageTooManyTickets MessageId = "commands.open.too_many_tickets" + MessageGuildChannelLimitReached MessageId = "commands.open.guild_channel_limit" + MessageTicketStartedFrom MessageId = "commands.open.from" + MessageTicketStartedFromNoContent MessageId = "commands.open.from.no_content" + MessageMovedToTicket MessageId = "commands.open.from.moved" + MessageFormMissingInput MessageId = "commands.open.missing_form_answer" + MessageOpenCommandDisabled MessageId = "commands.open.disabled" + MessageOpenCantSeeParentChannel MessageId = "commands.open.threads.cant_see_parent_channel" + MessageOpenCantMessageInThreads MessageId = "commands.open.threads.cant_message_in_threads" MessageCloseRequestNoReason MessageId = "commands.close_request.no_reason" MessageCloseRequestWithReason MessageId = "commands.close_request.with_reason" From 4b515f6a9f4778865a935af2e7c6579b8ea37bee Mon Sep 17 00:00:00 2001 From: Tobias <53872542+biast12@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:31:08 +0100 Subject: [PATCH 13/13] feat(RM-51): Show panel name in close container if available (CV2) (#22) * feat(RM-51): Show panel name in close container if available Adds the panel name to the close container details if the ticket has an associated panel with a title. Also refactors claimedBy and reason formatting logic for clarity and error handling. * Converted rating/feedback/DM transcript * Add server info and icon to close container UI The BuildCloseContainer function now accepts a guild parameter to display the server name and icon in the ticket close UI (DMs only). * Fix section accessory rendering in close embed Refactors the logic for building the section with a guild icon accessory in the close embed. Now only adds the accessory if the guild and icon are present, otherwise appends the text display directly. This prevents empty or incorrect accessory fields. --- bot/button/handlers/exitsurveysubmit.go | 2 +- bot/button/handlers/rate.go | 2 +- bot/logic/close.go | 45 +++--- bot/logic/closeembed.go | 175 +++++++++--------------- 4 files changed, 86 insertions(+), 138 deletions(-) diff --git a/bot/button/handlers/exitsurveysubmit.go b/bot/button/handlers/exitsurveysubmit.go index dca0b33..5fc6b5a 100644 --- a/bot/button/handlers/exitsurveysubmit.go +++ b/bot/button/handlers/exitsurveysubmit.go @@ -182,5 +182,5 @@ func addViewFeedbackButton(ctx context.Context, cmd *cmdcontext.ModalContext, ti return fmt.Errorf("exit survey was completed, but no rating was found (%d:%d)", ticket.GuildId, ticket.Id) } - return logic.EditGuildArchiveMessageIfExists(ctx, cmd.Worker(), ticket, settings, true, closedBy, reason, &rating) + return logic.EditGuildArchiveMessageIfExists(ctx, cmd, cmd.Worker(), ticket, settings, true, closedBy, reason, &rating) } diff --git a/bot/button/handlers/rate.go b/bot/button/handlers/rate.go index 2703f4e..a05756c 100644 --- a/bot/button/handlers/rate.go +++ b/bot/button/handlers/rate.go @@ -152,7 +152,7 @@ func (h *RateHandler) Execute(ctx *cmdcontext.ButtonContext) { return } - if err := logic.EditGuildArchiveMessageIfExists(ctx, ctx.Worker(), ticket, settings, hasFeedback, closedBy, reason, &rating); err != nil { + if err := logic.EditGuildArchiveMessageIfExists(ctx, ctx, ctx.Worker(), ticket, settings, hasFeedback, closedBy, reason, &rating); err != nil { ctx.HandleError(err) } } diff --git a/bot/logic/close.go b/bot/logic/close.go index 8393258..e5051ac 100644 --- a/bot/logic/close.go +++ b/bot/logic/close.go @@ -12,6 +12,7 @@ import ( "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" "github.com/TicketsBot-cloud/gdl/objects/channel/message" + "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/objects/member" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/gdl/rest/request" @@ -231,10 +232,10 @@ func CloseTicket(ctx context.Context, cmd registry.CommandContext, reason *strin } } - sendCloseEmbed(ctx, cmd, errorContext, member, settings, ticket, reason) + sendCloseContainer(ctx, cmd, errorContext, member, settings, ticket, reason) } -func sendCloseEmbed(ctx context.Context, cmd registry.CommandContext, errorContext sentry.ErrorContext, member member.Member, settings database.Settings, ticket database.Ticket, reason *string) { +func sendCloseContainer(ctx context.Context, cmd registry.CommandContext, errorContext sentry.ErrorContext, member member.Member, settings database.Settings, ticket database.Ticket, reason *string) { // Send logs to archive channel archiveChannelId, err := dbclient.Client.ArchiveChannel.Get(ctx, ticket.GuildId) if err != nil { @@ -249,14 +250,7 @@ func sendCloseEmbed(ctx context.Context, cmd registry.CommandContext, errorConte } if archiveChannelExists && archiveChannelId != nil { - componentBuilders := [][]CloseEmbedElement{ - { - TranscriptLinkElement(settings.StoreTranscripts), - ThreadLinkElement(ticket.IsThread && ticket.ChannelId != nil), - }, - } - - closeContainer := BuildCloseContainer(ctx, cmd, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) + closeContainer := BuildCloseContainer(ctx, cmd, cmd.Worker(), ticket, nil, member.User.Id, reason, nil, false) data := rest.CreateMessageData{ Flags: uint(message.FlagComponentsV2), @@ -321,33 +315,32 @@ func sendCloseEmbed(ctx context.Context, cmd registry.CommandContext, errorConte statsd.Client.IncrementKey(statsd.KeyDirectMessage) - componentBuilders := [][]CloseEmbedElement{ - { - TranscriptLinkElement(settings.StoreTranscripts), - ThreadLinkElement(ticket.IsThread && ticket.ChannelId != nil), - }, - { - FeedbackRowElement(feedbackEnabled && hasSentMessage && permLevel == permission.Everyone), - }, - } - - closeEmbed, closeComponents := BuildCloseEmbed(ctx, cmd.Worker(), ticket, member.User.Id, reason, nil, componentBuilders) - closeEmbed.SetAuthor(guild.Name, "", fmt.Sprintf("https://cdn.discordapp.com/icons/%d/%s.png", guild.Id, guild.Icon)) - // Use message content to tell users why they can't rate a ticket var content string + var viewFeedbackButton = true if feedbackEnabled { if permLevel > permission.Everyone { content = "-# " + cmd.GetMessage(i18n.MessageCloseCantRateStaff, guild.Name) + viewFeedbackButton = false } else if !hasSentMessage { content = "-# " + cmd.GetMessage(i18n.MessageCloseCantRateEmpty) + viewFeedbackButton = false + } + } + + closeContainer := BuildCloseContainer(ctx, cmd, cmd.Worker(), ticket, &guild, member.User.Id, reason, nil, viewFeedbackButton) + + components := []component.Component{*closeContainer} + if content != "" { + components = []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: content}), + *closeContainer, } } data := rest.CreateMessageData{ - Content: content, - Embeds: utils.Slice(closeEmbed), - Components: closeComponents, + Flags: uint(message.FlagComponentsV2), + Components: components, } if _, err := cmd.Worker().CreateMessageComplex(dmChannel, data); err != nil { diff --git a/bot/logic/closeembed.go b/bot/logic/closeembed.go index 1dd7e02..e48eeb7 100644 --- a/bot/logic/closeembed.go +++ b/bot/logic/closeembed.go @@ -9,8 +9,8 @@ import ( "github.com/TicketsBot-cloud/common/premium" "github.com/TicketsBot-cloud/common/sentry" "github.com/TicketsBot-cloud/database" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" "github.com/TicketsBot-cloud/gdl/objects/channel/message" + "github.com/TicketsBot-cloud/gdl/objects/guild" "github.com/TicketsBot-cloud/gdl/objects/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/gdl/rest" @@ -22,15 +22,15 @@ import ( "github.com/TicketsBot-cloud/worker/config" ) -type CloseEmbedElement func(worker *worker.Context, ticket database.Ticket) []component.Component +type CloseContainerElement func(worker *worker.Context, ticket database.Ticket) []component.Component -func NoopElement() CloseEmbedElement { +func NoopElement() CloseContainerElement { return func(worker *worker.Context, ticket database.Ticket) []component.Component { return nil } } -func TranscriptLinkElement(condition bool) CloseEmbedElement { +func TranscriptLinkElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -52,7 +52,7 @@ func TranscriptLinkElement(condition bool) CloseEmbedElement { } } -func ThreadLinkElement(condition bool) CloseEmbedElement { +func ThreadLinkElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -74,7 +74,7 @@ func ThreadLinkElement(condition bool) CloseEmbedElement { } } -func ViewFeedbackElement(condition bool) CloseEmbedElement { +func ViewFeedbackElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -91,7 +91,7 @@ func ViewFeedbackElement(condition bool) CloseEmbedElement { } } -func FeedbackRowElement(condition bool) CloseEmbedElement { +func FeedbackRowElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -123,96 +123,19 @@ func FeedbackRowElement(condition bool) CloseEmbedElement { } } -func BuildCloseEmbed( - ctx context.Context, - worker *worker.Context, - ticket database.Ticket, - closedBy uint64, - reason *string, - rating *uint8, - components [][]CloseEmbedElement, -) (*embed.Embed, []component.Component) { - var formattedReason string - if reason == nil { - formattedReason = "No reason specified" - } else { - formattedReason = *reason - if len(formattedReason) > 1024 { - formattedReason = formattedReason[:1024] - } - } - - var claimedBy string - { - claimUserId, err := dbclient.Client.TicketClaims.Get(ctx, ticket.GuildId, ticket.Id) - if err != nil { - sentry.Error(err) - } - - if claimUserId == 0 { - claimedBy = "Not claimed" - } else { - claimedBy = fmt.Sprintf("<@%d>", claimUserId) - } - } - - colour, err := utils.GetColourForGuild(ctx, worker, customisation.Green, ticket.GuildId) - if err != nil { - sentry.Error(err) - colour = customisation.Green.Default() - } - - // TODO: Translate titles - closeEmbed := embed.NewEmbed(). - SetTitle("Ticket Closed"). - SetColor(colour). - AddField(formatTitle("Ticket ID", customisation.EmojiId, worker.IsWhitelabel), strconv.Itoa(ticket.Id), true). - AddField(formatTitle("Opened By", customisation.EmojiOpen, worker.IsWhitelabel), fmt.Sprintf("<@%d>", ticket.UserId), true). - AddField(formatTitle("Closed By", customisation.EmojiClose, worker.IsWhitelabel), fmt.Sprintf("<@%d>", closedBy), true). - AddField(formatTitle("Open Time", customisation.EmojiOpenTime, worker.IsWhitelabel), message.BuildTimestamp(ticket.OpenTime, message.TimestampStyleShortDateTime), true). - AddField(formatTitle("Claimed By", customisation.EmojiClaim, worker.IsWhitelabel), claimedBy, true) - - if ticket.CloseTime != nil { - closeEmbed.SetTimestamp(*ticket.CloseTime) - } - - if rating == nil { - closeEmbed = closeEmbed.AddBlankField(true) - } else { - closeEmbed = closeEmbed.AddField(formatTitle("Rating", customisation.EmojiRating, worker.IsWhitelabel), fmt.Sprintf("%d ⭐", *rating), true) - } - - closeEmbed = closeEmbed.AddField(formatTitle("Reason", customisation.EmojiReason, worker.IsWhitelabel), formattedReason, false) - - var rows []component.Component - for _, row := range components { - var rowElements []component.Component - for _, element := range row { - rowElements = append(rowElements, element(worker, ticket)...) - } - - if len(rowElements) > 0 { - rows = append(rows, component.BuildActionRow(rowElements...)) - } - } - - return closeEmbed, rows -} - func BuildCloseContainer( ctx context.Context, cmd registry.CommandContext, worker *worker.Context, ticket database.Ticket, + guild *guild.Guild, closedBy uint64, reason *string, rating *uint8, - components [][]CloseEmbedElement, + viewFeedbackButton bool, ) *component.Component { - var formattedReason string - if reason == nil { - formattedReason = "No reason specified" - } else { + var formattedReason = "No reason specified" + if reason != nil { formattedReason = *reason if len(formattedReason) > 1024 { formattedReason = formattedReason[:1024] @@ -225,24 +148,35 @@ func BuildCloseContainer( } var claimedBy string - { - claimUserId, err := dbclient.Client.TicketClaims.Get(ctx, ticket.GuildId, ticket.Id) + claimUserId, err := dbclient.Client.TicketClaims.Get(ctx, ticket.GuildId, ticket.Id) + if err != nil { + sentry.Error(err) + } else if claimUserId > 0 { + claimedBy = fmt.Sprintf("<@%d>", claimUserId) + } + + var panelName string + if ticket.PanelId != nil { + p, err := dbclient.Client.Panel.GetById(ctx, *ticket.PanelId) if err != nil { sentry.Error(err) - } - - if claimUserId == 0 { - claimedBy = "" - } else { - claimedBy = fmt.Sprintf("<@%d>", claimUserId) + } else if p.Title != "" { + panelName = p.Title } } - section1Text := []string{ - formatRow("Ticket ID", strconv.Itoa(ticket.Id)), + section1Text := []string{} + if guild != nil { + section1Text = append(section1Text, formatRow("Server", guild.Name)) + } + section1Text = append(section1Text, formatRow("Ticket ID", strconv.Itoa(ticket.Id))) + if panelName != "" { + section1Text = append(section1Text, formatRow("Panel", panelName)) + } + section1Text = append(section1Text, formatRow("Opened By", fmt.Sprintf("<@%d>", ticket.UserId)), formatRow("Closed By", fmt.Sprintf("<@%d>", closedBy)), - } + ) section2Text := []string{ formatRow("Open Time", message.BuildTimestamp(ticket.OpenTime, message.TimestampStyleShortDateTime)), @@ -277,12 +211,39 @@ func BuildCloseContainer( Content: "## Ticket Closed", })), }), - component.BuildTextDisplay(component.TextDisplay{Content: strings.Join(section1Text, "\n")}), + } + + sectionContent := component.BuildTextDisplay(component.TextDisplay{ + Content: strings.Join(section1Text, "\n"), + }) + + if guild != nil && guild.Icon != "" { + section := component.Section{ + Components: utils.Slice(sectionContent), + Accessory: component.BuildThumbnail(component.Thumbnail{ + Media: component.UnfurledMediaItem{ + Url: fmt.Sprintf("https://cdn.discordapp.com/icons/%d/%s.png", guild.Id, guild.Icon), + }, + }), + } + innerComponents = append(innerComponents, component.BuildSection(section)) + } else { + innerComponents = append(innerComponents, sectionContent) + } + + innerComponents = append(innerComponents, component.BuildSeparator(component.Separator{}), component.BuildTextDisplay(component.TextDisplay{Content: strings.Join(section2Text, "\n")}), + ) + + if viewFeedbackButton { + innerComponents = append(innerComponents, component.BuildActionRow( + FeedbackRowElement(viewFeedbackButton)(worker, ticket)..., + )) } if cmd.PremiumTier() == premium.None { + innerComponents = append(innerComponents, component.BuildSeparator(component.Separator{})) innerComponents = utils.AddPremiumFooter(innerComponents) } @@ -308,6 +269,7 @@ func formatTitle(s string, emoji customisation.CustomEmoji, isWhitelabel bool) s func EditGuildArchiveMessageIfExists( ctx context.Context, + cmd registry.CommandContext, worker *worker.Context, ticket database.Ticket, settings database.Settings, @@ -325,18 +287,11 @@ func EditGuildArchiveMessageIfExists( return nil } - componentBuilders := [][]CloseEmbedElement{ - { - TranscriptLinkElement(settings.StoreTranscripts), - ThreadLinkElement(ticket.IsThread && ticket.ChannelId != nil), - ViewFeedbackElement(viewFeedbackButton), - }, - } + closeContainer := BuildCloseContainer(ctx, cmd, worker, ticket, nil, closedBy, reason, rating, viewFeedbackButton) - embed, components := BuildCloseEmbed(ctx, worker, ticket, closedBy, reason, rating, componentBuilders) _, err = worker.EditMessage(archiveMessage.ChannelId, archiveMessage.MessageId, rest.EditMessageData{ - Embeds: utils.Slice(embed), - Components: components, + Flags: uint(message.FlagComponentsV2), + Components: utils.Slice(*closeContainer), }) return err