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..170bb8d 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, []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/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/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/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/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/handlers/viewstaff.go b/bot/button/handlers/viewstaff.go index 75b3643..faf10f3 100644 --- a/bot/button/handlers/viewstaff.go +++ b/bot/button/handlers/viewstaff.go @@ -1,14 +1,11 @@ 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" @@ -51,64 +48,15 @@ func (h *ViewStaffHandler) Execute(ctx *context.ButtonContext) { return } - msgEmbed, isBlank := logic.BuildViewStaffMessage(ctx.Context, ctx, page) - if !isBlank { - 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, - }), - ), - }, - }) - } 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 - } - - ctx.Edit(command.MessageResponse{ - Embeds: embeds, - Components: components, - }) + comp, totalPages := logic.BuildViewStaffMessage(ctx.Context, ctx, page) + if page >= totalPages { + page = totalPages - 1 } + + ctx.Edit(command.MessageResponse{ + Components: []component.Component{ + comp, + logic.BuildViewStaffButtons(page, totalPages), + }, + }) } diff --git a/bot/button/handlers/viewsurvey.go b/bot/button/handlers/viewsurvey.go index 610b9ac..e90a206 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)...) + innerComponents := []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) + innerComponents = append(innerComponents, component.BuildActionRow(buttons...)) } + + ctx.ReplyWith(command.NewMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(ctx.GetColour(customisation.Green)), + Components: innerComponents, + })))) } diff --git a/bot/button/manager/manager.go b/bot/button/manager/manager.go index 1111315..8febb78 100644 --- a/bot/button/manager/manager.go +++ b/bot/button/manager/manager.go @@ -66,12 +66,15 @@ func (m *ComponentInteractionManager) RegisterCommands() { new(handlers.RedeemVoteCreditsHandler), new(handlers.ViewStaffHandler), new(handlers.ViewSurveyHandler), + new(handlers.HelpPageHandler), + new(handlers.RecacheHandler), ) 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/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/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/admindebug.go b/bot/command/impl/admin/admindebug.go new file mode 100644 index 0000000..347005b --- /dev/null +++ b/bot/command/impl/admin/admindebug.go @@ -0,0 +1,208 @@ +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(), + ), + 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/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..0bb4dc7 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 - } + innerComponents := []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,15 @@ func (AdminListGuildEntitlementsCommand) Execute(ctx registry.CommandContext, gu entitlement.SkuPriority, ) - embed.AddField(entitlement.SkuLabel, value, false) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: value})) } - ctx.ReplyWithEmbed(embed) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerWithComponents( + ctx, + customisation.Orange, + i18n.Admin, + innerComponents, + ), + })) } diff --git a/bot/command/impl/admin/adminlistuserentitlements.go b/bot/command/impl/admin/adminlistuserentitlements.go index e8b7a04..13b5e24 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 - } + innerComponents := []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,15 @@ func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, use entitlement.SkuPriority, ) - embed.AddField(entitlement.SkuLabel, value, false) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: value})) } - ctx.ReplyWithEmbed(embed) + ctx.ReplyWith(command.NewMessageResponseWithComponents([]component.Component{ + utils.BuildContainerWithComponents( + ctx, + customisation.Orange, + i18n.Admin, + innerComponents, + ), + })) } diff --git a/bot/command/impl/admin/adminrecache.go b/bot/command/impl/admin/adminrecache.go index fbf98ac..67a169a 100644 --- a/bot/command/impl/admin/adminrecache.go +++ b/bot/command/impl/admin/adminrecache.go @@ -2,15 +2,20 @@ 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/redis" + "github.com/TicketsBot-cloud/worker/bot/utils" "github.com/TicketsBot-cloud/worker/i18n" ) @@ -49,6 +54,23 @@ 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 ctx.Worker().Cache.DeleteGuild(ctx, guildId) ctx.Worker().Cache.DeleteGuildChannels(ctx, guildId) @@ -86,15 +108,37 @@ 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") + 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/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/admin/adminwhitelabeldata.go b/bot/command/impl/admin/adminwhitelabeldata.go index fb8dcbf..7d16832 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) + } + + innerComponents := []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: innerComponents, + })))) } 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/help.go b/bot/command/impl/general/help.go index 410cf30..8f69870 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.BuildHelpMessageCategorySelector(c.Registry, ctx, command.General), + *logic.BuildHelpMessagePaginationButtons(command.General.ToRawString(), 1, 1), + })) + 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/invite.go b/bot/command/impl/general/invite.go index d3fa1f8..2ec3899 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.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleInvite, []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..336c961 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{ + 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{ 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.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleJumpToTop, innerComponents), + })); err != nil { ctx.HandleError(err) return } diff --git a/bot/command/impl/general/vote.go b/bot/command/impl/general/vote.go index 1ef1f85..caa1362 100644 --- a/bot/command/impl/general/vote.go +++ b/bot/command/impl/general/vote.go @@ -5,7 +5,6 @@ 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" @@ -48,6 +47,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 +63,75 @@ 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 { + if _, err := ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + utils.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleVote, []component.Component{ + componentBody, + component.BuildSeparator(component.Separator{Divider: utils.Ptr(false)}), + buildVoteComponent(ctx, true), + }), + })); 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, + 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), + 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, }) 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/impl/settings/addadmin.go b/bot/command/impl/settings/addadmin.go index 5ac1d6e..92237b8 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) @@ -62,17 +61,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.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddAdmin, []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..8244930 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) @@ -66,17 +65,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.BuildContainerWithComponents(ctx, customisation.Green, i18n.TitleAddSupport, []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/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/settings/blacklist.go b/bot/command/impl/settings/blacklist.go index f15dc94..81f29ff 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) @@ -53,122 +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")) + } +} - if ctx.UserId() == id { - ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistSelf, utils.ToSlice(usageEmbed)) - return - } +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 + } - permLevel, err := permission.GetPermissionLevel(ctx, utils.ToRetriever(ctx.Worker()), member, ctx.GuildId()) - if err != nil { + if ctx.UserId() == id { + ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistSelf, utils.ToSlice(usageEmbed)) + return + } + + 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 } - - if permLevel > permission.Everyone { - ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageBlacklistStaff, utils.ToSlice(usageEmbed)) + } else { + // Check user blacklist limit + count, err := dbclient.Client.Blacklist.GetBlacklistedCount(ctx, ctx.GuildId()) + if err != nil { + ctx.HandleError(err) return } - isBlacklisted, err := dbclient.Client.Blacklist.IsBlacklisted(ctx, ctx.GuildId(), id) - if err != nil { - sentry.ErrorWithContext(err, ctx.ToErrorContext()) + if count >= 250 { + ctx.Reply(customisation.Red, i18n.Error, i18n.MessageBlacklistLimit, 250) return } - 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()) - 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 - } - - ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistAdd, member.User.Id) - } - } else if mentionableType == context.MentionableTypeRole { - // Check if role is staff - isSupport, err := dbclient.Client.RolePermissions.IsSupport(ctx, id) - if err != nil { + if err := dbclient.Client.Blacklist.Add(ctx, ctx.GuildId(), member.User.Id); 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? + BlacklistCommand{}.sendBlacklistResponse(ctx, id, isBlacklisted, true) +} + +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 + } + + // 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 } + } - 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()) - 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 - } - - ctx.Reply(customisation.Green, i18n.TitleBlacklist, i18n.MessageBlacklistAddRole, id) - } + BlacklistCommand{}.sendBlacklistResponse(ctx, id, isBlacklisted, false) +} + +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/language.go b/bot/command/impl/settings/language.go index cd21a4e..b780331 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,22 @@ 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, + 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..a4634ab 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,21 @@ 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, + []component.Component{ + component.BuildTextDisplay(component.TextDisplay{Content: ctx.GetMessage(content)}), + component.BuildActionRow(buttons...), + }, + ), + ), + ), + ) } else { var patreonEmoji, discordEmoji, keyEmoji *emoji.Emoji @@ -97,51 +107,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)), - }), + innerComponents := []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 { + innerComponents = utils.AddPremiumFooter(innerComponents) + } + + ctx.ReplyWith( + command.NewEphemeralMessageResponseWithComponents( + utils.Slice(component.BuildContainer(component.Container{ + Components: innerComponents, + 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/settings/viewstaff.go b/bot/command/impl/settings/viewstaff.go index 2c4836f..415e513 100644 --- a/bot/command/impl/settings/viewstaff.go +++ b/bot/command/impl/settings/viewstaff.go @@ -4,9 +4,6 @@ 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/guild/emoji" "github.com/TicketsBot-cloud/gdl/objects/interaction" "github.com/TicketsBot-cloud/gdl/objects/interaction/component" "github.com/TicketsBot-cloud/worker/bot/command" @@ -35,32 +32,10 @@ func (c ViewStaffCommand) GetExecutor() interface{} { } func (ViewStaffCommand) Execute(ctx registry.CommandContext) { - msgEmbed, _ := logic.BuildViewStaffMessage(ctx, ctx, 0) + comp, 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, - }), - ), - }, - } - - _, _ = ctx.ReplyWith(res) + _, _ = ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents([]component.Component{ + comp, + logic.BuildViewStaffButtons(0, totalPages), + })) } diff --git a/bot/command/impl/statistics/statsserver.go b/bot/command/impl/statistics/statsserver.go index 215512f..7734bc5 100644 --- a/bot/command/impl/statistics/statsserver.go +++ b/bot/command/impl/statistics/statsserver.go @@ -3,15 +3,15 @@ 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" @@ -31,7 +31,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,11 +46,17 @@ 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) - // totalTickets group.Go(func() (err error) { span := sentry.StartSpan(span.Context(), "GetTotalTicketCount") defer span.Finish() @@ -59,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() @@ -73,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() @@ -93,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() @@ -103,7 +105,6 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { }) // ticket duration - var ticketDuration analytics.TripleWindow group.Go(func() (err error) { span := sentry.StartSpan(span.Context(), "GetTicketDurationStats") defer span.Finish() @@ -113,7 +114,6 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { }) // tickets per day - var ticketVolumeTable string group.Go(func() error { span := sentry.StartSpan(span.Context(), "GetLastNTicketsPerDayGuild") defer span.Finish() @@ -123,16 +123,7 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { 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() + ticketVolumeData = counts return nil }) @@ -143,24 +134,76 @@ func (StatsServerCommand) Execute(ctx registry.CommandContext) { span = sentry.StartSpan(span.Context(), "Send Message") - msgEmbed := 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) - - _, _ = ctx.ReplyWith(command.NewEphemeralEmbedMessageResponse(msgEmbed)) + 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)), + } + tw := table.NewWriter() + tw.SetStyle(table.StyleLight) + tw.Style().Format.Header = text.FormatDefault + + 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{ + 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: 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```\n%s\n```", + ticketVolumeTable, + ), + }), + } + + ctx.ReplyWith(command.NewEphemeralMessageResponseWithComponents(utils.Slice(component.BuildContainer(component.Container{ + Components: innerComponents, + })))) + span.Finish() } diff --git a/bot/command/impl/statistics/statsuser.go b/bot/command/impl/statistics/statsuser.go index 4e4a28d..2590806 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") - msgEmbed := 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), + "**Permission Level**: Regular", + fmt.Sprintf("**Is Blacklisted**: %s", 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") - msgEmbed := 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/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..a601427 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,21 @@ 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 { + 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..f68fbf4 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,13 @@ 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}, - } - - 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..e77e4e3 100644 --- a/bot/command/impl/tickets/startticket.go +++ b/bot/command/impl/tickets/startticket.go @@ -9,6 +9,8 @@ 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" "github.com/TicketsBot-cloud/worker/bot/command/context" @@ -66,7 +68,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 } @@ -109,14 +111,24 @@ 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.BuildEmbed( - ctx, customisation.Green, i18n.Ticket, i18n.MessageTicketStartedFrom, nil, - 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().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 +192,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..7eae731 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,25 @@ 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 + 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/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..81edfcf 100644 --- a/bot/command/messageresponse.go +++ b/bot/command/messageresponse.go @@ -30,30 +30,16 @@ func NewEphemeralTextMessageResponse(content string) MessageResponse { } } -func NewEmbedMessageResponse(embeds ...*embed.Embed) MessageResponse { +func NewEphemeralMessageResponseWithComponents(components []component.Component) MessageResponse { return MessageResponse{ - Embeds: embeds, - } -} - -func NewEmbedMessageResponseWithComponents(e *embed.Embed, components []component.Component) MessageResponse { - return MessageResponse{ - Embeds: []*embed.Embed{e}, + Flags: message.SumFlags(message.FlagEphemeral, message.FlagComponentsV2), Components: components, } } -func NewEphemeralEmbedMessageResponse(embeds ...*embed.Embed) MessageResponse { +func NewMessageResponseWithComponents(components []component.Component) 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), + Flags: message.SumFlags(message.FlagComponentsV2), Components: components, } } @@ -134,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/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/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/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/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..e5051ac 100644 --- a/bot/logic/close.go +++ b/bot/logic/close.go @@ -11,8 +11,8 @@ 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/interaction/component" "github.com/TicketsBot-cloud/gdl/objects/member" "github.com/TicketsBot-cloud/gdl/rest" "github.com/TicketsBot-cloud/gdl/rest/request" @@ -20,6 +20,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 +176,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), }, } @@ -232,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 { @@ -250,18 +250,11 @@ func sendCloseEmbed(ctx context.Context, cmd registry.CommandContext, errorConte } if archiveChannelExists && archiveChannelId != nil { - componentBuilders := [][]CloseEmbedElement{ - { - TranscriptLinkElement(settings.StoreTranscripts), - ThreadLinkElement(ticket.IsThread && ticket.ChannelId != nil), - }, - } - - closeEmbed, closeComponents := BuildCloseEmbed(ctx, 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{ - Embeds: utils.Slice(closeEmbed), - Components: closeComponents, + Flags: uint(message.FlagComponentsV2), + Components: utils.Slice(*closeContainer), } msg, err := cmd.Worker().CreateMessageComplex(*archiveChannelId, data) @@ -322,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 6dd854f..e48eeb7 100644 --- a/bot/logic/closeembed.go +++ b/bot/logic/closeembed.go @@ -4,30 +4,33 @@ 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" "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" "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" "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() } @@ -49,7 +52,7 @@ func TranscriptLinkElement(condition bool) CloseEmbedElement { } } -func ThreadLinkElement(condition bool) CloseEmbedElement { +func ThreadLinkElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -71,7 +74,7 @@ func ThreadLinkElement(condition bool) CloseEmbedElement { } } -func ViewFeedbackElement(condition bool) CloseEmbedElement { +func ViewFeedbackElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -88,7 +91,7 @@ func ViewFeedbackElement(condition bool) CloseEmbedElement { } } -func FeedbackRowElement(condition bool) CloseEmbedElement { +func FeedbackRowElement(condition bool) CloseContainerElement { if !condition { return NoopElement() } @@ -120,80 +123,140 @@ func FeedbackRowElement(condition bool) CloseEmbedElement { } } -func BuildCloseEmbed( +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, -) (*embed.Embed, []component.Component) { - var formattedReason string - if reason == nil { - formattedReason = "No reason specified" - } else { + viewFeedbackButton bool, +) *component.Component { + var formattedReason = "No reason specified" + if reason != nil { 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) + 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 = "Not claimed" - } else { - claimedBy = fmt.Sprintf("<@%d>", claimUserId) + } else if p.Title != "" { + panelName = p.Title } } - colour, err := utils.GetColourForGuild(ctx, worker, customisation.Green, ticket.GuildId) - if err != nil { - sentry.Error(err) - colour = customisation.Green.Default() + 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)), + ) - // 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) + section2Text := []string{ + formatRow("Open Time", message.BuildTimestamp(ticket.OpenTime, message.TimestampStyleShortDateTime)), + } if ticket.CloseTime != nil { - closeEmbed.SetTimestamp(*ticket.CloseTime) + section2Text = append(section2Text, formatRow("Close Time", message.BuildTimestamp(*ticket.CloseTime, message.TimestampStyleShortDateTime))) } - if rating == nil { - closeEmbed = closeEmbed.AddBlankField(true) - } else { - closeEmbed = closeEmbed.AddField(formatTitle("Rating", customisation.EmojiRating, worker.IsWhitelabel), fmt.Sprintf("%d ⭐", *rating), true) + if claimedBy != "" { + section2Text = append(section2Text, formatRow("Claimed By", claimedBy)) } - closeEmbed = closeEmbed.AddField(formatTitle("Reason", customisation.EmojiReason, worker.IsWhitelabel), formattedReason, false) + if rating != nil { + section2Text = append(section2Text, formatRow("Rating", strings.Repeat("⭐", int(*rating)))) + } - var rows []component.Component - for _, row := range components { - var rowElements []component.Component - for _, element := range row { - rowElements = append(rowElements, element(worker, ticket)...) - } + if reason != nil { + section2Text = append(section2Text, formatRow("Reason", formattedReason)) + } - if len(rowElements) > 0 { - rows = append(rows, component.BuildActionRow(rowElements...)) + transcriptLink := fmt.Sprintf("%s/manage/%d/transcripts/view/%d", config.Conf.Bot.DashboardUrl, ticket.GuildId, ticket.Id) + innerComponents := []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", + })), + }), + } + + 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)..., + )) } - return closeEmbed, rows + if cmd.PremiumTier() == premium.None { + innerComponents = append(innerComponents, component.BuildSeparator(component.Separator{})) + innerComponents = utils.AddPremiumFooter(innerComponents) + } + + container := component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(cmd.GetColour(customisation.Green)), + Components: innerComponents, + }) + + 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 { @@ -206,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, @@ -223,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 diff --git a/bot/logic/help.go b/bot/logic/help.go new file mode 100644 index 0000000..eaac32a --- /dev/null +++ b/bot/logic/help.go @@ -0,0 +1,212 @@ +package logic + +import ( + "errors" + "fmt" + "sort" + + "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" + "github.com/elliotchance/orderedmap" +) + +func BuildHelpMessage(category command.Category, page int, ctx registry.CommandContext, cmds map[string]registry.Command) (*component.Component, error) { + innerComponents := []component.Component{} + + permLevel, _ := ctx.UserPermissionLevel(ctx) + + 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 + } + + innerComponents = append(innerComponents, + component.BuildTextDisplay(component.TextDisplay{ + Content: registry.FormatHelp2(cmd, ctx.GuildId(), &commandId), + }), + component.BuildSeparator(component.Separator{}), + ) + + } + + // get certain commands for pagination + componentsPerPage := 10 + if len(innerComponents) > componentsPerPage { + startIndex := (page - 1) * componentsPerPage + endIndex := startIndex + componentsPerPage + + if startIndex > len(innerComponents) { + return nil, fmt.Errorf("page %d is out of range", page) + } + + if endIndex > len(innerComponents) { + endIndex = len(innerComponents) + } + + innerComponents = innerComponents[startIndex:endIndex] + } + + container := utils.BuildContainerNoLocale( + ctx, + customisation.Green, + fmt.Sprintf("%s\n-# %s", ctx.GetMessage(i18n.TitleHelp), category), + innerComponents, + ) + + 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) + + 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/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/viewstaff.go b/bot/logic/viewstaff.go index dab6763..26c1776 100644 --- a/bot/logic/viewstaff.go +++ b/bot/logic/viewstaff.go @@ -5,142 +5,139 @@ import ( "fmt" "strings" - "github.com/TicketsBot-cloud/common/sentry" - "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/bot/utils" + "github.com/TicketsBot-cloud/worker/i18n" ) -// each msg is const perField = 8 +const viewStaffUserFormat = "- <@%d> (`%d`)\n" +const viewStaffRoleFormat = "- <@&%d> (`%d`)\n" + +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 BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (*embed.Embed, bool) { - isBlank := true +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() - embed := embed.NewEmbed(). - SetColor(cmd.GetColour(customisation.Green)). - SetTitle("Staff"). - SetFooter(fmt.Sprintf("Page %d", page+1), self.AvatarUrl(256)) +func BuildViewStaffMessage(ctx context.Context, cmd registry.CommandContext, page int) (component.Component, int) { + innerComponents := []component.Component{} - // Add field for admin users - { - adminUsers, err := dbclient.Client.Permissions.GetAdmins(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } + 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()) - 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 - } + maxLen := max(len(adminUsers), len(adminRoles), len(supportUsers), len(supportRoles)) + totalPages := (maxLen + perField - 1) / perField + if totalPages == 0 { + totalPages = 1 } - // 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 - } + if page < 0 { + page = 0 + } + if page >= totalPages { + page = totalPages - 1 } - embed.AddBlankField(false) // Add spacer between admin & support reps - - // Add field for support representatives - { - supportUsers, err := dbclient.Client.Permissions.GetSupportOnly(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } - - 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 - } + // Admin roles + label, value := buildPaginatedField( + cmd, adminRoles, page, + i18n.MessageViewStaffAdminRoles, + i18n.MessageViewStaffNoAdminRoles, + viewStaffRoleFormat, + "", + ) + 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( + cmd, adminUsers, page, + i18n.MessageViewStaffAdminUsers, + i18n.MessageViewStaffNoAdminUsers, + viewStaffUserFormat, + "", + ) + 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)})) + + // Support roles + label, value = buildPaginatedField( + cmd, supportRoles, page, + i18n.MessageViewStaffSupportRoles, + i18n.MessageViewStaffNoSupportRoles, + viewStaffRoleFormat, + "", + ) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) + + // Support users + if len(supportUsers) > 0 { + label, value = buildPaginatedField( + cmd, supportUsers, page, + i18n.MessageViewStaffSupportUsers, + "", + viewStaffUserFormat, + cmd.GetMessage(i18n.MessageViewStaffSupportUsersWarn), + ) + innerComponents = append(innerComponents, component.BuildTextDisplay(component.TextDisplay{Content: fmt.Sprintf("**%s**\n%s", label, value)})) } - // Add field for support roles - { - supportRoles, err := dbclient.Client.RolePermissions.GetSupportRolesOnly(ctx, cmd.GuildId()) - if err != nil { - sentry.ErrorWithContext(err, cmd.ToErrorContext()) - } + container := utils.BuildContainerWithComponents(cmd, customisation.Green, i18n.MessageViewStaffTitle, innerComponents) - 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 + return container, totalPages +} + +func max(nums ...int) int { + maxVal := 0 + for _, n := range nums { + if n > maxVal { + maxVal = n } } - - return embed, isBlank + return maxVal } diff --git a/bot/logic/welcomemessage.go b/bot/logic/welcomemessage.go index d7c2225..1b5432a 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,36 @@ 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)) + } + + innerComponents := []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) + innerComponents = utils.AddPremiumFooter(innerComponents) } - embeds = append(embeds, formAnswersEmbed) + embeds = append(embeds, component.BuildContainer(component.Container{ + AccentColor: &embedColor, + Components: innerComponents, + })) } buttons := []component.Component{ @@ -94,10 +102,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 +127,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 +140,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 +569,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 +577,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 { - e := &embed.Embed{ - Title: utils.ValueOrZero(customEmbed.Title), - Description: description, - Url: utils.ValueOrZero(customEmbed.Url), - Timestamp: customEmbed.Timestamp, - Color: int(customEmbed.Colour), + innerComponents := []component.Component{} + + 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 +640,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/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() +} diff --git a/bot/utils/messageutils.go b/bot/utils/messageutils.go index 5475062..5623374 100644 --- a/bot/utils/messageutils.go +++ b/bot/utils/messageutils.go @@ -5,57 +5,118 @@ import ( "fmt" "github.com/TicketsBot-cloud/common/premium" - "github.com/TicketsBot-cloud/gdl/objects/channel/embed" + "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" "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" ) -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...) +func BuildContainer(ctx registry.CommandContext, colour customisation.Colour, titleId, contentId i18n.MessageId, format ...interface{}) component.Component { + return BuildContainerWithComponents(ctx, colour, titleId, []component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: i18n.GetMessageFromGuild(ctx.GuildId(), contentId, format...), + })}) +} + +func BuildContainerWithFields(ctx registry.CommandContext, colour customisation.Colour, titleId, content i18n.MessageId, fields []model.Field, format ...interface{}) component.Component { - msgEmbed := embed.NewEmbed(). - SetColor(ctx.GetColour(colour)). - SetTitle(title). - SetDescription(content) + components := make([]component.Component, 0, len(fields)+2) for _, field := range fields { - msgEmbed.AddField(field.Name, field.Value, field.Inline) + components = append(components, component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("**%s**\n%s", field.Name, field.Value), + })) } + return BuildContainerWithComponents(ctx, colour, titleId, append([]component.Component{ + component.BuildTextDisplay(component.TextDisplay{ + Content: i18n.GetMessageFromGuild(ctx.GuildId(), content, format...), + }), + component.BuildSeparator(component.Separator{}), + }, components...)) +} + +func BuildContainerWithComponents( + ctx registry.CommandContext, colour customisation.Colour, title i18n.MessageId, 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 ctx.PremiumTier() == premium.None { - msgEmbed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) + components = AddPremiumFooter(components) } - return msgEmbed + return component.BuildContainer(component.Container{ + AccentColor: utils.Ptr(ctx.GetColour(colour)), + Components: components, + }) } -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 BuildContainerNoLocale( + ctx registry.CommandContext, colour customisation.Colour, title string, innerComponents []component.Component, +) component.Component { + components := append(Slice( + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("### %s", title), + }), + component.BuildSeparator(component.Separator{}), + ), innerComponents...) - for _, field := range fields { - msgEmbed.AddField(field.Name, field.Value, field.Inline) + if ctx.PremiumTier() == premium.None { + components = AddPremiumFooter(components) } + 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 { + components := Slice( + component.BuildTextDisplay(component.TextDisplay{ + Content: fmt.Sprintf("## %s", title), + }), + component.BuildSeparator(component.Separator{}), + component.BuildTextDisplay(component.TextDisplay{ + Content: content, + }), + ) + if tier == premium.None { - msgEmbed.SetFooter(fmt.Sprintf("Powered by %s", config.Conf.Bot.PoweredBy), config.Conf.Bot.IconUrl) + components = AddPremiumFooter(components) } - return msgEmbed + return component.BuildContainer(component.Container{ + AccentColor: &colourHex, + Components: components, + }) +} + +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("-# %s Powered by [%s](https://%s)", customisation.EmojiLogo, config.Conf.Bot.PoweredBy, config.Conf.Bot.PoweredBy), + }), + ) + + return components } func GetColourForGuild(ctx context.Context, worker *worker.Context, colour customisation.Colour, guildId uint64) (int, error) { @@ -78,28 +139,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/config/config.go b/config/config.go index d354e5c..8fdaf6f 100644 --- a/config/config.go +++ b/config/config.go @@ -30,6 +30,9 @@ type ( Bot struct { HttpAddress string `env:"HTTP_ADDR"` + 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"` DashboardUrl string `env:"DASHBOARD_URL" envDefault:"https://dashboard.tickets.bot"` FrontpageUrl string `env:"FRONTPAGE_URL" envDefault:"https://tickets.bot"` VoteUrl string `env:"VOTE_URL" envDefault:"https://vote.tickets.bot"` @@ -119,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"` 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 6cff801..84625e2 100644 --- a/event/httplisten.go +++ b/event/httplisten.go @@ -283,6 +283,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 +298,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 ecaf540..21edd2e 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,15 @@ 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/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/common => ../common require ( cloud.google.com/go/profiler v0.4.2 diff --git a/go.sum b/go.sum index 8584d30..2ebd3a6 100644 --- a/go.sum +++ b/go.sum @@ -31,16 +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/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= diff --git a/i18n/messages.go b/i18n/messages.go index 5429f7d..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" @@ -231,6 +230,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" diff --git a/locale b/locale index 1e90c3e..50a5bc9 160000 --- a/locale +++ b/locale @@ -1 +1 @@ -Subproject commit 1e90c3e69feb02f5502774aaa227ce56888a432a +Subproject commit 50a5bc9f7ef1af46627b28593c8f18cd1237be6a 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) }