From 9dd3196a8fc68792540f08a08ac8b3a3a7a388a7 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Sun, 26 Oct 2025 19:27:32 +0100 Subject: [PATCH 1/8] add separate disgo client --- embedg-server/actions/parser/parse.go | 7 +- embedg-server/embedg/bot.go | 140 +++++++ embedg-server/embedg/commands.go | 544 ++++++++++++++++++++++++++ embedg-server/embedg/helpers.go | 32 ++ embedg-server/embedg/rest/rest.go | 15 + embedg-server/go.mod | 21 +- embedg-server/go.sum | 21 + embedg-server/util/util.go | 19 +- go.work | 6 +- go.work.sum | 5 + 10 files changed, 796 insertions(+), 14 deletions(-) create mode 100644 embedg-server/embedg/bot.go create mode 100644 embedg-server/embedg/commands.go create mode 100644 embedg-server/embedg/helpers.go create mode 100644 embedg-server/embedg/rest/rest.go diff --git a/embedg-server/actions/parser/parse.go b/embedg-server/actions/parser/parse.go index aae2a6df8..ea0fec7d0 100644 --- a/embedg-server/actions/parser/parse.go +++ b/embedg-server/actions/parser/parse.go @@ -300,10 +300,15 @@ func (m *ActionParser) UnparseMessageComponent(data discordgo.MessageComponent) Content: c.Content, }, nil case *discordgo.Thumbnail: + description := "" + if c.Description != nil { + description = *c.Description + } + return actions.ComponentWithActions{ Type: discordgo.ThumbnailComponent, Media: &actions.UnfurledMediaItem{URL: c.Media.URL}, - Description: *c.Description, + Description: description, }, nil case *discordgo.MediaGallery: items := make([]actions.ComponentMediaGalleryItem, 0, len(c.Items)) diff --git a/embedg-server/embedg/bot.go b/embedg-server/embedg/bot.go new file mode 100644 index 000000000..3a050b368 --- /dev/null +++ b/embedg-server/embedg/bot.go @@ -0,0 +1,140 @@ +package embedg + +import ( + "context" + "fmt" + "log/slog" + "strconv" + + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/disgo/handler" + disgorest "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/disgo/sharding" + actionshandler "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" + actionsparser "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" + "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" +) + +type EmbedGeneratorConfig struct { + DiscordToken string +} + +type EmbedGenerator struct { + ctx context.Context + + cfg EmbedGeneratorConfig + + client *bot.Client + clientRouter handler.Router + rest *rest.RestClient + + pg *postgres.PostgresStore + actionHandler *actionshandler.ActionHandler + actionParser *actionsparser.ActionParser +} + +func NewEmbedGenerator( + ctx context.Context, + cfg EmbedGeneratorConfig, + + pg *postgres.PostgresStore, + actionHandler *actionshandler.ActionHandler, + actionParser *actionsparser.ActionParser, +) (*EmbedGenerator, error) { + clientRouter := handler.New() + + client, err := disgo.New(cfg.DiscordToken, + bot.WithShardManagerConfigOpts( + sharding.WithAutoScaling(false), + sharding.WithGatewayConfigOpts( + gateway.WithIntents( + gateway.IntentGuilds, + gateway.IntentGuildMembers, + gateway.IntentGuildExpressions, + gateway.IntentGuildMessages, + gateway.IntentMessageContent, + ), + gateway.WithPresenceOpts( + gateway.WithCustomActivity("message.style"), + ), + ), + ), + bot.WithEventManagerConfigOpts( + bot.WithAsyncEventsEnabled(), + ), + bot.WithRestClient(rest.NewRestClient(cfg.DiscordToken)), + bot.WithCacheConfigOpts( + cache.WithCaches( + cache.FlagGuilds, + cache.FlagChannels, + cache.FlagRoles, + cache.FlagEmojis, + ), + ), + bot.WithEventListenerFunc(func(e *events.Ready) { + slog.Info( + "Shard is ready", + slog.String("shard_id", strconv.Itoa(e.ShardID())), + slog.String("user_id", e.User.ID.String()), + slog.String("username", e.User.Username), + ) + }), + bot.WithEventListeners(clientRouter), + ) + if err != nil { + return nil, fmt.Errorf("Failed to create client: %w", err) + } + + embedg := &EmbedGenerator{ + ctx: ctx, + cfg: cfg, + + client: client, + clientRouter: clientRouter, + + pg: pg, + actionHandler: actionHandler, + actionParser: actionParser, + } + + embedg.registerHandlers() + + return embedg, nil +} + +func (g *EmbedGenerator) Start(ctx context.Context) error { + if err := g.client.OpenShardManager(ctx); err != nil { + return err + } + + return nil +} + +func (g *EmbedGenerator) Close(ctx context.Context) { + g.client.Close(ctx) +} + +func (g *EmbedGenerator) Client() *bot.Client { + return g.client +} + +func (g *EmbedGenerator) Caches() cache.Caches { + return g.client.Caches +} + +func (g *EmbedGenerator) Rest() disgorest.Rest { + return g.client.Rest +} + +func (g *EmbedGenerator) ActionHandler() *actionshandler.ActionHandler { + return g.actionHandler +} + +func (g *EmbedGenerator) ActionParser() *actionsparser.ActionParser { + return g.actionParser +} diff --git a/embedg-server/embedg/commands.go b/embedg-server/embedg/commands.go new file mode 100644 index 000000000..e0b6916ca --- /dev/null +++ b/embedg-server/embedg/commands.go @@ -0,0 +1,544 @@ +package embedg + +import ( + "encoding/json" + "fmt" + "log/slog" + "regexp" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/handler" + "github.com/disgoorg/disgo/handler/middleware" + "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/omit" + "github.com/disgoorg/snowflake/v2" + "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-server/actions" + "github.com/merlinfuchs/embed-generator/embedg-server/util" + "github.com/spf13/viper" +) + +var commands = []discord.ApplicationCommandCreate{ + discord.SlashCommandCreate{ + Name: "help", + Description: "Show help", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "invite", + Description: "Invite the Embed Generator bot to your server", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "website", + Description: "Open the Embed Generator website", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "format", + Description: "Get the API format for mentions, channels, roles, & custom emojis", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionSubCommand{ + Name: "text", + Description: "Get the API format for a text with multiple mentions, channels, & custom emojis", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "text", + Description: "The text that you want to format (usually containing mentions or custom emojis)", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "user", + Description: "Get the API format for mentioning a user", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionUser{ + Name: "user", + Description: "The user you want to mention", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "channel", + Description: "Get the API format for mentioning a channel", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionChannel{ + Name: "channel", + Description: "The channel you want to mention", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "role", + Description: "Get the API format for mentioning a role", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionRole{ + Name: "role", + Description: "The role you want to mention", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "emoji", + Description: "Get the API format for a standard or custom emoji", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "emoji", + Description: "The standard or custom emoji you want to use", + Required: true, + }, + }, + }, + }, + }, + discord.SlashCommandCreate{ + Name: "image", + Description: "Get the image URL for different entities", + Contexts: []discord.InteractionContextType{ + discord.InteractionContextTypeGuild, + }, + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionSubCommand{ + Name: "avatar", + Description: "Get the avatar URL for a user", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionUser{ + Name: "user", + Description: "The user you want to get the avatar for", + Required: true, + }, + discord.ApplicationCommandOptionBool{ + Name: "static", + Description: "Whether animated avatars should be converted to static images", + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "icon", + Description: "Get the icon URL for this server", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionBool{ + Name: "static", + Description: "Whether animated icons should be converted to static images", + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "emoji", + Description: "Get the image URL for a custom or standard emoji", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "emoji", + Description: "The standard or custom emoji you want the image URL for", + Required: true, + }, + discord.ApplicationCommandOptionBool{ + Name: "static", + Description: "Whether animated emojis should be converted to static images", + }, + }, + }, + }, + }, + discord.SlashCommandCreate{ + + Name: "message", + Description: "Get JSON for or restore a message on Embed Generator", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionSubCommand{ + Name: "restore", + Description: "Restore a message on Embed Generator", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "message_id_or_url", + Description: "ID or URL of the message you want to restore", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "dump", + Description: "Get the JSON code for a message", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "message_id_or_url", + Description: "ID or URL of the message you want to restore", + Required: true, + }, + }, + }, + }, + }, + discord.MessageCommandCreate{ + Name: "Restore Message", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.MessageCommandCreate{ + Name: "Dump Message", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.UserCommandCreate{ + Name: "Avatar Url", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.UserCommandCreate{ + Name: "Format Mention", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "embed", + Description: "Create an embed message", + DefaultMemberPermissions: omit.NewPtr(discord.PermissionManageWebhooks), + Contexts: []discord.InteractionContextType{ + discord.InteractionContextTypeGuild, + }, + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + }, +} + +func (g *EmbedGenerator) SyncCommands() error { + if err := handler.SyncCommands(g.Client(), commands, []snowflake.ID{}); err != nil { + return fmt.Errorf("error while syncing commands: %w", err) + } + return nil +} + +func (g *EmbedGenerator) registerHandlers() { + r := g.clientRouter + + r.Use(middleware.Logger) + r.Command("/invite", g.handleHelpCommand) + r.Command("/website", g.handleHelpCommand) + r.Command("/help", g.handleHelpCommand) + + r.Route("/format", func(r handler.Router) { + r.Command("/text", g.handleFormatTextCommand) + r.Command("/user", g.handleFormatUserCommand) + r.Command("/channel", g.handleFormatChannelCommand) + r.Command("/role", g.handleFormatRoleCommand) + r.Command("/emoji", g.handleFormatEmojiCommand) + }) + + r.Route("/image", func(r handler.Router) { + r.Command("/avatar", g.handleImageAvatarCommand) + r.Command("/icon", g.handleImageIconCommand) + r.Command("/emoji", g.handleImageEmojiCommand) + }) + + r.Route("/message", func(r handler.Router) { + r.Command("/restore", g.handleMessageRestoreCommand) + r.Command("/dump", g.handleMessageDumpCommand) + }) + + r.Command("/Restore Message", g.handleMessageRestoreContextCommand) + r.Command("/Dump Message", g.handleMessageDumpContextCommand) + + r.Command("/Avatar Url", g.handleUserAvatarURLContextCommand) + r.Command("/Format Mention", g.handleUserFormatMentionContextCommand) + + r.Command("/embed", g.handleEmbedCommand) +} + +func (g *EmbedGenerator) handleHelpCommand(e *handler.CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "**The best way to generate rich embed messages for your Discord Server!**\n\nhttps://www.youtube.com/watch?v=DnFP0MRJPIg", + Components: []discord.LayoutComponent{ + discord.ActionRowComponent{ + Components: []discord.InteractiveComponent{ + discord.ButtonComponent{ + Style: discord.ButtonStyleLink, + Label: "Website", + URL: "https://message.style", + }, + discord.ButtonComponent{ + Style: discord.ButtonStyleLink, + Label: "Invite Bot", + URL: util.BotInviteURL(), + }, + discord.ButtonComponent{ + Style: discord.ButtonStyleLink, + Label: "Discord Server", + URL: viper.GetString("links.discord"), + }, + }, + }, + }, + }) +} + +func (g *EmbedGenerator) handleFormatTextCommand(e *handler.CommandEvent) error { + value := e.SlashCommandInteractionData().String("text") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for the provided text: ```%s```", value), + }) +} + +func (g *EmbedGenerator) handleFormatUserCommand(e *handler.CommandEvent) error { + user := e.SlashCommandInteractionData().User("user") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for %s: ```<@%s>```", user.Mention(), user.ID), + }) +} + +func (g *EmbedGenerator) handleFormatChannelCommand(e *handler.CommandEvent) error { + channel := e.SlashCommandInteractionData().Channel("channel") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for <#%s>: ```<#%s>```", channel.ID, channel.ID), + }) +} + +func (g *EmbedGenerator) handleFormatRoleCommand(e *handler.CommandEvent) error { + role := e.SlashCommandInteractionData().Role("role") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for %s: ```<@&%s>```", role.Mention(), role.ID), + }) +} + +func (g *EmbedGenerator) handleFormatEmojiCommand(e *handler.CommandEvent) error { + emoji := e.SlashCommandInteractionData().String("emoji") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for %s: ```%s```", emoji, emoji), + }) +} + +func (g *EmbedGenerator) handleImageAvatarCommand(e *handler.CommandEvent) error { + user := e.SlashCommandInteractionData().User("user") + static := e.SlashCommandInteractionData().Bool("static") + + opts := []discord.CDNOpt{ + discord.WithSize(1024), + } + if static { + opts = append(opts, discord.WithFormat(discord.FileFormatPNG)) + } + + avatarURL := user.EffectiveAvatarURL(opts...) + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: avatarURL, + Image: &discord.EmbedResource{ + URL: avatarURL, + }, + }, + }, + }) +} + +func (g *EmbedGenerator) handleImageIconCommand(e *handler.CommandEvent) error { + static := e.SlashCommandInteractionData().Bool("static") + + guild, ok := e.Guild() + if !ok { + slog.Error( + "Guild for image command is not in cache", + slog.Uint64("guild_id", uint64(*e.GuildID())), + ) + return e.CreateMessage(discord.MessageCreate{ + Content: "Server is not in cache, please report this!", + }) + } + + opts := []discord.CDNOpt{ + discord.WithSize(1024), + } + if static { + opts = append(opts, discord.WithFormat(discord.FileFormatPNG)) + } + + iconURL := guild.IconURL(opts...) + if iconURL == nil { + return e.CreateMessage(discord.MessageCreate{ + Content: "This server doesn't have an icon.", + }) + } + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: *iconURL, + Image: &discord.EmbedResource{ + URL: *iconURL, + }, + }, + }, + }) +} + +var emojiRegex = regexp.MustCompile(`<(a?):.+?:(\d{18})>`) +var unicodeEmojiRegex = regexp.MustCompile(`[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{1F1E0}-\x{1F1FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]`) + +func (g *EmbedGenerator) handleImageEmojiCommand(e *handler.CommandEvent) error { + rawEmoji := e.SlashCommandInteractionData().String("emoji") + static := e.SlashCommandInteractionData().Bool("static") + + // Check if it's a unicode emoji + if unicodeEmojiRegex.MatchString(rawEmoji) { + emojiURL := emojiImageURL(rawEmoji, false) + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: emojiURL, + Image: &discord.EmbedResource{ + URL: emojiURL, + }, + }, + }, + }) + } + + // Parse Discord emoji + matches := emojiRegex.FindStringSubmatch(rawEmoji) + if len(matches) < 2 { + return e.CreateMessage(discord.MessageCreate{ + Content: "Invalid emoji format. Please use a custom Discord emoji like `<:name:id>` or ``.", + }) + } + + emojiID := matches[2] + isAnimated := matches[1] == "a" + + // Build the URL + extension := "gif" + if static || !isAnimated { + extension = "png" + } + + emojiURL := fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.%s", emojiID, extension) + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: emojiURL, + Image: &discord.EmbedResource{ + URL: emojiURL, + }, + }, + }, + }) +} + +var messageURLRegex = regexp.MustCompile(`https?://(?:canary\\.|ptb\\.)?discord\\.com/channels/[0-9]+/([0-9]+)/([0-9]+)`) +var messageIDRegex = regexp.MustCompile(`^[0-9]+$`) + +func (g *EmbedGenerator) handleMessageRestoreCommand(e *handler.CommandEvent) error { + messageIDOrURL := e.SlashCommandInteractionData().String("message_id_or_url") + + channelID := e.Channel().ID() + var messageID snowflake.ID + + match := messageURLRegex.FindStringSubmatch(messageIDOrURL) + if match != nil { + channelID, _ = snowflake.Parse(match[1]) + messageID, _ = snowflake.Parse(match[2]) + + channel, ok := g.Client().Caches.Channel(channelID) + if !ok { + return e.CreateMessage(discord.MessageCreate{ + Content: "The message belongs to a channel that the bot doesn't have access to.", + }) + } + + if channel.GuildID() != *e.GuildID() { + return e.CreateMessage(discord.MessageCreate{ + Content: "The channel doesn't belong to this server.", + }) + } + } + + message, err := g.Client().Rest.GetMessage(channelID, messageID, rest.WithCtx(g.ctx)) + if err != nil { + if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMessage) { + return e.CreateMessage(discord.MessageCreate{ + Content: "Message not found.", + }) + } + + return e.CreateMessage(discord.MessageCreate{ + Content: "Failed to get message.", + }) + } + + // TODO: Unparse components + + messageDump, err := json.MarshalIndent(actions.MessageWithActions{ + Username: message.Author.Username, + // AvatarURL: message.Author.AvatarURL("1024"), + Content: message.Content, + // Embeds: message.Embeds, + }, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal message dump: %w", err) + } + + fmt.Println(messageDump) + + return nil +} + +func (g *EmbedGenerator) handleMessageRestoreContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleMessageDumpCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleMessageDumpContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleUserAvatarURLContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleUserFormatMentionContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleEmbedCommand(e *handler.CommandEvent) error { + return nil +} diff --git a/embedg-server/embedg/helpers.go b/embedg-server/embedg/helpers.go new file mode 100644 index 000000000..2ec535863 --- /dev/null +++ b/embedg-server/embedg/helpers.go @@ -0,0 +1,32 @@ +package embedg + +import ( + "fmt" + "strings" +) + +func emojiImageURL(emoji string, animated bool) string { + // Convert unicode emoji to Twemoji URL + var codepoints []string + + // Iterate through each rune in the emoji string + for _, r := range emoji { + // Skip zero-width joiners and variation selectors + if r == 0x200D || r == 0xFE0F { + continue + } + // Convert rune to lowercase hex codepoint + codepoints = append(codepoints, fmt.Sprintf("%x", r)) + } + + // Join codepoints with hyphens for multi-codepoint emojis + unicode := strings.Join(codepoints, "-") + + if animated { + // Google Noto animated emoji URL structure + // https://googlefonts.github.io/noto-emoji-animation/ + return fmt.Sprintf("https://fonts.gstatic.com/s/e/notoemoji/latest/%s/512.gif", unicode) + } + + return fmt.Sprintf("https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/%s.png", unicode) +} diff --git a/embedg-server/embedg/rest/rest.go b/embedg-server/embedg/rest/rest.go new file mode 100644 index 000000000..e02e11ca3 --- /dev/null +++ b/embedg-server/embedg/rest/rest.go @@ -0,0 +1,15 @@ +package rest + +import "github.com/disgoorg/disgo/rest" + +type RestClient struct { + rest.Rest +} + +func NewRestClient(token string, opts ...rest.ConfigOpt) *RestClient { + return &RestClient{ + Rest: rest.New(rest.NewClient(token, opts...)), + } +} + +// TODO: Add caching for some endpoints diff --git a/embedg-server/go.mod b/embedg-server/go.mod index 5b22ae579..8a5d2b920 100644 --- a/embedg-server/go.mod +++ b/embedg-server/go.mod @@ -1,13 +1,13 @@ module github.com/merlinfuchs/embed-generator/embedg-server -go 1.18 +go 1.24 require ( github.com/adhocore/gronx v1.8.1 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/gofiber/fiber/v2 v2.43.0 github.com/golang-migrate/migrate/v4 v4.15.2 - github.com/gorilla/websocket v1.5.0 + github.com/gorilla/websocket v1.5.3 github.com/jellydator/ttlcache/v3 v3.0.1 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 @@ -36,6 +36,10 @@ require ( github.com/botlabs-gg/yagpdb/v2 v2.38.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/disgoorg/disgo v0.19.0-rc.10 // indirect + github.com/disgoorg/json/v2 v2.0.0 // indirect + github.com/disgoorg/omit v1.0.0 // indirect + github.com/disgoorg/snowflake/v2 v2.0.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -47,7 +51,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -64,6 +68,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rs/xid v1.5.0 // indirect + github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -71,7 +76,7 @@ require ( github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -82,11 +87,11 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/embedg-server/go.sum b/embedg-server/go.sum index 582bc5319..784a07609 100644 --- a/embedg-server/go.sum +++ b/embedg-server/go.sum @@ -368,6 +368,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.10 h1:0frpeeoM9pHouHjhLeZDuDTJ0PqjDTrycaHaMmkJAo8= github.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0= +github.com/disgoorg/disgo v0.19.0-rc.10 h1:FYflS+wU7TF8u/qFfeytcsHkrqu2rGib7QtdkRmYBzY= +github.com/disgoorg/disgo v0.19.0-rc.10/go.mod h1:JORF6o1leAHEo1bv2SKe0zTuUMhreIj3a7lCF025Te0= +github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI= +github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI= +github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78= +github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ= +github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= +github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= @@ -640,6 +648,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -784,6 +794,8 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -1091,6 +1103,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/sashabaranov/go-openai v1.17.6 h1:hYXRPM1xO6QLOJhWEOMlSg/l3jERiKDKd1qIoK22lvs= github.com/sashabaranov/go-openai v1.17.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/sashabaranov/go-openai v1.35.7 h1:icyrRbkYoKPa4rbO1WSInpJu3qDQrPEnsoJVZ6QymdI= @@ -1180,6 +1194,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= @@ -1341,6 +1356,8 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1498,6 +1515,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1630,6 +1648,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1651,6 +1671,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/embedg-server/util/util.go b/embedg-server/util/util.go index 77341124d..dc87a539c 100644 --- a/embedg-server/util/util.go +++ b/embedg-server/util/util.go @@ -2,10 +2,12 @@ package util import ( "crypto/sha256" + "errors" "fmt" "mime" "strconv" + "github.com/disgoorg/disgo/rest" "github.com/merlinfuchs/discordgo" "github.com/spf13/viper" ) @@ -34,17 +36,28 @@ func GetFileExtensionFromMimeType(mimeType string) string { } func IsDiscordRestErrorCode(err error, codes ...int) bool { - if err, ok := err.(*discordgo.RESTError); ok { - if err.Message == nil { + var restError *discordgo.RESTError + if errors.As(err, &restError) { + if restError.Message == nil { return false } for _, code := range codes { - if err.Message.Code == code { + if restError.Message.Code == code { return true } } } + + var httpErr *rest.Error + if errors.As(err, &httpErr) { + for _, code := range codes { + if int(httpErr.Code) == code { + return true + } + } + } + return false } diff --git a/go.work b/go.work index 871260bed..783f2f46a 100644 --- a/go.work +++ b/go.work @@ -1,5 +1,7 @@ -go 1.18 +go 1.24 use ./embedg-server + use ./embedg-app -use ./embedg-site \ No newline at end of file + +use ./embedg-site diff --git a/go.work.sum b/go.work.sum index eaf2a658c..8521519dd 100644 --- a/go.work.sum +++ b/go.work.sum @@ -645,15 +645,20 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhp golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s= From 93f7fa3934b5eb371cb3bf3a54fec43e125835ab Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Sun, 26 Oct 2025 21:07:20 +0100 Subject: [PATCH 2/8] start swapping out discordgo with disgo --- embedg-server/actions/data.go | 12 +- embedg-server/actions/parser/parse.go | 7 +- embedg-server/actions/parser/permissions.go | 84 ++++---- embedg-server/actions/template/data.go | 201 +++++++++++------- embedg-server/actions/template/provider.go | 17 +- embedg-server/api/access/access.go | 91 ++++---- embedg-server/api/access/check.go | 5 +- embedg-server/api/access/helpers.go | 54 +++-- embedg-server/api/api.go | 7 +- .../api/handlers/assistant/handler.go | 6 +- embedg-server/api/handlers/auth/handler.go | 5 +- .../api/handlers/custom_bots/handler.go | 76 ++++--- embedg-server/api/handlers/guilds/handler.go | 141 ++++++------ embedg-server/api/handlers/health/handler.go | 77 ++++--- embedg-server/api/handlers/images/handler.go | 19 +- .../api/handlers/interaction/handler.go | 15 +- embedg-server/api/handlers/premium/handler.go | 46 ++-- .../api/handlers/send_message/handler.go | 12 +- .../api/handlers/shared_messages/handler.go | 9 +- embedg-server/api/handlers/users/handler.go | 2 +- embedg-server/api/managers.go | 24 ++- embedg-server/api/premium/manager.go | 26 ++- embedg-server/api/premium/roles.go | 44 ++-- embedg-server/api/routes.go | 33 ++- embedg-server/api/session/session.go | 14 +- embedg-server/api/stores.go | 2 - embedg-server/api/wire/guild.go | 37 ++-- embedg-server/api/wire/health.go | 13 +- embedg-server/embedg/bot.go | 17 +- embedg-server/embedg/commands.go | 4 +- embedg-server/embedg/rest/rest.go | 54 ++++- embedg-server/entry/server/cmd.go | 21 +- embedg-server/scheduled_messages/manager.go | 19 +- embedg-server/store/plan.go | 5 +- embedg-server/util/id.go | 52 ++++- embedg-server/util/util.go | 17 ++ 36 files changed, 775 insertions(+), 493 deletions(-) diff --git a/embedg-server/actions/data.go b/embedg-server/actions/data.go index b68ac8971..2f425795d 100644 --- a/embedg-server/actions/data.go +++ b/embedg-server/actions/data.go @@ -3,7 +3,9 @@ package actions import ( "slices" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) type MessageWithActions struct { @@ -117,11 +119,11 @@ type ActionSet struct { } type ActionDerivedPermissions struct { - UserID string `json:"user_id"` - GuildIsOwner bool `json:"guild_is_owner"` - GuildPermissions int64 `json:"guild_permissions"` - ChannelPermissions int64 `json:"channel_permissions"` - AllowedRoleIDs []string `json:"lower_role_ids"` + UserID util.ID `json:"user_id"` + GuildIsOwner bool `json:"guild_is_owner"` + GuildPermissions discord.Permissions `json:"guild_permissions"` + ChannelPermissions discord.Permissions `json:"channel_permissions"` + AllowedRoleIDs []util.ID `json:"lower_role_ids"` } func (a *ActionDerivedPermissions) HasChannelPermission(permission int64) bool { diff --git a/embedg-server/actions/parser/parse.go b/embedg-server/actions/parser/parse.go index ea0fec7d0..c6ba4ff76 100644 --- a/embedg-server/actions/parser/parse.go +++ b/embedg-server/actions/parser/parse.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/disgoorg/disgo/cache" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" @@ -16,14 +17,14 @@ import ( type ActionParser struct { accessManager *access.AccessManager pg *postgres.PostgresStore - state *discordgo.State + caches cache.Caches } -func New(accessManager *access.AccessManager, pg *postgres.PostgresStore, state *discordgo.State) *ActionParser { +func New(accessManager *access.AccessManager, pg *postgres.PostgresStore, caches cache.Caches) *ActionParser { return &ActionParser{ accessManager: accessManager, pg: pg, - state: state, + caches: caches, } } diff --git a/embedg-server/actions/parser/permissions.go b/embedg-server/actions/parser/permissions.go index 38b8b4c14..bacf11821 100644 --- a/embedg-server/actions/parser/permissions.go +++ b/embedg-server/actions/parser/permissions.go @@ -6,33 +6,33 @@ import ( "encoding/json" "fmt" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) -func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actions.ActionSet, userID string, guildID string, channelID string) error { - var channel *discordgo.Channel - if channelID != "" { - var err error - channel, err = m.state.Channel(channelID) - if err != nil { - return err +func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actions.ActionSet, userID util.ID, guildID util.ID, channelID util.ID) error { + if channelID != 0 { + channel, ok := m.caches.Channel(channelID) + if !ok { + return fmt.Errorf("channel not found in cache") } - if channel.GuildID != guildID { + if channel.GuildID() != guildID { return fmt.Errorf("Channel %s does not belong to guild %s", channelID, guildID) } } - guild, err := m.state.Guild(guildID) - if err != nil { - return err + guild, ok := m.caches.Guild(guildID) + if !ok { + return fmt.Errorf("guild not found in cache") } var channelAccess *access.ChannelAccess - if channelID != "" { + if channelID != 0 { ca, err := m.accessManager.GetChannelAccessForUser(userID, channelID) if err != nil { return err @@ -52,17 +52,17 @@ func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actio memberIsOwner := guild.OwnerID == userID highestRolePosition := 0 - var permissions int64 + var permissions discord.Permissions - defaultRole, err := m.state.Role(guildID, guildID) - if err == nil { + defaultRole, ok := m.caches.Role(guildID, guildID) + if ok { highestRolePosition = defaultRole.Position permissions = defaultRole.Permissions } - for _, roleID := range member.Roles { - role, err := m.state.Role(guildID, roleID) - if err == nil && role.Position > highestRolePosition { + for _, roleID := range member.RoleIDs { + role, ok := m.caches.Role(guildID, roleID) + if ok && role.Position > highestRolePosition { highestRolePosition = role.Position permissions |= role.Permissions } @@ -89,12 +89,14 @@ func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actio return fmt.Errorf("You have no permission to manage roles in the channel %s", channelID) } - role, err := m.state.Role(guildID, action.TargetID) + roleID, err := util.ParseID(action.TargetID) if err != nil { - if err == discordgo.ErrStateNotFound { - return fmt.Errorf("Role %s does not exist", action.TargetID) - } - return err + return fmt.Errorf("Invalid role ID: %s", action.TargetID) + } + + role, ok := m.caches.Role(guildID, roleID) + if !ok { + return fmt.Errorf("Role %s does not exist", action.TargetID) } if !memberIsOwner && role.Position >= highestRolePosition { @@ -103,7 +105,7 @@ func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actio break case actions.ActionTypeSavedMessageResponse, actions.ActionTypeSavedMessageDM, actions.ActionTypeSavedMessageEdit: msg, err := m.pg.Q.GetSavedMessageForGuild(context.TODO(), pgmodel.GetSavedMessageForGuildParams{ - GuildID: sql.NullString{Valid: true, String: guildID}, + GuildID: sql.NullString{Valid: true, String: guildID.String()}, ID: action.TargetID, }) if err != nil { @@ -130,32 +132,30 @@ func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actio return checkActions(actionSets, 0) } -func (m *ActionParser) DerivePermissionsForActions(userID string, guildID string, channelID string) (actions.ActionDerivedPermissions, error) { +func (m *ActionParser) DerivePermissionsForActions(userID util.ID, guildID util.ID, channelID util.ID) (actions.ActionDerivedPermissions, error) { res := actions.ActionDerivedPermissions{ UserID: userID, } - var channel *discordgo.Channel - if channelID != "" { - var err error - channel, err = m.state.Channel(channelID) - if err != nil { - return res, err + if channelID != 0 { + channel, ok := m.caches.Channel(channelID) + if !ok { + return res, fmt.Errorf("channel not found in cache") } - if channel.GuildID != guildID { + if channel.GuildID() != guildID { return res, fmt.Errorf("Channel %s does not belong to guild %s", channelID, guildID) } } - guild, err := m.state.Guild(guildID) - if err != nil { - return res, err + guild, ok := m.caches.Guild(guildID) + if !ok { + return res, fmt.Errorf("guild not found in cache") } res.GuildIsOwner = guild.OwnerID == userID - if channelID != "" { + if channelID != 0 { ca, err := m.accessManager.GetChannelAccessForUser(userID, channelID) if err != nil { return res, err @@ -170,21 +170,21 @@ func (m *ActionParser) DerivePermissionsForActions(userID string, guildID string highestRolePosition := 0 - defaultRole, err := m.state.Role(guildID, guildID) - if err == nil { + defaultRole, ok := m.caches.Role(guildID, guildID) + if ok { highestRolePosition = defaultRole.Position res.GuildPermissions = defaultRole.Permissions } - for _, roleID := range member.Roles { - role, err := m.state.Role(guildID, roleID) - if err == nil && role.Position > highestRolePosition { + for _, roleID := range member.RoleIDs { + role, ok := m.caches.Role(guildID, roleID) + if ok && role.Position > highestRolePosition { highestRolePosition = role.Position res.GuildPermissions |= role.Permissions } } - for _, role := range guild.Roles { + for role := range m.caches.Roles(guildID) { if role.Position < highestRolePosition { res.AllowedRoleIDs = append(res.AllowedRoleIDs, role.ID) } diff --git a/embedg-server/actions/template/data.go b/embedg-server/actions/template/data.go index 9b2802633..f2a017c51 100644 --- a/embedg-server/actions/template/data.go +++ b/embedg-server/actions/template/data.go @@ -4,20 +4,23 @@ import ( "fmt" "time" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) var standardDataMap = map[string]interface{}{} type InteractionData struct { - state *discordgo.State - i *discordgo.Interaction + caches cache.Caches + i *discord.Interaction } -func NewInteractionData(state *discordgo.State, i *discordgo.Interaction) *InteractionData { +func NewInteractionData(caches cache.Caches, i *discord.Interaction) *InteractionData { return &InteractionData{ - state: state, - i: i, + caches: caches, + i: i, } } @@ -48,10 +51,10 @@ func (d *InteractionData) Command() *CommandData { } type UserData struct { - u *discordgo.User + u discord.User } -func NewUserData(u *discordgo.User) *UserData { +func NewUserData(u discord.User) *UserData { return &UserData{u: u} } @@ -60,12 +63,12 @@ func (d *UserData) String() string { } func (d *UserData) ID() string { - return d.u.ID + return d.u.ID.String() } func (d *UserData) Name() string { - if d.u.GlobalName != "" { - return d.u.GlobalName + if d.u.GlobalName != nil { + return *d.u.GlobalName } return d.u.Username @@ -76,7 +79,11 @@ func (d *UserData) Username() string { } func (d *UserData) GlobalName() string { - return d.u.GlobalName + if d.u.GlobalName != nil { + return *d.u.GlobalName + } + + return "" } func (d *UserData) Discriminator() string { @@ -84,11 +91,19 @@ func (d *UserData) Discriminator() string { } func (d *UserData) Avatar() string { - return d.u.Avatar + if d.u.Avatar != nil { + return *d.u.Avatar + } + + return "" } func (d *UserData) Banner() string { - return d.u.Banner + if d.u.Banner != nil { + return *d.u.Banner + } + + return "" } func (d *UserData) Mention() string { @@ -96,68 +111,82 @@ func (d *UserData) Mention() string { } func (d *UserData) AvatarURL() string { - return d.u.AvatarURL("512") + avatarURL := d.u.AvatarURL(discord.WithSize(512)) + if avatarURL == nil { + return "" + } + + return *avatarURL } func (d *UserData) BannerURL() string { - return d.u.BannerURL("1024") + bannerURL := d.u.BannerURL(discord.WithSize(1024)) + if bannerURL == nil { + return "" + } + + return *bannerURL } type MemberData struct { UserData - state *discordgo.State - guildID string - m *discordgo.Member + caches cache.Caches + guildID util.ID + m *discord.Member } -func NewMemberData(state *discordgo.State, guildID string, m *discordgo.Member) *MemberData { +func NewMemberData(caches cache.Caches, guildID util.ID, m *discord.Member) *MemberData { return &MemberData{ UserData: UserData{m.User}, - state: state, + caches: caches, guildID: guildID, m: m, } } func (d *MemberData) Nick() string { - return d.m.Nick + if d.m.Nick != nil { + return *d.m.Nick + } + + return "" } func (d *MemberData) Roles() []*RoleData { - res := make([]*RoleData, len(d.m.Roles)) - for i, roleID := range d.m.Roles { - res[i] = NewRoleData(d.state, d.guildID, roleID, nil) + res := make([]*RoleData, len(d.m.RoleIDs)) + for i, roleID := range d.m.RoleIDs { + res[i] = NewRoleData(d.caches, d.guildID, roleID, nil) } return res } func (d *MemberData) JoinedAt() time.Time { - return d.m.JoinedAt + if d.m.JoinedAt != nil { + return *d.m.JoinedAt + } + + return time.Time{} } func (d *MemberData) Name() string { - if d.m.Nick != "" { - return d.m.Nick + if d.m.Nick != nil { + return *d.m.Nick } return d.UserData.Name() } func (d *MemberData) Avatar() string { - if d.m.Avatar != "" { - return d.m.Avatar + if d.m.Avatar != nil { + return *d.m.Avatar } return d.UserData.Avatar() } func (d *MemberData) AvatarURL() string { - if d.m.Avatar != "" { - return d.m.AvatarURL("512") - } - - return d.UserData.AvatarURL() + return d.m.EffectiveAvatarURL(discord.WithSize(512)) } type CommandData struct { @@ -246,14 +275,14 @@ func NewCommandOptionData(state *discordgo.State, guildID string, c *discordgo.A } type GuildData struct { - state *discordgo.State - guildID string - guild *discordgo.Guild + caches cache.Caches + guildID util.ID + guild *discord.Guild } -func NewGuildData(state *discordgo.State, guildID string, g *discordgo.Guild) *GuildData { +func NewGuildData(caches cache.Caches, guildID util.ID, g *discord.Guild) *GuildData { return &GuildData{ - state: state, + caches: caches, guildID: guildID, guild: g, } @@ -264,24 +293,24 @@ func (d *GuildData) ensureGuild() error { return nil } - guild, err := d.state.Guild(d.guildID) - if err != nil { - return err + guild, ok := d.caches.Guild(d.guildID) + if !ok { + return fmt.Errorf("guild not found in cache") } - d.guild = guild + d.guild = &guild return nil } func (d *GuildData) String() string { if err := d.ensureGuild(); err != nil { - return d.guildID + return d.guildID.String() } return d.guild.Name } func (d *GuildData) ID() string { - return d.guildID + return d.guildID.String() } func (d *GuildData) Name() (string, error) { @@ -297,7 +326,11 @@ func (d *GuildData) Description() (string, error) { return "", err } - return d.guild.Description, nil + if d.guild.Description != nil { + return *d.guild.Description, nil + } + + return "", nil } func (d *GuildData) Icon() (string, error) { @@ -305,7 +338,11 @@ func (d *GuildData) Icon() (string, error) { return "", err } - return d.guild.Icon, nil + if d.guild.Icon != nil { + return *d.guild.Icon, nil + } + + return "", nil } func (d *GuildData) IconURL() (string, error) { @@ -313,7 +350,12 @@ func (d *GuildData) IconURL() (string, error) { return "", err } - return d.guild.IconURL("512"), nil + iconURL := d.guild.IconURL(discord.WithSize(512)) + if iconURL == nil { + return "", nil + } + + return *iconURL, nil } func (d *GuildData) Banner() (string, error) { @@ -321,7 +363,11 @@ func (d *GuildData) Banner() (string, error) { return "", err } - return d.guild.Banner, nil + if d.guild.Banner != nil { + return *d.guild.Banner, nil + } + + return "", nil } func (d *GuildData) BannerURL() (string, error) { @@ -329,12 +375,16 @@ func (d *GuildData) BannerURL() (string, error) { return "", err } - return d.guild.BannerURL("1024"), nil + bannerURL := d.guild.BannerURL(discord.WithSize(1024)) + if bannerURL == nil { + return "", nil + } + + return *bannerURL, nil } func (d *GuildData) MemberCount() (int, error) { if err := d.ensureGuild(); err != nil { - fmt.Println(err) return 0, err } @@ -358,14 +408,14 @@ func (d *GuildData) BoostLevel() (int, error) { } type ChannelData struct { - state *discordgo.State + caches cache.Caches channelID string - channel *discordgo.Channel + channel discord.GuildChannel } -func NewChannelData(state *discordgo.State, channelID string, c *discordgo.Channel) *ChannelData { +func NewChannelData(caches cache.Caches, channelID string, c discord.GuildChannel) *ChannelData { return &ChannelData{ - state: state, + caches: caches, channelID: channelID, channel: c, } @@ -376,9 +426,9 @@ func (d *ChannelData) ensureChannel() error { return nil } - channel, err := d.state.Channel(d.channelID) - if err != nil { - return err + channel, ok := d.caches.Channel(util.ToID(d.channelID)) + if !ok { + return fmt.Errorf("channel not found in cache") } d.channel = channel @@ -398,7 +448,7 @@ func (d *ChannelData) Name() (string, error) { return "", err } - return d.channel.Name, nil + return d.channel.Name(), nil } func (d *ChannelData) Mention() string { @@ -410,19 +460,26 @@ func (d *ChannelData) Topic() (string, error) { return "", err } - return d.channel.Topic, nil + if text, ok := d.channel.(discord.GuildTextChannel); ok { + topic := text.Topic() + if topic != nil { + return *topic, nil + } + } + + return "", nil } type RoleData struct { - state *discordgo.State - guildID string - roleID string - role *discordgo.Role + caches cache.Caches + guildID util.ID + roleID util.ID + role *discord.Role } -func NewRoleData(state *discordgo.State, guildID string, roleID string, role *discordgo.Role) *RoleData { +func NewRoleData(caches cache.Caches, guildID util.ID, roleID util.ID, role *discord.Role) *RoleData { return &RoleData{ - state: state, + caches: caches, guildID: guildID, roleID: roleID, role: role, @@ -434,12 +491,12 @@ func (d *RoleData) ensureRole() error { return nil } - role, err := d.state.Role(d.guildID, d.roleID) - if err != nil { - return err + role, ok := d.caches.Role(d.guildID, d.roleID) + if !ok { + return fmt.Errorf("role not found in cache") } - d.role = role + d.role = &role return nil } @@ -448,11 +505,11 @@ func (d *RoleData) String() string { } func (d *RoleData) ID() string { - return d.roleID + return d.roleID.String() } func (d *RoleData) Mention() string { - return fmt.Sprintf("<@&%s>", d.roleID) + return fmt.Sprintf("<@&%s>", d.roleID.String()) } func (d *RoleData) Name() (string, error) { diff --git a/embedg-server/actions/template/provider.go b/embedg-server/actions/template/provider.go index 8ecf5c548..4f0c672e2 100644 --- a/embedg-server/actions/template/provider.go +++ b/embedg-server/actions/template/provider.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/disgoorg/disgo/cache" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/model" "github.com/merlinfuchs/embed-generator/embedg-server/store" @@ -44,14 +45,14 @@ func (p *InteractionProvider) ProvideData(data map[string]interface{}) { } type GuildProvider struct { - state *discordgo.State + caches cache.Caches guildID string guild *discordgo.Guild } -func NewGuildProvider(state *discordgo.State, guildID string, guild *discordgo.Guild) *GuildProvider { +func NewGuildProvider(caches cache.Caches, guildID string, guild *discordgo.Guild) *GuildProvider { return &GuildProvider{ - state: state, + caches: caches, guildID: guildID, guild: guild, } @@ -60,20 +61,20 @@ func NewGuildProvider(state *discordgo.State, guildID string, guild *discordgo.G func (p *GuildProvider) ProvideFuncs(funcs map[string]interface{}) {} func (p *GuildProvider) ProvideData(data map[string]interface{}) { - guildData := NewGuildData(p.state, p.guildID, p.guild) + guildData := NewGuildData(p.caches, p.guildID, p.guild) data["Guild"] = guildData data["Server"] = guildData } type ChannelProvider struct { - state *discordgo.State + caches cache.Caches channelID string channel *discordgo.Channel } -func NewChannelProvider(state *discordgo.State, channelID string, channel *discordgo.Channel) *ChannelProvider { +func NewChannelProvider(caches cache.Caches, channelID string, channel *discordgo.Channel) *ChannelProvider { return &ChannelProvider{ - state: state, + caches: caches, channelID: channelID, channel: channel, } @@ -82,7 +83,7 @@ func NewChannelProvider(state *discordgo.State, channelID string, channel *disco func (p *ChannelProvider) ProvideFuncs(funcs map[string]interface{}) {} func (p *ChannelProvider) ProvideData(data map[string]interface{}) { - data["Channel"] = NewChannelData(p.state, p.channelID, p.channel) + data["Channel"] = NewChannelData(p.caches, p.channelID, p.channel) } type KVProvider struct { diff --git a/embedg-server/api/access/access.go b/embedg-server/api/access/access.go index 018b45b38..4199e7e4f 100644 --- a/embedg-server/api/access/access.go +++ b/embedg-server/api/access/access.go @@ -1,26 +1,25 @@ package access import ( - "context" "fmt" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/bot/rest" "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/spf13/viper" ) type AccessManager struct { - state *discordgo.State - session *discordgo.Session - rest rest.RestClient + caches cache.Caches + rest rest.Rest } -func New(state *discordgo.State, session *discordgo.Session, rest rest.RestClient) *AccessManager { +func New(caches cache.Caches, rest rest.Rest) *AccessManager { return &AccessManager{ - state: state, - session: session, - rest: rest, + caches: caches, + rest: rest, } } @@ -30,8 +29,8 @@ type GuildAccess struct { } type ChannelAccess struct { - UserPermissions int64 - BotPermissions int64 + UserPermissions discord.Permissions + BotPermissions discord.Permissions } func (c *ChannelAccess) UserAccess() bool { @@ -42,25 +41,24 @@ func (c *ChannelAccess) BotAccess() bool { return c.BotPermissions&(discordgo.PermissionManageWebhooks|discordgo.PermissionAdministrator) != 0 } -func (m *AccessManager) GetGuildAccessForUser(userID string, guildID string) (GuildAccess, error) { +func (m *AccessManager) GetGuildAccessForUser(userID util.ID, guildID util.ID) (GuildAccess, error) { res := GuildAccess{} - guild, err := m.state.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return res, nil - } - return res, err + guild, ok := m.caches.Guild(guildID) + if !ok { + return res, nil } if guild.OwnerID == userID { res.HasChannelWithUserAccess = true } - for _, channel := range guild.Channels { + channels := m.caches.ChannelsForGuild(guildID) + + for channel := range channels { if !res.HasChannelWithUserAccess { access := ChannelAccess{} - err := m.SetChannelAccessUserPermissions(&access, userID, channel.ID) + err := m.SetChannelAccessUserPermissions(&access, userID, channel.ID()) if err != nil { return res, err } @@ -72,7 +70,7 @@ func (m *AccessManager) GetGuildAccessForUser(userID string, guildID string) (Gu if !res.HasChannelWithBotAccess { access := ChannelAccess{} - err = m.SetChannelAccessBotPermissions(&access, channel.ID) + err := m.SetChannelAccessBotPermissions(&access, channel.ID()) if err != nil { return res, err } @@ -91,7 +89,7 @@ func (m *AccessManager) GetGuildAccessForUser(userID string, guildID string) (Gu return res, nil } -func (m *AccessManager) GetChannelAccessForUser(userID string, channelID string) (ChannelAccess, error) { +func (m *AccessManager) GetChannelAccessForUser(userID util.ID, channelID util.ID) (ChannelAccess, error) { res := ChannelAccess{} err := m.SetChannelAccessUserPermissions(&res, userID, channelID) @@ -107,7 +105,7 @@ func (m *AccessManager) GetChannelAccessForUser(userID string, channelID string) return res, nil } -func (m *AccessManager) SetChannelAccessUserPermissions(res *ChannelAccess, userID string, channelID string) (err error) { +func (m *AccessManager) SetChannelAccessUserPermissions(res *ChannelAccess, userID util.ID, channelID util.ID) (err error) { res.UserPermissions, err = m.ComputeUserPermissionsForChannel(userID, channelID) if err != nil { if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMember) { @@ -120,7 +118,7 @@ func (m *AccessManager) SetChannelAccessUserPermissions(res *ChannelAccess, user return nil } -func (m *AccessManager) SetChannelAccessBotPermissions(res *ChannelAccess, channelID string) error { +func (m *AccessManager) SetChannelAccessBotPermissions(res *ChannelAccess, channelID util.ID) error { botPerms, err := m.ComputeBotPermissionsForChannel(channelID) if err != nil { return err @@ -134,21 +132,21 @@ func (m *AccessManager) SetChannelAccessBotPermissions(res *ChannelAccess, chann return nil } -func (m *AccessManager) ComputeUserPermissionsForChannel(userID string, channelID string) (int64, error) { - channel, err := m.state.Channel(channelID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return 0, nil - } - return 0, err +func (m *AccessManager) ComputeUserPermissionsForChannel(userID util.ID, channelID util.ID) (discord.Permissions, error) { + channel, ok := m.caches.Channel(channelID) + if !ok { + return 0, nil } - guild, err := m.state.Guild(channel.GuildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return 0, nil - } - return 0, err + guild, ok := m.caches.Guild(channel.GuildID()) + if !ok { + return 0, nil + } + + roleIterator := m.caches.Roles(channel.GuildID()) + roles := make([]discord.Role, 0) + for role := range roleIterator { + roles = append(roles, role) } if guild.OwnerID == userID { @@ -161,23 +159,26 @@ func (m *AccessManager) ComputeUserPermissionsForChannel(userID string, channelI return 0, err } - perms := memberPermissions(guild, channel, userID, member.Roles) + perms := memberPermissions(&guild, roles, channel, userID, member.RoleIDs) return perms, err } -func (m *AccessManager) ComputeBotPermissionsForChannel(channelID string) (int64, error) { - userID := viper.GetString("discord.client_id") +func (m *AccessManager) ComputeBotPermissionsForChannel(channelID util.ID) (discord.Permissions, error) { + userID, err := util.ParseID(viper.GetString("discord.client_id")) + if err != nil { + return 0, fmt.Errorf("Failed to parse bot user ID: %w", err) + } return m.ComputeUserPermissionsForChannel(userID, channelID) } -func (m *AccessManager) GetGuildMember(guildID string, userID string) (*discordgo.Member, error) { - member, _ := m.state.Member(guildID, userID) - if member != nil { - return member, nil +func (m *AccessManager) GetGuildMember(guildID util.ID, userID util.ID) (*discord.Member, error) { + cached, ok := m.caches.Member(guildID, userID) + if ok { + return &cached, nil } - member, err := m.rest.GuildMember(context.Background(), guildID, userID) + member, err := m.rest.GetMember(guildID, userID) if err != nil { return nil, fmt.Errorf("Failed to get guild member: %w", err) } diff --git a/embedg-server/api/access/check.go b/embedg-server/api/access/check.go index ed1a9f8a5..45b61e98f 100644 --- a/embedg-server/api/access/check.go +++ b/embedg-server/api/access/check.go @@ -4,9 +4,10 @@ import ( "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) -func (m *AccessManager) CheckGuildAccessForRequest(c *fiber.Ctx, guildID string) error { +func (m *AccessManager) CheckGuildAccessForRequest(c *fiber.Ctx, guildID util.ID) error { session := c.Locals("session").(*session.Session) access, err := m.GetGuildAccessForUser(session.UserID, guildID) @@ -25,7 +26,7 @@ func (m *AccessManager) CheckGuildAccessForRequest(c *fiber.Ctx, guildID string) return nil } -func (m *AccessManager) CheckChannelAccessForRequest(c *fiber.Ctx, channelID string) error { +func (m *AccessManager) CheckChannelAccessForRequest(c *fiber.Ctx, channelID util.ID) error { session := c.Locals("session").(*session.Session) access, err := m.GetChannelAccessForUser(session.UserID, channelID) diff --git a/embedg-server/api/access/helpers.go b/embedg-server/api/access/helpers.go index 1c2e33587..09eec9869 100644 --- a/embedg-server/api/access/helpers.go +++ b/embedg-server/api/access/helpers.go @@ -1,22 +1,26 @@ package access -import "github.com/merlinfuchs/discordgo" +import ( + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-server/util" +) -func memberPermissions(guild *discordgo.Guild, channel *discordgo.Channel, userID string, roles []string) (apermissions int64) { +func memberPermissions(guild *discord.Guild, roles []discord.Role, channel discord.GuildChannel, userID util.ID, roleIDs []util.ID) (apermissions discord.Permissions) { if userID == guild.OwnerID { apermissions = discordgo.PermissionAll return } - for _, role := range guild.Roles { + for _, role := range roles { if role.ID == guild.ID { apermissions |= role.Permissions break } } - for _, role := range guild.Roles { - for _, roleID := range roles { + for _, role := range roles { + for _, roleID := range roleIDs { if role.ID == roleID { apermissions |= role.Permissions break @@ -34,22 +38,26 @@ func memberPermissions(guild *discordgo.Guild, channel *discordgo.Channel, userI } // Apply @everyone overrides from the channel. - for _, overwrite := range channel.PermissionOverwrites { - if guild.ID == overwrite.ID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow - break + for _, overwrite := range channel.PermissionOverwrites() { + if roleOverwrite, ok := overwrite.(discord.RolePermissionOverwrite); ok { + if guild.ID == roleOverwrite.ID() { + apermissions &= ^roleOverwrite.Deny + apermissions |= roleOverwrite.Allow + break + } } } - var denies, allows int64 + var denies, allows discord.Permissions // Member overwrites can override role overrides, so do two passes - for _, overwrite := range channel.PermissionOverwrites { - for _, roleID := range roles { - if overwrite.Type == discordgo.PermissionOverwriteTypeRole && roleID == overwrite.ID { - denies |= overwrite.Deny - allows |= overwrite.Allow - break + for _, overwrite := range channel.PermissionOverwrites() { + if roleOverwrite, ok := overwrite.(discord.RolePermissionOverwrite); ok { + for _, roleID := range roleIDs { + if roleOverwrite.ID() == roleID { + denies |= roleOverwrite.Deny + allows |= roleOverwrite.Allow + break + } } } } @@ -57,11 +65,13 @@ func memberPermissions(guild *discordgo.Guild, channel *discordgo.Channel, userI apermissions &= ^denies apermissions |= allows - for _, overwrite := range channel.PermissionOverwrites { - if overwrite.Type == discordgo.PermissionOverwriteTypeMember && overwrite.ID == userID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow - break + for _, overwrite := range channel.PermissionOverwrites() { + if memberOverwrite, ok := overwrite.(discord.MemberPermissionOverwrite); ok { + if memberOverwrite.ID() == userID { + apermissions &= ^memberOverwrite.Deny + apermissions |= memberOverwrite.Allow + break + } } } diff --git a/embedg-server/api/api.go b/embedg-server/api/api.go index 2d1eee23d..ec36d0e9e 100644 --- a/embedg-server/api/api.go +++ b/embedg-server/api/api.go @@ -7,11 +7,12 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) -func Serve(stores *Stores) { +func Serve(embedg *embedg.EmbedGenerator, stores *Stores) { app := fiber.New(fiber.Config{ ErrorHandler: func(c *fiber.Ctx, err error) error { var e *wire.Error @@ -34,9 +35,9 @@ func Serve(stores *Stores) { EnableStackTrace: true, })) - managers := createManagers(stores, stores.Bot) + managers := createManagers(stores, embedg) - registerRoutes(app, stores, stores.Bot, managers) + registerRoutes(app, stores, embedg, managers) err := app.Listen(fmt.Sprintf("%s:%d", viper.GetString("api.host"), viper.GetInt("api.port"))) if err != nil { diff --git a/embedg-server/api/handlers/assistant/handler.go b/embedg-server/api/handlers/assistant/handler.go index c9724de64..b6d3e999d 100644 --- a/embedg-server/api/handlers/assistant/handler.go +++ b/embedg-server/api/handlers/assistant/handler.go @@ -9,6 +9,7 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/store" + "github.com/merlinfuchs/embed-generator/embedg-server/util" openai "github.com/sashabaranov/go-openai" "github.com/spf13/viper" ) @@ -30,7 +31,10 @@ func New(pg *postgres.PostgresStore, am *access.AccessManager, planStore store.P } func (h *AssistantHandler) HandleAssistantGenerateMessage(c *fiber.Ctx, req wire.AssistantGenerateMessageRequestWire) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err diff --git a/embedg-server/api/handlers/auth/handler.go b/embedg-server/api/handlers/auth/handler.go index 68ec81822..924fe2315 100644 --- a/embedg-server/api/handlers/auth/handler.go +++ b/embedg-server/api/handlers/auth/handler.go @@ -11,7 +11,6 @@ import ( "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" "github.com/ravener/discord-oauth2" @@ -23,12 +22,11 @@ import ( type AuthHandler struct { pg *postgres.PostgresStore - bot *bot.Bot sessionManager *session.SessionManager oauth2Config *oauth2.Config } -func New(pg *postgres.PostgresStore, bot *bot.Bot, sessionManager *session.SessionManager) *AuthHandler { +func New(pg *postgres.PostgresStore, sessionManager *session.SessionManager) *AuthHandler { conf := &oauth2.Config{ RedirectURL: fmt.Sprintf("%s/auth/callback", viper.GetString("api.public_url")), ClientID: viper.GetString("discord.client_id"), @@ -39,7 +37,6 @@ func New(pg *postgres.PostgresStore, bot *bot.Bot, sessionManager *session.Sessi return &AuthHandler{ pg: pg, - bot: bot, sessionManager: sessionManager, oauth2Config: conf, } diff --git a/embedg-server/api/handlers/custom_bots/handler.go b/embedg-server/api/handlers/custom_bots/handler.go index 6ba32258f..d198eda07 100644 --- a/embedg-server/api/handlers/custom_bots/handler.go +++ b/embedg-server/api/handlers/custom_bots/handler.go @@ -7,13 +7,14 @@ import ( "slices" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/rest" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" "github.com/merlinfuchs/embed-generator/embedg-server/store" @@ -25,16 +26,16 @@ import ( type CustomBotsHandler struct { pg *postgres.PostgresStore - bot *bot.Bot + caches cache.Caches am *access.AccessManager planStore store.PlanStore actionParser *parser.ActionParser } -func New(pg *postgres.PostgresStore, bot *bot.Bot, am *access.AccessManager, planStore store.PlanStore, actionParser *parser.ActionParser) *CustomBotsHandler { +func New(pg *postgres.PostgresStore, caches cache.Caches, am *access.AccessManager, planStore store.PlanStore, actionParser *parser.ActionParser) *CustomBotsHandler { return &CustomBotsHandler{ pg: pg, - bot: bot, + caches: caches, am: am, planStore: planStore, actionParser: actionParser, @@ -56,26 +57,26 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust return helpers.Forbidden("insufficient_plan", "This feature is not available on your plan!") } - session, err := discordgo.New("Bot " + req.Token) + client := rest.New(rest.NewClient(req.Token)) if err != nil { return err } - app, err := session.Application("@me") + app, err := client.GetCurrentApplication() if err != nil { - if derr, ok := err.(*discordgo.RESTError); ok && derr.Response.StatusCode == 401 { + if util.IsDiscordRestStatusCode(err, 401) { return fmt.Errorf("Invalid bot token, please check it again.") } return err } - user, err := session.User("@me") + user, err := client.GetCurrentUser("") if err != nil { return err } isMember := true - member, err := session.GuildMember(guildID, user.ID) + member, err := client.GetMember(util.ToID(guildID), user.ID) if err != nil { if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeMissingAccess, discordgo.ErrCodeUnknownGuild) { isMember = false @@ -84,15 +85,12 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust } } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - return err - } + roles := h.caches.Roles(util.ToID(guildID)) hasPermissions := false if isMember { - for _, role := range guild.Roles { - if slices.Contains(member.Roles, role.ID) || role.ID == guildID { + for role := range roles { + if slices.Contains(member.RoleIDs, role.ID) || role.ID == util.ToID(guildID) { if role.Permissions&discordgo.PermissionManageWebhooks != 0 { hasPermissions = true break @@ -101,14 +99,19 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust } } + var userAvatar sql.NullString + if user.Avatar != nil { + userAvatar = sql.NullString{String: *user.Avatar, Valid: *user.Avatar != ""} + } + customBot, err := h.pg.Q.UpsertCustomBot(c.Context(), pgmodel.UpsertCustomBotParams{ ID: util.UniqueID(), GuildID: guildID, - ApplicationID: app.ID, - UserID: user.ID, + ApplicationID: app.ID.String(), + UserID: user.ID.String(), UserName: user.Username, UserDiscriminator: user.Discriminator, - UserAvatar: sql.NullString{String: user.Avatar, Valid: user.Avatar != ""}, + UserAvatar: userAvatar, Token: req.Token, PublicKey: app.VerifyKey, CreatedAt: time.Now().UTC(), @@ -216,53 +219,48 @@ func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { return err } - session, err := discordgo.New("Bot " + customBot.Token) + client := rest.New(rest.NewClient(customBot.Token)) if err != nil { return err } isMember := true tokenValid := true - member, err := session.GuildMember(guildID, customBot.UserID) + member, err := client.GetMember(util.ToID(guildID), util.ToID(customBot.UserID)) if err != nil { - if derr, ok := err.(*discordgo.RESTError); ok { - if derr.Response.StatusCode == 401 { - tokenValid = false - isMember = false - } else if derr.Response.StatusCode == 403 || derr.Response.StatusCode == 404 { - isMember = false - } else { - return err - } + if util.IsDiscordRestStatusCode(err, 401) { + tokenValid = false + isMember = false + } else if util.IsDiscordRestStatusCode(err, 403) || util.IsDiscordRestStatusCode(err, 404) { + isMember = false } else { return err } } + var userAvatar sql.NullString + if member.User.Avatar != nil { + userAvatar = sql.NullString{String: *member.User.Avatar, Valid: *member.User.Avatar != ""} + } + if member != nil { customBot, err = h.pg.Q.UpdateCustomBotUser(c.Context(), pgmodel.UpdateCustomBotUserParams{ GuildID: guildID, UserName: member.User.Username, UserDiscriminator: member.User.Discriminator, - UserAvatar: sql.NullString{ - String: member.User.Avatar, - Valid: member.User.Avatar != "", - }, + UserAvatar: userAvatar, }) if err != nil { log.Error().Err(err).Msg("Failed to update custom bot user info") } } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - return err - } + roles := h.caches.Roles(util.ToID(guildID)) hasPermissions := false if member != nil { - for _, role := range guild.Roles { - if slices.Contains(member.Roles, role.ID) || role.ID == guildID { + for role := range roles { + if slices.Contains(member.RoleIDs, role.ID) || role.ID == util.ToID(guildID) { if role.Permissions&discordgo.PermissionManageWebhooks != 0 { hasPermissions = true break diff --git a/embedg-server/api/handlers/guilds/handler.go b/embedg-server/api/handlers/guilds/handler.go index 2ca593ad2..b9274f0d3 100644 --- a/embedg-server/api/handlers/guilds/handler.go +++ b/embedg-server/api/handlers/guilds/handler.go @@ -4,13 +4,12 @@ import ( "database/sql" "fmt" + "github.com/disgoorg/disgo/cache" "github.com/gofiber/fiber/v2" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/store" "github.com/merlinfuchs/embed-generator/embedg-server/util" @@ -20,15 +19,15 @@ import ( type GuildsHanlder struct { pg *postgres.PostgresStore - bot *bot.Bot + caches cache.Caches am *access.AccessManager planStore store.PlanStore } -func New(pg *postgres.PostgresStore, bot *bot.Bot, am *access.AccessManager, planStore store.PlanStore) *GuildsHanlder { +func New(pg *postgres.PostgresStore, caches cache.Caches, am *access.AccessManager, planStore store.PlanStore) *GuildsHanlder { return &GuildsHanlder{ pg: pg, - bot: bot, + caches: caches, am: am, planStore: planStore, } @@ -39,15 +38,12 @@ func (h *GuildsHanlder) HandleListGuilds(c *fiber.Ctx) error { res := make([]wire.GuildWire, 0, len(session.GuildIDs)) for _, guildID := range session.GuildIDs { - guild, err := h.bot.State.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - continue - } - return err + guild, ok := h.caches.Guild(guildID) + if !ok { + continue } - access, err := h.am.GetGuildAccessForUser(session.UserID, guild.ID) + access, err := h.am.GetGuildAccessForUser(session.UserID, guildID) if err != nil { log.Error().Err(err).Msg("Failed to check guild access") return err @@ -56,7 +52,7 @@ func (h *GuildsHanlder) HandleListGuilds(c *fiber.Ctx) error { res = append(res, wire.GuildWire{ ID: guild.ID, Name: guild.Name, - Icon: null.NewString(guild.Icon, guild.Icon != ""), + Icon: null.StringFromPtr(guild.Icon), HasChannelWithUserAccess: access.HasChannelWithUserAccess, HasChannelWithBotAccess: access.HasChannelWithBotAccess, }) @@ -70,21 +66,21 @@ func (h *GuildsHanlder) HandleListGuilds(c *fiber.Ctx) error { func (h *GuildsHanlder) HandleGetGuild(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) - guildID := c.Params("guildID") + guildID, err := util.ParseID(c.Params("guildID")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return helpers.NotFound("unknown_guild", "The guild does not exist.") - } - return err + guild, ok := h.caches.Guild(guildID) + if !ok { + return helpers.NotFound("unknown_guild", "The guild does not exist.") } - access, err := h.am.GetGuildAccessForUser(session.UserID, guild.ID) + access, err := h.am.GetGuildAccessForUser(session.UserID, guildID) if err != nil { log.Error().Err(err).Msg("Failed to check guild access") return err @@ -93,7 +89,7 @@ func (h *GuildsHanlder) HandleGetGuild(c *fiber.Ctx) error { res := wire.GuildWire{ ID: guild.ID, Name: guild.Name, - Icon: null.NewString(guild.Icon, guild.Icon != ""), + Icon: null.StringFromPtr(guild.Icon), HasChannelWithUserAccess: access.HasChannelWithUserAccess, HasChannelWithBotAccess: access.HasChannelWithBotAccess, } @@ -106,35 +102,33 @@ func (h *GuildsHanlder) HandleGetGuild(c *fiber.Ctx) error { func (h *GuildsHanlder) HandleListGuildChannels(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) - guildID := c.Params("guildID") + guildID, err := util.ParseID(c.Params("guildID")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return helpers.NotFound("unknown_guild", "The guild does not exist.") - } - return err - } + // TODO: validate that this contains both channels and threads + channels := h.caches.ChannelsForGuild(guildID) - res := make([]wire.GuildChannelWire, 0, len(guild.Channels)+len(guild.Threads)) + res := make([]wire.GuildChannelWire, 0) - for _, channel := range guild.Channels { - access, err := h.am.GetChannelAccessForUser(session.UserID, channel.ID) + for channel := range channels { + access, err := h.am.GetChannelAccessForUser(session.UserID, channel.ID()) if err != nil { log.Error().Err(err).Msg("Failed to check channel access") return err } res = append(res, wire.GuildChannelWire{ - ID: channel.ID, - Name: channel.Name, - Position: channel.Position, - ParentID: null.NewString(channel.ParentID, channel.ParentID != ""), - Type: int(channel.Type), + ID: channel.ID(), + Name: channel.Name(), + Position: channel.Position(), + ParentID: util.NullIDFromPtr(channel.ParentID()), + Type: int(channel.Type()), UserAccess: access.UserAccess(), UserPermissions: fmt.Sprintf("%d", access.UserPermissions), BotAccess: access.BotAccess(), @@ -142,7 +136,7 @@ func (h *GuildsHanlder) HandleListGuildChannels(c *fiber.Ctx) error { }) } - for _, channel := range guild.Threads { + /* for _, channel := range guild.Threads { access, err := h.am.GetChannelAccessForUser(session.UserID, channel.ID) if err != nil { log.Error().Err(err).Msg("Failed to check channel access") @@ -160,7 +154,7 @@ func (h *GuildsHanlder) HandleListGuildChannels(c *fiber.Ctx) error { BotAccess: access.BotAccess(), BotPermissions: fmt.Sprintf("%d", access.BotPermissions), }) - } + } */ return c.JSON(wire.ListChannelsResponseWire{ Success: true, @@ -169,21 +163,19 @@ func (h *GuildsHanlder) HandleListGuildChannels(c *fiber.Ctx) error { } func (h *GuildsHanlder) HandleListGuildRoles(c *fiber.Ctx) error { - guildID := c.Params("guildID") - if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { - return err + guildID, err := util.ParseID(c.Params("guildID")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return helpers.NotFound("unknown_guild", "The guild does not exist.") - } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - res := make([]wire.GuildRoleWire, 0, len(guild.Roles)) - for _, role := range guild.Roles { + roles := h.caches.Roles(guildID) + + res := make([]wire.GuildRoleWire, 0) + for role := range roles { res = append(res, wire.GuildRoleWire{ ID: role.ID, Name: role.Name, @@ -200,21 +192,19 @@ func (h *GuildsHanlder) HandleListGuildRoles(c *fiber.Ctx) error { } func (h *GuildsHanlder) HandleListGuildEmojis(c *fiber.Ctx) error { - guildID := c.Params("guildID") - if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { - return err + guildID, err := util.ParseID(c.Params("guildID")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return helpers.NotFound("unknown_guild", "The guild does not exist.") - } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - res := make([]wire.GuildEmojiWire, 0, len(guild.Emojis)) - for _, emoji := range guild.Emojis { + emojis := h.caches.Emojis(guildID) + + res := make([]wire.GuildEmojiWire, 0) + for emoji := range emojis { res = append(res, wire.GuildEmojiWire{ ID: emoji.ID, Name: emoji.Name, @@ -231,25 +221,28 @@ func (h *GuildsHanlder) HandleListGuildEmojis(c *fiber.Ctx) error { } func (h *GuildsHanlder) HandleListGuildStickers(c *fiber.Ctx) error { - guildID := c.Params("guildID") + guildID, err := util.ParseID(c.Params("guildID")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - guild, err := h.bot.State.Guild(guildID) - if err != nil { - if err == discordgo.ErrStateNotFound { - return helpers.NotFound("unknown_guild", "The guild does not exist.") + stickers := h.caches.Stickers(guildID) + + res := make([]wire.GuildStickerWire, 0) + for sticker := range stickers { + var available bool + if sticker.Available != nil { + available = *sticker.Available } - return err - } - res := make([]wire.GuildStickerWire, 0, len(guild.Stickers)) - for _, sticker := range guild.Stickers { res = append(res, wire.GuildStickerWire{ ID: sticker.ID, Name: sticker.Name, - Available: sticker.Available, + Available: available, Description: sticker.Description, }) } @@ -261,14 +254,18 @@ func (h *GuildsHanlder) HandleListGuildStickers(c *fiber.Ctx) error { } func (h *GuildsHanlder) HandleGetGuildBranding(c *fiber.Ctx) error { - guildID := c.Params("guildID") + guildID, err := util.ParseID(c.Params("guildID")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } res := wire.GuildBrandingWire{} - customBot, err := h.pg.Q.GetCustomBotByGuildID(c.Context(), guildID) + customBot, err := h.pg.Q.GetCustomBotByGuildID(c.Context(), guildID.String()) if err != nil { if err != sql.ErrNoRows { return err diff --git a/embedg-server/api/handlers/health/handler.go b/embedg-server/api/handlers/health/handler.go index eeb152e83..d0784cccc 100644 --- a/embedg-server/api/handlers/health/handler.go +++ b/embedg-server/api/handlers/health/handler.go @@ -2,21 +2,22 @@ package health import ( "net/http" - "strconv" "time" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/gateway" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) type HealthHandler struct { - bot *bot.Bot + client *bot.Client } -func New(bot *bot.Bot) *HealthHandler { +func New(client *bot.Client) *HealthHandler { return &HealthHandler{ - bot: bot, + client: client, } } @@ -26,57 +27,63 @@ func (h *HealthHandler) HandleHealth(c *fiber.Ctx) error { func (h *HealthHandler) HandleHealthShardList(c *fiber.Ctx) error { rawGuildID := c.Query("guild_id") - var guildID uint64 + var guildID util.ID if rawGuildID != "" { var err error - guildID, err = strconv.ParseUint(rawGuildID, 10, 64) + guildID, err = util.ParseID(rawGuildID) if err != nil { return c.SendStatus(http.StatusBadRequest) } } - suspiciousOnly := c.Query("suspicious") == "true" + shardListWire := make([]wire.ShardWire, 0) + var shardCount int - shards := h.bot.ShardManager.ShardList() + if guildID != 0 { + shard := h.client.ShardManager.ShardByGuildID(guildID) - shardListWire := make([]wire.ShardWire, 0, len(shards)) - for _, shard := range shards { - if guildID != 0 && guildID%uint64(h.bot.ShardManager.ShardCount) != uint64(shard.ID) { - continue - } + shardListWire = append(shardListWire, wire.ShardWire{ + ID: shard.ShardID(), + Status: shard.Status().String(), + Latency: shard.Latency().Milliseconds(), + Suspicious: isShardSuspicious(shard), + }) + } else { + suspiciousOnly := c.Query("suspicious") == "true" - if shard.Session == nil { - shardListWire = append(shardListWire, wire.ShardWire{ - ID: shard.ID, - Suspicious: true, - }) - } else { - var suspicious bool - if time.Since(shard.Session.LastHeartbeatAck) > 5*60*time.Second { - suspicious = true - } - if time.Since(shard.Session.LastHeartbeatSent) > 5*time.Second && shard.Session.LastHeartbeatAck.Before(shard.Session.LastHeartbeatSent) { - suspicious = true - } + shards := h.client.ShardManager.Shards() + + for shard := range shards { + shardCount += 1 + suspicious := isShardSuspicious(shard) if suspiciousOnly && !suspicious { continue } shardListWire = append(shardListWire, wire.ShardWire{ - ID: shard.ID, - HasSession: true, - LastHeartbeatAck: shard.Session.LastHeartbeatAck, - LastHeartbeatSent: shard.Session.LastHeartbeatSent, - ShouldReconnectOnError: shard.Session.ShouldReconnectOnError, - ShouldRetryOnRateLimit: shard.Session.ShouldRetryOnRateLimit, - Suspicious: suspicious, + ID: shard.ShardID(), + Status: shard.Status().String(), + Latency: shard.Latency().Milliseconds(), + Suspicious: suspicious, }) } } return c.JSON(wire.ShardListWire{ - ShardCount: h.bot.ShardManager.ShardCount, + ShardCount: shardCount, Shards: shardListWire, }) } + +func isShardSuspicious(shard gateway.Gateway) bool { + if shard.Status() != gateway.StatusReady { + return true + } + + if shard.Latency() > 10*time.Second { + return true + } + + return false +} diff --git a/embedg-server/api/handlers/images/handler.go b/embedg-server/api/handlers/images/handler.go index d414086fc..8c79c7c8b 100644 --- a/embedg-server/api/handlers/images/handler.go +++ b/embedg-server/api/handlers/images/handler.go @@ -41,8 +41,17 @@ func New(pg *postgres.PostgresStore, am *access.AccessManager, planStore store.P func (h *ImagesHandler) HandleUploadImage(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") - if guildID != "" { + + rawGuildID := c.Query("guild_id") + var guildID util.ID + + if rawGuildID != "" { + var err error + guildID, err = util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -96,10 +105,10 @@ func (h *ImagesHandler) HandleUploadImage(c *fiber.Ctx) error { image, err := h.pg.Q.InsertImage(c.Context(), pgmodel.InsertImageParams{ ID: util.UniqueID(), - UserID: session.UserID, + UserID: session.UserID.String(), GuildID: sql.NullString{ - String: guildID, - Valid: guildID != "", + String: guildID.String(), + Valid: guildID != 0, }, FileName: file.Filename, FileHash: fileHash, diff --git a/embedg-server/api/handlers/interaction/handler.go b/embedg-server/api/handlers/interaction/handler.go index 0673c97a2..8b798dd8c 100644 --- a/embedg-server/api/handlers/interaction/handler.go +++ b/embedg-server/api/handlers/interaction/handler.go @@ -7,22 +7,27 @@ import ( "strings" "time" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/rest" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) type InteractionHandler struct { - bot *bot.Bot + caches cache.Caches + rest rest.Rest + actionHandler *handler.ActionHandler } -func New(bot *bot.Bot) *InteractionHandler { +func New(caches cache.Caches, rest rest.Rest, actionHandler *handler.ActionHandler) *InteractionHandler { return &InteractionHandler{ - bot: bot, + caches: caches, + rest: rest, + actionHandler: actionHandler, } } @@ -64,7 +69,7 @@ func (h *InteractionHandler) HandleBotInteraction(c *fiber.Ctx) error { go func() { if customAction { - err := h.bot.ActionHandler.HandleActionInteraction(h.bot.Session, ri) + err := h.actionHandler.HandleActionInteraction(h.bot.Session, ri) if err != nil { log.Error().Err(err).Msg("Failed to handle action interaction") } diff --git a/embedg-server/api/handlers/premium/handler.go b/embedg-server/api/handlers/premium/handler.go index 7d7757d39..e17f47bd3 100644 --- a/embedg-server/api/handlers/premium/handler.go +++ b/embedg-server/api/handlers/premium/handler.go @@ -5,16 +5,17 @@ import ( "errors" "fmt" + "github.com/disgoorg/disgo/rest" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" "github.com/merlinfuchs/embed-generator/embedg-server/model" "github.com/merlinfuchs/embed-generator/embedg-server/store" + "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" "github.com/spf13/viper" "gopkg.in/guregu/null.v4" @@ -22,15 +23,15 @@ import ( type PremiumHandler struct { pg *postgres.PostgresStore - bot *bot.Bot + rest rest.Rest am *access.AccessManager planStore store.PlanStore } -func New(pg *postgres.PostgresStore, bot *bot.Bot, am *access.AccessManager, planStore store.PlanStore) *PremiumHandler { +func New(pg *postgres.PostgresStore, rest rest.Rest, am *access.AccessManager, planStore store.PlanStore) *PremiumHandler { return &PremiumHandler{ pg: pg, - bot: bot, + rest: rest, am: am, planStore: planStore, } @@ -38,12 +39,17 @@ func New(pg *postgres.PostgresStore, bot *bot.Bot, am *access.AccessManager, pla func (h *PremiumHandler) HandleGetFeatures(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") var features model.PlanFeatures var err error - if guildID != "" { + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -78,18 +84,29 @@ func (h *PremiumHandler) HandleGetFeatures(c *fiber.Ctx) error { func (h *PremiumHandler) HandleListEntitlements(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") var entitlements []pgmodel.Entitlement var err error - if guildID != "" { + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - entitlements, err = h.pg.Q.GetActiveEntitlementsForGuild(c.Context(), sql.NullString{String: guildID, Valid: true}) + entitlements, err = h.pg.Q.GetActiveEntitlementsForGuild( + c.Context(), + sql.NullString{String: guildID.String(), Valid: true}, + ) } else { - entitlements, err = h.pg.Q.GetActiveEntitlementsForUser(c.Context(), sql.NullString{String: session.UserID, Valid: true}) + entitlements, err = h.pg.Q.GetActiveEntitlementsForUser( + c.Context(), + sql.NullString{String: session.UserID.String(), Valid: true}, + ) } if err != nil { @@ -134,7 +151,7 @@ func (h *PremiumHandler) HandleConsumeEntitlement(c *fiber.Ctx, req wire.Consume entitlement, err := h.pg.Q.GetEntitlement(c.Context(), pgmodel.GetEntitlementParams{ ID: entitlementID, UserID: sql.NullString{ - String: session.UserID, + String: session.UserID.String(), Valid: true, }, }) @@ -161,12 +178,11 @@ func (h *PremiumHandler) HandleConsumeEntitlement(c *fiber.Ctx, req wire.Consume } if !entitlement.Consumed { - clientID := viper.GetString("discord.client_id") - url := fmt.Sprintf("https://discord.com/api/v10/applications/%s/entitlements/%s/consume", clientID, entitlement.ID) + clientID := util.ToID(viper.GetString("discord.client_id")) - _, err := h.bot.Session.Request("POST", url, nil) + err = h.rest.ConsumeEntitlement(clientID, util.ToID(entitlement.ID), rest.WithCtx(c.Context())) if err != nil { - return fmt.Errorf("failed to do request: %w", err) + return fmt.Errorf("failed to consume entitlement: %w", err) } } diff --git a/embedg-server/api/handlers/send_message/handler.go b/embedg-server/api/handlers/send_message/handler.go index 2c38b575d..50b085414 100644 --- a/embedg-server/api/handlers/send_message/handler.go +++ b/embedg-server/api/handlers/send_message/handler.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/rest" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" @@ -14,7 +16,6 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/store" "github.com/merlinfuchs/embed-generator/embedg-server/util" @@ -23,7 +24,8 @@ import ( ) type SendMessageHandler struct { - bot *bot.Bot + caches cache.Caches + rest rest.Rest pg *postgres.PostgresStore accessManager *access.AccessManager actionParser *parser.ActionParser @@ -31,14 +33,16 @@ type SendMessageHandler struct { } func New( - bot *bot.Bot, + caches cache.Caches, + rest rest.Rest, pg *postgres.PostgresStore, accessManager *access.AccessManager, actionParser *parser.ActionParser, planStore store.PlanStore, ) *SendMessageHandler { return &SendMessageHandler{ - bot: bot, + caches: caches, + rest: rest, pg: pg, accessManager: accessManager, actionParser: actionParser, diff --git a/embedg-server/api/handlers/shared_messages/handler.go b/embedg-server/api/handlers/shared_messages/handler.go index ebaf1a96e..e4fc8a276 100644 --- a/embedg-server/api/handlers/shared_messages/handler.go +++ b/embedg-server/api/handlers/shared_messages/handler.go @@ -8,7 +8,6 @@ import ( "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" "github.com/merlinfuchs/embed-generator/embedg-server/util" @@ -17,14 +16,12 @@ import ( ) type SharedMessageHandler struct { - bot *bot.Bot - pg *postgres.PostgresStore + pg *postgres.PostgresStore } -func New(bot *bot.Bot, pg *postgres.PostgresStore) *SharedMessageHandler { +func New(pg *postgres.PostgresStore) *SharedMessageHandler { return &SharedMessageHandler{ - bot: bot, - pg: pg, + pg: pg, } } diff --git a/embedg-server/api/handlers/users/handler.go b/embedg-server/api/handlers/users/handler.go index 523acb9e4..0f5094124 100644 --- a/embedg-server/api/handlers/users/handler.go +++ b/embedg-server/api/handlers/users/handler.go @@ -30,7 +30,7 @@ func (h *UsersHandler) HandleGetUser(c *fiber.Ctx) error { userID := c.Params("userID") if userID == "@me" { - userID = session.UserID + userID = session.UserID.String() } user, err := h.pg.Q.GetUser(c.Context(), userID) diff --git a/embedg-server/api/managers.go b/embedg-server/api/managers.go index 1e5165f58..9aa518b74 100644 --- a/embedg-server/api/managers.go +++ b/embedg-server/api/managers.go @@ -6,8 +6,8 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/api/premium" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/custom_bots" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/merlinfuchs/embed-generator/embedg-server/scheduled_messages" ) @@ -22,19 +22,25 @@ type managers struct { actionHandler *handler.ActionHandler } -func createManagers(stores *Stores, bot *bot.Bot) *managers { +func createManagers(stores *Stores, embedg *embedg.EmbedGenerator) *managers { sessionManager := session.New(stores.PG) - accessManager := access.New(bot.State, bot.Session, bot.Rest) - premiumManager := premium.New(stores.PG, bot) + accessManager := access.New(embedg.Caches(), embedg.Rest()) + premiumManager := premium.New(stores.PG, embedg.Rest()) - actionParser := parser.New(accessManager, stores.PG, bot.State) + actionParser := parser.New(accessManager, stores.PG, embedg.Caches()) actionHandler := handler.New(stores.PG, actionParser, premiumManager) customBots := custom_bots.NewCustomBotManager(stores.PG, actionHandler) - scheduledMessages := scheduled_messages.NewScheduledMessageManager(stores.PG, actionParser, bot, premiumManager) - - bot.ActionHandler = actionHandler - bot.ActionParser = actionParser + scheduledMessages := scheduled_messages.NewScheduledMessageManager( + stores.PG, + actionParser, + embedg.Caches(), + embedg.Rest(), + premiumManager, + ) + + embedg.SetActionHandler(actionHandler) + embedg.SetActionParser(actionParser) return &managers{ session: sessionManager, diff --git a/embedg-server/api/premium/manager.go b/embedg-server/api/premium/manager.go index 4ec744254..223065907 100644 --- a/embedg-server/api/premium/manager.go +++ b/embedg-server/api/premium/manager.go @@ -5,16 +5,17 @@ import ( "database/sql" "fmt" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" + "github.com/disgoorg/disgo/rest" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/model" + "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) type PremiumManager struct { pg *postgres.PostgresStore - bot *bot.Bot + rest rest.Rest plans []*model.Plan defaultPlanFeatures model.PlanFeatures } @@ -39,10 +40,10 @@ func (m *PremiumManager) GetPlanBySKUID(skuID string) *model.Plan { return nil } -func (m *PremiumManager) GetPlanFeaturesForGuild(ctx context.Context, guildID string) (model.PlanFeatures, error) { +func (m *PremiumManager) GetPlanFeaturesForGuild(ctx context.Context, guildID util.ID) (model.PlanFeatures, error) { planFeatures := m.defaultPlanFeatures - entitlements, err := m.pg.Q.GetActiveEntitlementsForGuild(ctx, sql.NullString{String: guildID, Valid: true}) + entitlements, err := m.pg.Q.GetActiveEntitlementsForGuild(ctx, sql.NullString{String: guildID.String(), Valid: true}) if err != nil { return planFeatures, fmt.Errorf("failed to retrieve entitlments for guild: %w", err) } @@ -57,10 +58,10 @@ func (m *PremiumManager) GetPlanFeaturesForGuild(ctx context.Context, guildID st return planFeatures, nil } -func (m *PremiumManager) GetPlanFeaturesForUser(ctx context.Context, userID string) (model.PlanFeatures, error) { +func (m *PremiumManager) GetPlanFeaturesForUser(ctx context.Context, userID util.ID) (model.PlanFeatures, error) { planFeatures := m.defaultPlanFeatures - entitlements, err := m.pg.Q.GetActiveEntitlementsForUser(ctx, sql.NullString{String: userID, Valid: true}) + entitlements, err := m.pg.Q.GetActiveEntitlementsForUser(ctx, sql.NullString{String: userID.String(), Valid: true}) if err != nil { return planFeatures, fmt.Errorf("failed to retrieve entitlments for user: %w", err) } @@ -75,7 +76,7 @@ func (m *PremiumManager) GetPlanFeaturesForUser(ctx context.Context, userID stri return planFeatures, nil } -func (m *PremiumManager) GetEntitledUserIDs(ctx context.Context) ([]string, error) { +func (m *PremiumManager) GetEntitledUserIDs(ctx context.Context) ([]util.ID, error) { entitlements, err := m.pg.Q.GetEntitlements(ctx) if err != nil { return nil, fmt.Errorf("failed to retrieve entitlments: %w", err) @@ -88,15 +89,20 @@ func (m *PremiumManager) GetEntitledUserIDs(ctx context.Context) ([]string, erro } } - res := make([]string, 0, len(userIDs)) + res := make([]util.ID, 0, len(userIDs)) for userID := range userIDs { + userID, err := util.ParseID(userID) + if err != nil { + return nil, fmt.Errorf("failed to parse user ID: %w", err) + } + res = append(res, userID) } return res, nil } -func New(pg *postgres.PostgresStore, bot *bot.Bot) *PremiumManager { +func New(pg *postgres.PostgresStore, rest rest.Rest) *PremiumManager { plans := make([]*model.Plan, 0) err := viper.UnmarshalKey("premium.plans", &plans) if err != nil { @@ -116,7 +122,7 @@ func New(pg *postgres.PostgresStore, bot *bot.Bot) *PremiumManager { manager := &PremiumManager{ pg: pg, - bot: bot, + rest: rest, plans: plans, defaultPlanFeatures: defaultPlanFeatures, } diff --git a/embedg-server/api/premium/roles.go b/embedg-server/api/premium/roles.go index bfb6be677..fe27bf259 100644 --- a/embedg-server/api/premium/roles.go +++ b/embedg-server/api/premium/roles.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/disgoorg/disgo/rest" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" @@ -12,35 +13,50 @@ import ( ) func (m *PremiumManager) lazyPremiumRolesTask() { - for { - time.Sleep(15 * time.Minute) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - if err := m.assignPremiumRoles(); err != nil { - log.Error().Err(err).Msg("Failed to assign premium roles") + for { + select { + case <-ctx.Done(): + return + case <-time.After(15 * time.Minute): + if err := m.assignPremiumRoles(ctx); err != nil { + log.Error().Err(err).Msg("Failed to assign premium roles") + } } } } -func (m *PremiumManager) assignPremiumRoles() error { - guildID := viper.GetString("premium.guild_id") - roleID := viper.GetString("premium.role_id") - if guildID == "" || roleID == "" { +func (m *PremiumManager) assignPremiumRoles(ctx context.Context) error { + rawGuildID := viper.GetString("premium.guild_id") + rawRoleID := viper.GetString("premium.role_id") + if rawGuildID == "" || rawRoleID == "" { return nil } - userIDs, err := m.GetEntitledUserIDs(context.Background()) + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return fmt.Errorf("Failed to parse guild ID: %w", err) + } + roleID, err := util.ParseID(rawRoleID) + if err != nil { + return fmt.Errorf("Failed to parse role ID: %w", err) + } + + userIDs, err := m.GetEntitledUserIDs(ctx) if err != nil { return fmt.Errorf("Failed to get entitled user IDs: %w", err) } for _, userID := range userIDs { - features, err := m.GetPlanFeaturesForUser(context.Background(), userID) + features, err := m.GetPlanFeaturesForUser(ctx, userID) if err != nil { log.Error().Err(err).Msg("Failed to get plan features for guild") continue } - member, err := m.bot.Session.GuildMember(guildID, userID) + member, err := m.rest.GetMember(guildID, userID, rest.WithCtx(ctx)) if err != nil { if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMember) { continue @@ -51,7 +67,7 @@ func (m *PremiumManager) assignPremiumRoles() error { } hasPremiumRole := false - for _, r := range member.Roles { + for _, r := range member.RoleIDs { if r == roleID { hasPremiumRole = true break @@ -59,12 +75,12 @@ func (m *PremiumManager) assignPremiumRoles() error { } if features.IsPremium && !hasPremiumRole { - err = m.bot.Session.GuildMemberRoleAdd(guildID, userID, roleID) + err = m.rest.AddMemberRole(guildID, userID, roleID, rest.WithCtx(ctx)) if err != nil { log.Error().Err(err).Msg("Failed to add premium role") } } else if !features.IsPremium && hasPremiumRole { - err = m.bot.Session.GuildMemberRoleRemove(guildID, userID, roleID) + err = m.rest.RemoveMemberRole(guildID, userID, roleID, rest.WithCtx(ctx)) if err != nil { log.Error().Err(err).Msg("Failed to remove premium role") } diff --git a/embedg-server/api/routes.go b/embedg-server/api/routes.go index 6f754f321..4208648e8 100644 --- a/embedg-server/api/routes.go +++ b/embedg-server/api/routes.go @@ -23,19 +23,19 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/api/handlers/users" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/api/session" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/merlinfuchs/embed-generator/embedg-server/util" embedgsite "github.com/merlinfuchs/embed-generator/embedg-site" "github.com/spf13/viper" ) -func registerRoutes(app *fiber.App, stores *Stores, bot *bot.Bot, managers *managers) { - healthHandler := health.New(bot) +func registerRoutes(app *fiber.App, stores *Stores, embedg *embedg.EmbedGenerator, managers *managers) { + healthHandler := health.New(embedg.Client()) healthGroup := app.Group("/api/health") healthGroup.Get("/", healthHandler.HandleHealth) app.Get("/api/health/shard-list", healthHandler.HandleHealthShardList) - authHandler := auth.New(stores.PG, bot, managers.session) + authHandler := auth.New(stores.PG, managers.session) app.Get("/api/auth/login", authHandler.HandleAuthRedirect) app.Get("/api/auth/callback", authHandler.HandleAuthCallback) app.Post("/api/auth/exchange", helpers.WithRequestBody(authHandler.HandleAuthExchange)) @@ -55,7 +55,7 @@ func registerRoutes(app *fiber.App, stores *Stores, bot *bot.Bot, managers *mana savedMessagesGroup.Put("/:messageID", helpers.WithRequestBodyValidated(savedMessagesHandler.HandleUpdateSavedMessage)) savedMessagesGroup.Delete("/:messageID", savedMessagesHandler.HandleDeleteSavedMessage) - sharedMessageHandler := shared_messages.New(bot, stores.PG) + sharedMessageHandler := shared_messages.New(stores.PG) sharedMessagesGroup := app.Group("/api/shared-messages") sharedMessagesGroup.Post("/", helpers.WithRequestBodyValidated(sharedMessageHandler.HandleCreateSharedMessage)) sharedMessagesGroup.Get("/:messageID", sharedMessageHandler.HandleGetSharedMessage) @@ -63,7 +63,7 @@ func registerRoutes(app *fiber.App, stores *Stores, bot *bot.Bot, managers *mana assistantHandler := assistant.New(stores.PG, managers.access, managers.premium) app.Post("/api/assistant/message", sessionMiddleware.SessionRequired(), helpers.WithRequestBody(assistantHandler.HandleAssistantGenerateMessage)) - guildsHanlder := guilds.New(stores.PG, bot, managers.access, managers.premium) + guildsHanlder := guilds.New(stores.PG, embedg.Caches(), managers.access, managers.premium) guildsGroup := app.Group("/api/guilds", sessionMiddleware.SessionRequired()) guildsGroup.Get("/", guildsHanlder.HandleListGuilds) guildsGroup.Get("/:guildID", guildsHanlder.HandleGetGuild) @@ -73,18 +73,31 @@ func registerRoutes(app *fiber.App, stores *Stores, bot *bot.Bot, managers *mana guildsGroup.Get("/:guildID/stickers", guildsHanlder.HandleListGuildStickers) guildsGroup.Get("/:guildID/branding", guildsHanlder.HandleGetGuildBranding) - sendMessageHandler := send_message.New(bot, stores.PG, managers.access, managers.actionParser, managers.premium) + sendMessageHandler := send_message.New( + embedg.Caches(), + embedg.Rest(), + stores.PG, + managers.access, + managers.actionParser, + managers.premium, + ) app.Post("/api/send-message/channel", sessionMiddleware.SessionRequired(), helpers.WithRequestBodyValidated(sendMessageHandler.HandleSendMessageToChannel)) app.Post("/api/send-message/webhook", helpers.WithRequestBodyValidated(sendMessageHandler.HandleSendMessageToWebhook)) app.Post("/api/restore-message/channel", sessionMiddleware.SessionRequired(), helpers.WithRequestBodyValidated(sendMessageHandler.HandleRestoreMessageFromChannel)) app.Post("/api/restore-message/webhook", helpers.WithRequestBodyValidated(sendMessageHandler.HandleRestoreMessageFromWebhook)) - premiumHandler := premium_handler.New(stores.PG, bot, managers.access, managers.premium) + premiumHandler := premium_handler.New(stores.PG, embedg.Rest(), managers.access, managers.premium) app.Get("/api/premium/features", sessionMiddleware.SessionRequired(), premiumHandler.HandleGetFeatures) app.Get("/api/premium/entitlements", sessionMiddleware.SessionRequired(), premiumHandler.HandleListEntitlements) app.Post("/api/premium/entitlements/:entitlementID/consume", sessionMiddleware.SessionRequired(), helpers.WithRequestBodyValidated(premiumHandler.HandleConsumeEntitlement)) - customBotHandler := custom_bots.New(stores.PG, bot, managers.access, managers.premium, managers.actionParser) + customBotHandler := custom_bots.New( + stores.PG, + embedg.Caches(), + managers.access, + managers.premium, + managers.actionParser, + ) app.Post("/api/custom-bot", sessionMiddleware.SessionRequired(), helpers.WithRequestBodyValidated(customBotHandler.HandleConfigureCustomBot)) app.Put("/api/custom-bot/presence", sessionMiddleware.SessionRequired(), helpers.WithRequestBodyValidated(customBotHandler.HandleUpdateCustomBotPresence)) app.Get("/api/custom-bot", sessionMiddleware.SessionRequired(), customBotHandler.HandleGetCustomBot) @@ -97,7 +110,7 @@ func registerRoutes(app *fiber.App, stores *Stores, bot *bot.Bot, managers *mana app.Post("/api/custom-bot/commands/deploy", sessionMiddleware.SessionRequired(), customBotHandler.HandleDeployCustomCommands) app.Post("/api/gateway/:customBotID", customBotHandler.HandleCustomBotInteraction) - interactionHandler := interaction.New(bot) + interactionHandler := interaction.New(embedg.Caches(), embedg.Rest(), managers.actionHandler) app.Post("/api/gateway", interactionHandler.HandleBotInteraction) imagesHandler := images.New(stores.PG, managers.access, managers.premium, stores.Blob) diff --git a/embedg-server/api/session/session.go b/embedg-server/api/session/session.go index 784d0d7fa..cecbc1c9e 100644 --- a/embedg-server/api/session/session.go +++ b/embedg-server/api/session/session.go @@ -12,13 +12,14 @@ import ( "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) type Session struct { - UserID string - GuildIDs []string + UserID util.ID + GuildIDs []util.ID AccessToken string CreatedAt time.Time ExpiresAt time.Time @@ -53,9 +54,14 @@ func (s *SessionManager) GetSession(c *fiber.Ctx) (*Session, error) { return nil, err } + guildIDs := make([]util.ID, len(model.GuildIds)) + for i, guildID := range model.GuildIds { + guildIDs[i] = util.ToID(guildID) + } + return &Session{ - UserID: model.UserID, - GuildIDs: model.GuildIds, + UserID: util.ToID(model.UserID), + GuildIDs: guildIDs, AccessToken: model.AccessToken, CreatedAt: model.CreatedAt, ExpiresAt: model.ExpiresAt, diff --git a/embedg-server/api/stores.go b/embedg-server/api/stores.go index 3e71987fb..78eaa05e7 100644 --- a/embedg-server/api/stores.go +++ b/embedg-server/api/stores.go @@ -1,7 +1,6 @@ package api import ( - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/s3" ) @@ -9,5 +8,4 @@ import ( type Stores struct { PG *postgres.PostgresStore Blob *s3.BlobStore - Bot *bot.Bot } diff --git a/embedg-server/api/wire/guild.go b/embedg-server/api/wire/guild.go index 999d601e7..59a3d654b 100644 --- a/embedg-server/api/wire/guild.go +++ b/embedg-server/api/wire/guild.go @@ -1,11 +1,12 @@ package wire import ( + "github.com/merlinfuchs/embed-generator/embedg-server/util" "gopkg.in/guregu/null.v4" ) type GuildWire struct { - ID string `json:"id"` + ID util.ID `json:"id"` Name string `json:"name"` Icon null.String `json:"icon"` @@ -18,10 +19,10 @@ type ListGuildsResponseWire APIResponse[[]GuildWire] type GetGuildResponseWire APIResponse[GuildWire] type GuildChannelWire struct { - ID string `json:"id"` + ID util.ID `json:"id"` Name string `json:"name"` Position int `json:"position"` - ParentID null.String `json:"parent_id"` + ParentID util.NullID `json:"parent_id"` Type int `json:"type"` UserAccess bool `json:"user_access"` @@ -33,31 +34,31 @@ type GuildChannelWire struct { type ListChannelsResponseWire APIResponse[[]GuildChannelWire] type GuildRoleWire struct { - ID string `json:"id"` - Name string `json:"name"` - Managed bool `json:"managed"` - Color int `json:"color"` - Position int `json:"position"` + ID util.ID `json:"id"` + Name string `json:"name"` + Managed bool `json:"managed"` + Color int `json:"color"` + Position int `json:"position"` } type ListRolesResponseWire APIResponse[[]GuildRoleWire] type GuildEmojiWire struct { - ID string `json:"id"` - Name string `json:"name"` - Available bool `json:"available"` - Animated bool `json:"animated"` - Managed bool `json:"managed"` + ID util.ID `json:"id"` + Name string `json:"name"` + Available bool `json:"available"` + Animated bool `json:"animated"` + Managed bool `json:"managed"` } type ListEmojisResponseWire APIResponse[[]GuildEmojiWire] type GuildStickerWire struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Available bool `json:"available"` - FormantType int `json:"formant_type"` + ID util.ID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Available bool `json:"available"` + FormantType int `json:"formant_type"` } type ListStickersResponseWire APIResponse[[]GuildStickerWire] diff --git a/embedg-server/api/wire/health.go b/embedg-server/api/wire/health.go index ce8e474eb..469cdd3af 100644 --- a/embedg-server/api/wire/health.go +++ b/embedg-server/api/wire/health.go @@ -1,18 +1,13 @@ package wire -import "time" - type ShardListWire struct { ShardCount int `json:"shard_count"` Shards []ShardWire `json:"shards"` } type ShardWire struct { - ID int `json:"id"` - HasSession bool `json:"has_session"` - LastHeartbeatAck time.Time `json:"last_heartbeat_ack"` - LastHeartbeatSent time.Time `json:"last_heartbeat_sent"` - ShouldReconnectOnError bool `json:"should_reconnect_on_error"` - ShouldRetryOnRateLimit bool `json:"should_retry_on_rate_limit"` - Suspicious bool `json:"suspicious"` + ID int `json:"id"` + Status string `json:"status"` + Latency int64 `json:"latency"` + Suspicious bool `json:"suspicious"` } diff --git a/embedg-server/embedg/bot.go b/embedg-server/embedg/bot.go index 3a050b368..51feeb584 100644 --- a/embedg-server/embedg/bot.go +++ b/embedg-server/embedg/bot.go @@ -39,12 +39,9 @@ type EmbedGenerator struct { } func NewEmbedGenerator( - ctx context.Context, cfg EmbedGeneratorConfig, pg *postgres.PostgresStore, - actionHandler *actionshandler.ActionHandler, - actionParser *actionsparser.ActionParser, ) (*EmbedGenerator, error) { clientRouter := handler.New() @@ -91,15 +88,12 @@ func NewEmbedGenerator( } embedg := &EmbedGenerator{ - ctx: ctx, cfg: cfg, client: client, clientRouter: clientRouter, - pg: pg, - actionHandler: actionHandler, - actionParser: actionParser, + pg: pg, } embedg.registerHandlers() @@ -108,6 +102,7 @@ func NewEmbedGenerator( } func (g *EmbedGenerator) Start(ctx context.Context) error { + g.ctx = ctx if err := g.client.OpenShardManager(ctx); err != nil { return err } @@ -135,6 +130,14 @@ func (g *EmbedGenerator) ActionHandler() *actionshandler.ActionHandler { return g.actionHandler } +func (g *EmbedGenerator) SetActionHandler(actionHandler *actionshandler.ActionHandler) { + g.actionHandler = actionHandler +} + func (g *EmbedGenerator) ActionParser() *actionsparser.ActionParser { return g.actionParser } + +func (g *EmbedGenerator) SetActionParser(actionParser *actionsparser.ActionParser) { + g.actionParser = actionParser +} diff --git a/embedg-server/embedg/commands.go b/embedg-server/embedg/commands.go index e0b6916ca..b6299716b 100644 --- a/embedg-server/embedg/commands.go +++ b/embedg-server/embedg/commands.go @@ -233,7 +233,7 @@ var commands = []discord.ApplicationCommandCreate{ } func (g *EmbedGenerator) SyncCommands() error { - if err := handler.SyncCommands(g.Client(), commands, []snowflake.ID{}); err != nil { + if err := handler.SyncCommands(g.Client(), commands, []util.ID{}); err != nil { return fmt.Errorf("error while syncing commands: %w", err) } return nil @@ -468,7 +468,7 @@ func (g *EmbedGenerator) handleMessageRestoreCommand(e *handler.CommandEvent) er messageIDOrURL := e.SlashCommandInteractionData().String("message_id_or_url") channelID := e.Channel().ID() - var messageID snowflake.ID + var messageID util.ID match := messageURLRegex.FindStringSubmatch(messageIDOrURL) if match != nil { diff --git a/embedg-server/embedg/rest/rest.go b/embedg-server/embedg/rest/rest.go index e02e11ca3..ce9c2c270 100644 --- a/embedg-server/embedg/rest/rest.go +++ b/embedg-server/embedg/rest/rest.go @@ -1,15 +1,63 @@ package rest -import "github.com/disgoorg/disgo/rest" +import ( + "fmt" + "time" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/jellydator/ttlcache/v3" + "github.com/merlinfuchs/embed-generator/embedg-server/util" +) type RestClient struct { rest.Rest + + memberCache *ttlcache.Cache[string, *discord.Member] } func NewRestClient(token string, opts ...rest.ConfigOpt) *RestClient { + memberCache := ttlcache.New( + ttlcache.WithTTL[string, *discord.Member](5 * time.Minute), + ) + go memberCache.Start() + return &RestClient{ - Rest: rest.New(rest.NewClient(token, opts...)), + Rest: rest.New(rest.NewClient(token, opts...)), + memberCache: memberCache, } } -// TODO: Add caching for some endpoints +func (c *RestClient) GetMember(guildID util.ID, userID util.ID, opts ...rest.RequestOpt) (*discord.Member, error) { + var resErr error + + key := memberCacheKey(guildID, userID) + + loader := func(cache *ttlcache.Cache[string, *discord.Member], key string) *ttlcache.Item[string, *discord.Member] { + member, err := c.Rest.GetMember(guildID, userID, opts...) + if err != nil { + resErr = err + return nil + } + + return cache.Set(key, member, 0) + } + + member := c.memberCache.Get( + key, + ttlcache.WithLoader(ttlcache.LoaderFunc[string, *discord.Member](loader)), + ) + if resErr != nil { + return nil, resErr + } + + if member == nil { + return nil, resErr + } + + return member.Value(), nil +} + +func memberCacheKey(guildID util.ID, userID util.ID) string { + return fmt.Sprintf("%s:%s", guildID.String(), userID.String()) +} diff --git a/embedg-server/entry/server/cmd.go b/embedg-server/entry/server/cmd.go index 4f9ee55c7..31e7bc50b 100644 --- a/embedg-server/entry/server/cmd.go +++ b/embedg-server/entry/server/cmd.go @@ -1,8 +1,10 @@ package server import ( + "context" + "github.com/merlinfuchs/embed-generator/embedg-server/api" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -23,16 +25,23 @@ func Setup() *cobra.Command { func startServer() { stores := createStores() - bot, err := bot.New(viper.GetString("discord.token"), stores.pg) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + embedg, err := embedg.NewEmbedGenerator( + embedg.EmbedGeneratorConfig{ + DiscordToken: viper.GetString("discord.token"), + }, + stores.pg, + ) if err != nil { - log.Fatal().Err(err).Msg("Failed to initialize bot") + log.Fatal().Err(err).Msg("Failed to initialize embedg") } - go bot.Start() + go embedg.Start(ctx) - api.Serve(&api.Stores{ + api.Serve(embedg, &api.Stores{ PG: stores.pg, Blob: stores.blob, - Bot: bot, }) } diff --git a/embedg-server/scheduled_messages/manager.go b/embedg-server/scheduled_messages/manager.go index 39dfb4001..9d566d6bb 100644 --- a/embedg-server/scheduled_messages/manager.go +++ b/embedg-server/scheduled_messages/manager.go @@ -7,21 +7,24 @@ import ( "fmt" "time" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/rest" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/actions/template" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" - "github.com/merlinfuchs/embed-generator/embedg-server/bot" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" "github.com/merlinfuchs/embed-generator/embedg-server/store" + "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" ) type ScheduledMessageManager struct { pg *postgres.PostgresStore - bot *bot.Bot + caches cache.Caches + rest rest.Rest actionParser *parser.ActionParser planStore store.PlanStore } @@ -29,12 +32,14 @@ type ScheduledMessageManager struct { func NewScheduledMessageManager( pg *postgres.PostgresStore, actionParser *parser.ActionParser, - bot *bot.Bot, + caches cache.Caches, + rest rest.Rest, planStore store.PlanStore, ) *ScheduledMessageManager { m := &ScheduledMessageManager{ pg: pg, - bot: bot, + caches: caches, + rest: rest, actionParser: actionParser, planStore: planStore, } @@ -114,15 +119,15 @@ func (m *ScheduledMessageManager) SendScheduledMessage(ctx context.Context, sche return fmt.Errorf("Failed to get saved message from scheduled message: %w", err) } - features, err := m.planStore.GetPlanFeaturesForGuild(ctx, scheduledMessage.GuildID) + features, err := m.planStore.GetPlanFeaturesForGuild(ctx, util.ToID(scheduledMessage.GuildID)) if err != nil { return fmt.Errorf("could not get plan features: %w", err) } templates := template.NewContext( "SCHEDULED_MESSAGE", features.MaxTemplateOps, - template.NewGuildProvider(m.bot.State, scheduledMessage.GuildID, nil), - template.NewChannelProvider(m.bot.State, scheduledMessage.ChannelID, nil), + template.NewGuildProvider(m.caches, scheduledMessage.GuildID, nil), + template.NewChannelProvider(m.caches, scheduledMessage.ChannelID, nil), template.NewKVProvider(scheduledMessage.GuildID, m.pg, features.MaxKVKeys), ) diff --git a/embedg-server/store/plan.go b/embedg-server/store/plan.go index 53886a615..dd0e6861e 100644 --- a/embedg-server/store/plan.go +++ b/embedg-server/store/plan.go @@ -4,11 +4,12 @@ import ( "context" "github.com/merlinfuchs/embed-generator/embedg-server/model" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) type PlanStore interface { GetPlanByID(id string) *model.Plan GetPlanBySKUID(skuID string) *model.Plan - GetPlanFeaturesForGuild(ctx context.Context, guildID string) (model.PlanFeatures, error) - GetPlanFeaturesForUser(ctx context.Context, userID string) (model.PlanFeatures, error) + GetPlanFeaturesForGuild(ctx context.Context, guildID util.ID) (model.PlanFeatures, error) + GetPlanFeaturesForUser(ctx context.Context, userID util.ID) (model.PlanFeatures, error) } diff --git a/embedg-server/util/id.go b/embedg-server/util/id.go index 59e7d12d0..8b64e0133 100644 --- a/embedg-server/util/id.go +++ b/embedg-server/util/id.go @@ -1,8 +1,58 @@ package util -import gonanoid "github.com/matoous/go-nanoid" +import ( + "encoding/json" + + "github.com/disgoorg/snowflake/v2" + gonanoid "github.com/matoous/go-nanoid" +) func UniqueID() string { id, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyzAPCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", 8) return id } + +type ID = snowflake.ID + +type NullID struct { + Valid bool + ID ID +} + +func (n NullID) String() string { + if !n.Valid { + return "null" + } + return n.ID.String() +} + +func (n NullID) MarshalJSON() ([]byte, error) { + if !n.Valid { + return []byte("null"), nil + } + return json.Marshal(n.ID) +} + +func (n *NullID) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.Valid = false + return nil + } + return json.Unmarshal(data, &n.ID) +} + +func ToID(val string) snowflake.ID { + id, _ := snowflake.Parse(val) + return id +} + +func ParseID(val string) (snowflake.ID, error) { + return snowflake.Parse(val) +} + +func NullIDFromPtr(ptr *snowflake.ID) NullID { + if ptr == nil { + return NullID{Valid: false} + } + return NullID{ID: *ptr, Valid: true} +} diff --git a/embedg-server/util/util.go b/embedg-server/util/util.go index dc87a539c..902273953 100644 --- a/embedg-server/util/util.go +++ b/embedg-server/util/util.go @@ -61,6 +61,23 @@ func IsDiscordRestErrorCode(err error, codes ...int) bool { return false } +func IsDiscordRestStatusCode(err error, statusCodes ...int) bool { + var httpErr *rest.Error + if errors.As(err, &httpErr) { + if httpErr.Response == nil { + return false + } + + for _, statusCode := range statusCodes { + if httpErr.Response.StatusCode == statusCode { + return true + } + } + } + + return false +} + func DiscordAvatarURL(id string, discriminator string, avatar string) string { if avatar == "" { parsedDiscriminator, _ := strconv.Atoi(discriminator) From 648d93d9d2f7baed0b84fbee8ccfe12bbebee99f Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Mon, 27 Oct 2025 14:45:36 +0100 Subject: [PATCH 3/8] refactor all handlers to use disgo --- embedg-server/actions/data.go | 28 ++-- embedg-server/actions/handler/handle.go | 5 +- embedg-server/actions/handler/interaction.go | 77 ++++++----- embedg-server/actions/parser/actions.go | 12 +- embedg-server/actions/parser/parse.go | 9 +- embedg-server/actions/template/data.go | 127 +++++++++--------- embedg-server/actions/template/provider.go | 50 ++++--- .../api/handlers/custom_bots/commands.go | 64 ++++++--- .../api/handlers/custom_bots/handler.go | 64 +++++---- .../api/handlers/custom_bots/interaction.go | 31 ++--- .../api/handlers/interaction/handler.go | 50 ++----- .../api/handlers/saved_messages/handler.go | 69 +++++++--- .../handlers/scheduled_messages/handler.go | 43 ++++-- .../api/handlers/send_message/handler.go | 124 +++++++++-------- .../api/handlers/send_message/restore.go | 39 +++--- embedg-server/api/managers.go | 3 +- embedg-server/api/routes.go | 7 +- embedg-server/custom_bots/bot.go | 97 +++++++++---- embedg-server/custom_bots/manager.go | 28 ++-- embedg-server/embedg/commands.go | 11 ++ embedg-server/embedg/events.go | 7 + embedg-server/embedg/helpers.go | 14 ++ embedg-server/scheduled_messages/manager.go | 26 ++-- embedg-server/util/guilded.go | 3 +- 24 files changed, 575 insertions(+), 413 deletions(-) create mode 100644 embedg-server/embedg/events.go diff --git a/embedg-server/actions/data.go b/embedg-server/actions/data.go index 2f425795d..55d6f386f 100644 --- a/embedg-server/actions/data.go +++ b/embedg-server/actions/data.go @@ -9,15 +9,15 @@ import ( ) type MessageWithActions struct { - Content string `json:"content,omitempty"` - Username string `json:"username,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - TTS bool `json:"tts,omitempty"` - Embeds []*discordgo.MessageEmbed `json:"embeds,omitempty"` - AllowedMentions *discordgo.MessageAllowedMentions `json:"allowed_mentions,omitempty"` - Components []ComponentWithActions `json:"components,omitempty"` - Actions map[string]ActionSet `json:"actions,omitempty"` - Flags discordgo.MessageFlags `json:"flags,omitempty"` + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + Embeds []discord.Embed `json:"embeds,omitempty"` + AllowedMentions *discord.AllowedMentions `json:"allowed_mentions,omitempty"` + Components []ComponentWithActions `json:"components,omitempty"` + Actions map[string]ActionSet `json:"actions,omitempty"` + Flags discord.MessageFlags `json:"flags,omitempty"` } func (m MessageWithActions) ComponentsV2Enabled() bool { @@ -126,15 +126,15 @@ type ActionDerivedPermissions struct { AllowedRoleIDs []util.ID `json:"lower_role_ids"` } -func (a *ActionDerivedPermissions) HasChannelPermission(permission int64) bool { - return a.GuildIsOwner || (a.GuildPermissions&discordgo.PermissionAdministrator) != 0 || (a.ChannelPermissions&permission) != 0 +func (a *ActionDerivedPermissions) HasChannelPermission(permission discord.Permissions) bool { + return a.GuildIsOwner || (a.GuildPermissions&discord.PermissionAdministrator) != 0 || (a.ChannelPermissions&permission) != 0 } -func (a *ActionDerivedPermissions) HasGuildPermission(permission int64) bool { - return a.GuildIsOwner || (a.GuildPermissions&discordgo.PermissionAdministrator) != 0 || (a.GuildPermissions&permission) != 0 +func (a *ActionDerivedPermissions) HasGuildPermission(permission discord.Permissions) bool { + return a.GuildIsOwner || (a.GuildPermissions&discord.PermissionAdministrator) != 0 || (a.GuildPermissions&permission) != 0 } -func (a *ActionDerivedPermissions) CanManageRole(roleID string) bool { +func (a *ActionDerivedPermissions) CanManageRole(roleID util.ID) bool { if a.GuildIsOwner { return true } diff --git a/embedg-server/actions/handler/handle.go b/embedg-server/actions/handler/handle.go index 908b9d9cd..4ef39256e 100644 --- a/embedg-server/actions/handler/handle.go +++ b/embedg-server/actions/handler/handle.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/disgoorg/disgo/rest" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" @@ -38,7 +39,7 @@ func New(pg *postgres.PostgresStore, parser *parser.ActionParser, planStore stor } } -func (m *ActionHandler) HandleActionInteraction(s *discordgo.Session, i Interaction) error { +func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) error { interaction := i.Interaction() var rawActions []byte @@ -83,7 +84,7 @@ func (m *ActionHandler) HandleActionInteraction(s *discordgo.Session, i Interact col, err := m.pg.Q.GetCustomCommandByName(context.TODO(), pgmodel.GetCustomCommandByNameParams{ Name: fullName, - GuildID: interaction.GuildID, + GuildID: interaction.GuildID().String(), }) if err != nil { if err == sql.ErrNoRows { diff --git a/embedg-server/actions/handler/interaction.go b/embedg-server/actions/handler/interaction.go index 54990a3df..f53085533 100644 --- a/embedg-server/actions/handler/interaction.go +++ b/embedg-server/actions/handler/interaction.go @@ -1,23 +1,26 @@ package handler import ( - "github.com/merlinfuchs/discordgo" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" "github.com/rs/zerolog/log" ) type Interaction interface { - Interaction() *discordgo.Interaction + Interaction() discord.Interaction HasResponded() bool - Respond(data *discordgo.InteractionResponseData, t ...discordgo.InteractionResponseType) *discordgo.Message + Respond(data discord.InteractionResponseData, t ...discord.InteractionResponseType) *discord.Message } type GatewayInteraction struct { Responded bool - Session *discordgo.Session - Inner *discordgo.Interaction + Rest rest.Rest + Inner discord.Interaction } -func (i *GatewayInteraction) Interaction() *discordgo.Interaction { +func (i *GatewayInteraction) Interaction() discord.Interaction { return i.Inner } @@ -25,30 +28,32 @@ func (i *GatewayInteraction) HasResponded() bool { return i.Responded } -func (i *GatewayInteraction) Respond(data *discordgo.InteractionResponseData, t ...discordgo.InteractionResponseType) *discordgo.Message { +func (i *GatewayInteraction) Respond(data discord.InteractionResponseData, t ...discord.InteractionResponseType) *discord.Message { var err error - responseType := discordgo.InteractionResponseChannelMessageWithSource + responseType := discord.InteractionResponseTypeCreateMessage if len(t) > 0 { responseType = t[0] } - var msg *discordgo.Message + var msg *discord.Message if !i.Responded { - err = i.Session.InteractionRespond(i.Inner, &discordgo.InteractionResponse{ - Type: responseType, - Data: data, - }) + err = i.Rest.CreateInteractionResponse( + i.Inner.ID(), + i.Inner.Token(), + discord.InteractionResponse{ + Type: responseType, + Data: data, + }, + ) } else { - msg, err = i.Session.FollowupMessageCreate(i.Inner, true, &discordgo.WebhookParams{ - Content: data.Content, - Embeds: data.Embeds, - Components: data.Components, - Files: data.Files, - Flags: data.Flags, - AllowedMentions: data.AllowedMentions, - }) + msgData, ok := data.(discord.MessageCreate) + if !ok { + err = fmt.Errorf("can't create followup message, data is not a MessageCreate") + } else { + msg, err = i.Rest.CreateFollowupMessage(i.Inner.ApplicationID(), i.Inner.Token(), msgData) + } } if err != nil { @@ -62,12 +67,12 @@ func (i *GatewayInteraction) Respond(data *discordgo.InteractionResponseData, t type RestInteraction struct { Responded bool - InitialResponse chan *discordgo.InteractionResponse - Session *discordgo.Session - Inner *discordgo.Interaction + InitialResponse chan *discord.InteractionResponse + Rest rest.Rest + Inner discord.Interaction } -func (i *RestInteraction) Interaction() *discordgo.Interaction { +func (i *RestInteraction) Interaction() discord.Interaction { return i.Inner } @@ -75,30 +80,28 @@ func (i *RestInteraction) HasResponded() bool { return i.Responded } -func (i *RestInteraction) Respond(data *discordgo.InteractionResponseData, t ...discordgo.InteractionResponseType) *discordgo.Message { +func (i *RestInteraction) Respond(data discord.InteractionResponseData, t ...discord.InteractionResponseType) *discord.Message { var err error - responseType := discordgo.InteractionResponseChannelMessageWithSource + responseType := discord.InteractionResponseTypeCreateMessage if len(t) > 0 { responseType = t[0] } - var msg *discordgo.Message + var msg *discord.Message if !i.Responded { - i.InitialResponse <- &discordgo.InteractionResponse{ + i.InitialResponse <- &discord.InteractionResponse{ Type: responseType, Data: data, } } else { - msg, err = i.Session.FollowupMessageCreate(i.Inner, true, &discordgo.WebhookParams{ - Content: data.Content, - Embeds: data.Embeds, - Components: data.Components, - Files: data.Files, - Flags: data.Flags, - AllowedMentions: data.AllowedMentions, - }) + msgData, ok := data.(discord.MessageCreate) + if !ok { + err = fmt.Errorf("can't create followup message, data is not a MessageCreate") + } else { + msg, err = i.Rest.CreateFollowupMessage(i.Inner.ApplicationID(), i.Inner.Token(), msgData) + } } if err != nil { diff --git a/embedg-server/actions/parser/actions.go b/embedg-server/actions/parser/actions.go index 0adcc98e8..7f096ebbf 100644 --- a/embedg-server/actions/parser/actions.go +++ b/embedg-server/actions/parser/actions.go @@ -12,8 +12,8 @@ import ( "github.com/sqlc-dev/pqtype" ) -func (m *ActionParser) CreateActionsForMessage(ctx context.Context, actionSets map[string]actions.ActionSet, derivedPerms actions.ActionDerivedPermissions, messageID string, ephemeral bool) error { - err := m.pg.Q.DeleteMessageActionSetsForMessage(ctx, messageID) +func (m *ActionParser) CreateActionsForMessage(ctx context.Context, actionSets map[string]actions.ActionSet, derivedPerms actions.ActionDerivedPermissions, messageID util.ID, ephemeral bool) error { + err := m.pg.Q.DeleteMessageActionSetsForMessage(ctx, messageID.String()) if err != nil { log.Error().Err(err).Msg("Failed to delete message action sets") } @@ -31,7 +31,7 @@ func (m *ActionParser) CreateActionsForMessage(ctx context.Context, actionSets m _, err = m.pg.Q.InsertMessageActionSet(ctx, pgmodel.InsertMessageActionSetParams{ ID: util.UniqueID(), - MessageID: messageID, + MessageID: messageID.String(), SetID: actionSetID, Actions: raw, DerivedPermissions: pqtype.NullRawMessage{Valid: true, RawMessage: rawDerivedPerms}, @@ -44,8 +44,8 @@ func (m *ActionParser) CreateActionsForMessage(ctx context.Context, actionSets m return nil } -func (m *ActionParser) RetrieveActionsForMessage(ctx context.Context, messageID string) (map[string]actions.ActionSet, error) { - rows, err := m.pg.Q.GetMessageActionSets(ctx, messageID) +func (m *ActionParser) RetrieveActionsForMessage(ctx context.Context, messageID util.ID) (map[string]actions.ActionSet, error) { + rows, err := m.pg.Q.GetMessageActionSets(ctx, messageID.String()) if err != nil { return nil, fmt.Errorf("Failed to get message action sets: %w", err) } @@ -65,6 +65,6 @@ func (m *ActionParser) RetrieveActionsForMessage(ctx context.Context, messageID return res, nil } -func (m *ActionParser) DeleteActionsForMessage(messageID string) error { +func (m *ActionParser) DeleteActionsForMessage(messageID util.ID) error { return nil } diff --git a/embedg-server/actions/parser/parse.go b/embedg-server/actions/parser/parse.go index c6ba4ff76..f8f4929d2 100644 --- a/embedg-server/actions/parser/parse.go +++ b/embedg-server/actions/parser/parse.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" @@ -28,8 +29,8 @@ func New(accessManager *access.AccessManager, pg *postgres.PostgresStore, caches } } -func (m *ActionParser) ParseMessageComponents(data []actions.ComponentWithActions, allowedComponentTypes []int) ([]discordgo.MessageComponent, error) { - components := make([]discordgo.MessageComponent, 0, len(data)) +func (m *ActionParser) ParseMessageComponents(data []actions.ComponentWithActions, allowedComponentTypes []int) ([]discord.LayoutComponent, error) { + components := make([]discord.LayoutComponent, 0, len(data)) for _, component := range data { parsed, err := m.ParseMessageComponent(component, allowedComponentTypes) @@ -211,7 +212,7 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, } } -func (m *ActionParser) UnparseMessageComponents(data []discordgo.MessageComponent) ([]actions.ComponentWithActions, error) { +func (m *ActionParser) UnparseMessageComponents(data []discord.LayoutComponent) ([]actions.ComponentWithActions, error) { res := make([]actions.ComponentWithActions, 0, len(data)) for _, comp := range data { @@ -225,7 +226,7 @@ func (m *ActionParser) UnparseMessageComponents(data []discordgo.MessageComponen return res, nil } -func (m *ActionParser) UnparseMessageComponent(data discordgo.MessageComponent) (actions.ComponentWithActions, error) { +func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (actions.ComponentWithActions, error) { switch c := data.(type) { case *discordgo.ActionsRow: ar := actions.ComponentWithActions{ diff --git a/embedg-server/actions/template/data.go b/embedg-server/actions/template/data.go index f2a017c51..f9aa7fa81 100644 --- a/embedg-server/actions/template/data.go +++ b/embedg-server/actions/template/data.go @@ -6,7 +6,6 @@ import ( "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/util" ) @@ -14,10 +13,10 @@ var standardDataMap = map[string]interface{}{} type InteractionData struct { caches cache.Caches - i *discord.Interaction + i discord.Interaction } -func NewInteractionData(caches cache.Caches, i *discord.Interaction) *InteractionData { +func NewInteractionData(caches cache.Caches, i discord.Interaction) *InteractionData { return &InteractionData{ caches: caches, i: i, @@ -25,29 +24,33 @@ func NewInteractionData(caches cache.Caches, i *discord.Interaction) *Interactio } func (d *InteractionData) User() interface{} { - if d.i.Member != nil { - res := NewMemberData(d.state, d.i.GuildID, d.i.Member) + if d.i.Member() != nil { + res := NewMemberData(d.caches, *d.i.GuildID(), d.i.Member().Member) return &res } - return NewUserData(d.i.User) + return NewUserData(d.i.User()) } func (d *InteractionData) Member() *MemberData { - if d.i.Member == nil { + if d.i.Member() == nil { return nil } - return NewMemberData(d.state, d.i.GuildID, d.i.Member) + return NewMemberData(d.caches, *d.i.GuildID(), d.i.Member().Member) } func (d *InteractionData) Command() *CommandData { - if d.i.Type != discordgo.InteractionApplicationCommand { + if d.i.Type() != discord.InteractionTypeApplicationCommand { return nil } - data := d.i.ApplicationCommandData() - return NewCommandData(d.state, d.i.GuildID, &data) + cmdInteraction, ok := d.i.(discord.ApplicationCommandInteraction) + if !ok { + return nil + } + + return NewCommandData(d.caches, *d.i.GuildID(), cmdInteraction.Data) } type UserData struct { @@ -132,10 +135,10 @@ type MemberData struct { UserData caches cache.Caches guildID util.ID - m *discord.Member + m discord.Member } -func NewMemberData(caches cache.Caches, guildID util.ID, m *discord.Member) *MemberData { +func NewMemberData(caches cache.Caches, guildID util.ID, m discord.Member) *MemberData { return &MemberData{ UserData: UserData{m.User}, caches: caches, @@ -190,14 +193,14 @@ func (d *MemberData) AvatarURL() string { } type CommandData struct { - state *discordgo.State - guildID string - c *discordgo.ApplicationCommandInteractionData + caches cache.Caches + guildID util.ID + c discord.ApplicationCommandInteractionData } -func NewCommandData(state *discordgo.State, guildID string, c *discordgo.ApplicationCommandInteractionData) *CommandData { +func NewCommandData(caches cache.Caches, guildID util.ID, c discord.ApplicationCommandInteractionData) *CommandData { return &CommandData{ - state: state, + caches: caches, guildID: guildID, c: c, } @@ -208,21 +211,24 @@ func (d *CommandData) String() string { } func (d *CommandData) ID() string { - return d.c.ID + return d.c.CommandID().String() } func (d *CommandData) Name() string { - return d.c.Name + return d.c.CommandName() } func (d *CommandData) Mention() string { - return fmt.Sprintf("", d.c.Name, d.c.ID) + return fmt.Sprintf("", d.c.CommandName(), d.c.CommandID().String()) } func (d *CommandData) Options() map[string]interface{} { res := make(map[string]interface{}) - for _, opt := range d.c.Options { - res[opt.Name] = NewCommandOptionData(d.state, d.guildID, d.c, opt) + + if slashCMD, ok := d.c.(discord.SlashCommandInteractionData); ok { + for _, opt := range slashCMD.Options { + res[opt.Name] = NewCommandOptionData(d.caches, d.guildID, slashCMD, opt) + } } return res @@ -232,43 +238,40 @@ func (d *CommandData) Args() map[string]interface{} { return d.Options() } -func NewCommandOptionData(state *discordgo.State, guildID string, c *discordgo.ApplicationCommandInteractionData, o *discordgo.ApplicationCommandInteractionDataOption) interface{} { +func NewCommandOptionData(caches cache.Caches, guildID util.ID, c discord.SlashCommandInteractionData, o discord.SlashCommandOption) interface{} { switch o.Type { - case discordgo.ApplicationCommandOptionString: - return o.StringValue() - case discordgo.ApplicationCommandOptionInteger: - return o.IntValue() - case discordgo.ApplicationCommandOptionBoolean: - return o.BoolValue() - case discordgo.ApplicationCommandOptionUser: - user := o.UserValue(nil) - resolved := c.Resolved.Users[user.ID] - if resolved != nil { + case discord.ApplicationCommandOptionTypeString: + return o.String() + case discord.ApplicationCommandOptionTypeInt: + return o.Int() + case discord.ApplicationCommandOptionTypeBool: + return o.Bool() + case discord.ApplicationCommandOptionTypeUser: + userID := o.Snowflake() + resolved, ok := c.Resolved.Users[userID] + if ok { return UserData{resolved} } - return UserData{user} - case discordgo.ApplicationCommandOptionChannel: - channel := o.ChannelValue(nil) - resolved := c.Resolved.Channels[channel.ID] - if resolved != nil { - return NewChannelData(state, channel.ID, resolved) + return UserData{u: discord.User{ID: userID}} + case discord.ApplicationCommandOptionTypeChannel: + channelID := o.Snowflake() + return NewChannelData(caches, channelID, nil) + case discord.ApplicationCommandOptionTypeRole: + roleID := o.Snowflake() + resolved, ok := c.Resolved.Roles[roleID] + if ok { + return NewRoleData(caches, guildID, roleID, &resolved) } - return NewChannelData(state, channel.ID, nil) - case discordgo.ApplicationCommandOptionRole: - role := o.RoleValue(nil, "") - resolved := c.Resolved.Roles[role.ID] - if resolved != nil { - return NewRoleData(state, guildID, role.ID, resolved) + return NewRoleData(caches, guildID, roleID, nil) + case discord.ApplicationCommandOptionTypeFloat: + return o.Float() + case discord.ApplicationCommandOptionTypeAttachment: + attachmentID := o.Snowflake() + resolved, ok := c.Resolved.Attachments[attachmentID] + if ok { + return NewAttachmentData(resolved) } - return NewRoleData(state, guildID, role.ID, nil) - case discordgo.ApplicationCommandOptionNumber: - return fmt.Sprintf("%f", o.FloatValue()) - case discordgo.ApplicationCommandOptionAttachment: - attachment := c.Resolved.Attachments[o.Value.(string)] - if attachment != nil { - return NewAttachmentData(attachment) - } - return nil + return NewAttachmentData(discord.Attachment{ID: attachmentID}) } return nil @@ -409,11 +412,11 @@ func (d *GuildData) BoostLevel() (int, error) { type ChannelData struct { caches cache.Caches - channelID string + channelID util.ID channel discord.GuildChannel } -func NewChannelData(caches cache.Caches, channelID string, c discord.GuildChannel) *ChannelData { +func NewChannelData(caches cache.Caches, channelID util.ID, c discord.GuildChannel) *ChannelData { return &ChannelData{ caches: caches, channelID: channelID, @@ -426,7 +429,7 @@ func (d *ChannelData) ensureChannel() error { return nil } - channel, ok := d.caches.Channel(util.ToID(d.channelID)) + channel, ok := d.caches.Channel(d.channelID) if !ok { return fmt.Errorf("channel not found in cache") } @@ -440,7 +443,7 @@ func (d *ChannelData) String() string { } func (d *ChannelData) ID() string { - return d.channelID + return d.channelID.String() } func (d *ChannelData) Name() (string, error) { @@ -521,10 +524,10 @@ func (d *RoleData) Name() (string, error) { } type AttachmentData struct { - a *discordgo.MessageAttachment + a discord.Attachment } -func NewAttachmentData(a *discordgo.MessageAttachment) *AttachmentData { +func NewAttachmentData(a discord.Attachment) *AttachmentData { return &AttachmentData{a: a} } @@ -533,7 +536,7 @@ func (d *AttachmentData) String() string { } func (d *AttachmentData) ID() string { - return d.a.ID + return d.a.ID.String() } func (d *AttachmentData) URL() string { diff --git a/embedg-server/actions/template/provider.go b/embedg-server/actions/template/provider.go index 4f0c672e2..3e5f3a6f9 100644 --- a/embedg-server/actions/template/provider.go +++ b/embedg-server/actions/template/provider.go @@ -7,9 +7,10 @@ import ( "time" "github.com/disgoorg/disgo/cache" - "github.com/merlinfuchs/discordgo" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/embed-generator/embedg-server/model" "github.com/merlinfuchs/embed-generator/embedg-server/store" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) const MaxKVValueLength = 16 * 1024 @@ -21,13 +22,13 @@ type ContextProvider interface { } type InteractionProvider struct { - state *discordgo.State - interaction *discordgo.Interaction + caches cache.Caches + interaction discord.Interaction } -func NewInteractionProvider(state *discordgo.State, interaction *discordgo.Interaction) *InteractionProvider { +func NewInteractionProvider(caches cache.Caches, interaction discord.Interaction) *InteractionProvider { return &InteractionProvider{ - state: state, + caches: caches, interaction: interaction, } } @@ -35,22 +36,27 @@ func NewInteractionProvider(state *discordgo.State, interaction *discordgo.Inter func (p *InteractionProvider) ProvideFuncs(funcs map[string]interface{}) {} func (p *InteractionProvider) ProvideData(data map[string]interface{}) { - data["Interaction"] = NewInteractionData(p.state, p.interaction) + data["Interaction"] = NewInteractionData(p.caches, p.interaction) - guildData := NewGuildData(p.state, p.interaction.GuildID, nil) + guildID := p.interaction.GuildID() + if guildID == nil { + return + } + + guildData := NewGuildData(p.caches, *guildID, nil) data["Guild"] = guildData data["Server"] = guildData - data["Channel"] = NewChannelData(p.state, p.interaction.ChannelID, nil) + data["Channel"] = NewChannelData(p.caches, p.interaction.Channel().ID(), nil) } type GuildProvider struct { caches cache.Caches - guildID string - guild *discordgo.Guild + guildID util.ID + guild *discord.Guild } -func NewGuildProvider(caches cache.Caches, guildID string, guild *discordgo.Guild) *GuildProvider { +func NewGuildProvider(caches cache.Caches, guildID util.ID, guild *discord.Guild) *GuildProvider { return &GuildProvider{ caches: caches, guildID: guildID, @@ -68,11 +74,11 @@ func (p *GuildProvider) ProvideData(data map[string]interface{}) { type ChannelProvider struct { caches cache.Caches - channelID string - channel *discordgo.Channel + channelID util.ID + channel discord.GuildChannel } -func NewChannelProvider(caches cache.Caches, channelID string, channel *discordgo.Channel) *ChannelProvider { +func NewChannelProvider(caches cache.Caches, channelID util.ID, channel discord.GuildChannel) *ChannelProvider { return &ChannelProvider{ caches: caches, channelID: channelID, @@ -87,12 +93,12 @@ func (p *ChannelProvider) ProvideData(data map[string]interface{}) { } type KVProvider struct { - guildID string + guildID util.ID kvStore store.KVEntryStore maxGuildKeys int } -func NewKVProvider(guildID string, kvStore store.KVEntryStore, maxGuildKeys int) *KVProvider { +func NewKVProvider(guildID util.ID, kvStore store.KVEntryStore, maxGuildKeys int) *KVProvider { return &KVProvider{ guildID: guildID, kvStore: kvStore, @@ -111,7 +117,7 @@ func (p *KVProvider) ProvideFuncs(funcs map[string]interface{}) { func (p *KVProvider) ProvideData(data map[string]interface{}) {} func (kv *KVProvider) getKey(key string) (string, error) { - entry, err := kv.kvStore.GetKVEntry(context.TODO(), kv.guildID, key) + entry, err := kv.kvStore.GetKVEntry(context.TODO(), kv.guildID.String(), key) if err != nil { if err == store.ErrNotFound { return "", nil @@ -134,7 +140,7 @@ func (kv *KVProvider) setKey(key string, value string) error { } err := kv.kvStore.SetKVEntry(context.TODO(), model.KVEntry{ - GuildID: kv.guildID, + GuildID: kv.guildID.String(), Key: key, Value: value, CreatedAt: time.Now().UTC(), @@ -156,7 +162,7 @@ func (kv *KVProvider) increaseKey(key string, delta int) (string, error) { } entry, err := kv.kvStore.IncreaseKVEntry(context.TODO(), model.KVEntryIncreaseParams{ - GuildID: kv.guildID, + GuildID: kv.guildID.String(), Key: key, Delta: delta, CreatedAt: time.Now().UTC(), @@ -172,7 +178,7 @@ func (kv *KVProvider) increaseKey(key string, delta int) (string, error) { } func (kv *KVProvider) deleteKey(key string) (string, error) { - entry, err := kv.kvStore.DeleteKVEntry(context.TODO(), kv.guildID, key) + entry, err := kv.kvStore.DeleteKVEntry(context.TODO(), kv.guildID.String(), key) if err != nil { if err == sql.ErrNoRows { return "", nil @@ -183,7 +189,7 @@ func (kv *KVProvider) deleteKey(key string) (string, error) { } func (kv *KVProvider) searchKeys(pattern string) (map[string]string, error) { - entries, err := kv.kvStore.SearchKVEntries(context.TODO(), kv.guildID, pattern) + entries, err := kv.kvStore.SearchKVEntries(context.TODO(), kv.guildID.String(), pattern) if err != nil { return nil, err } @@ -197,7 +203,7 @@ func (kv *KVProvider) searchKeys(pattern string) (map[string]string, error) { } func (kv *KVProvider) checkKeyCountLimit() error { - entryCount, err := kv.kvStore.CountKVEntries(context.TODO(), kv.guildID) + entryCount, err := kv.kvStore.CountKVEntries(context.TODO(), kv.guildID.String()) if err != nil { return fmt.Errorf("failed to count KV keys: %w", err) } diff --git a/embedg-server/api/handlers/custom_bots/commands.go b/embedg-server/api/handlers/custom_bots/commands.go index e89a8bf60..d759311ac 100644 --- a/embedg-server/api/handlers/custom_bots/commands.go +++ b/embedg-server/api/handlers/custom_bots/commands.go @@ -20,12 +20,16 @@ import ( ) func (h *CustomBotsHandler) HandleListCustomCommands(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - commands, err := h.pg.Q.GetCustomCommands(c.Context(), guildID) + commands, err := h.pg.Q.GetCustomCommands(c.Context(), guildID.String()) if err != nil { return err } @@ -58,13 +62,17 @@ func (h *CustomBotsHandler) HandleListCustomCommands(c *fiber.Ctx) error { } func (h *CustomBotsHandler) HandleGetCustomCommand(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } command, err := h.pg.Q.GetCustomCommand(c.Context(), pgmodel.GetCustomCommandParams{ - GuildID: guildID, + GuildID: guildID.String(), ID: c.Params("commandID"), }) if err != nil { @@ -100,7 +108,11 @@ func (h *CustomBotsHandler) HandleCreateCustomCommand(c *fiber.Ctx, req wire.Cus session := c.Locals("session").(*session.Session) req.Normalize() - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -114,7 +126,7 @@ func (h *CustomBotsHandler) HandleCreateCustomCommand(c *fiber.Ctx, req wire.Cus return helpers.Forbidden("insufficient_plan", "This feature is not available on your plan!") } - existingCount, err := h.pg.Q.CountCustomCommands(c.Context(), guildID) + existingCount, err := h.pg.Q.CountCustomCommands(c.Context(), guildID.String()) if err != nil { return err } @@ -129,7 +141,7 @@ func (h *CustomBotsHandler) HandleCreateCustomCommand(c *fiber.Ctx, req wire.Cus return err } - derivedPerms, err := h.actionParser.DerivePermissionsForActions(session.UserID, guildID, "") + derivedPerms, err := h.actionParser.DerivePermissionsForActions(session.UserID, guildID, 0) if err != nil { return helpers.BadRequest("invalid_actions", err.Error()) } @@ -146,7 +158,7 @@ func (h *CustomBotsHandler) HandleCreateCustomCommand(c *fiber.Ctx, req wire.Cus command, err := h.pg.Q.InsertCustomCommand(c.Context(), pgmodel.InsertCustomCommandParams{ ID: util.UniqueID(), - GuildID: guildID, + GuildID: guildID.String(), Name: req.Name, Description: req.Description, Parameters: rawParameters, @@ -182,7 +194,11 @@ func (h *CustomBotsHandler) HandleUpdateCustomCommand(c *fiber.Ctx, req wire.Cus session := c.Locals("session").(*session.Session) req.Normalize() - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -202,7 +218,7 @@ func (h *CustomBotsHandler) HandleUpdateCustomCommand(c *fiber.Ctx, req wire.Cus return err } - derivedPerms, err := h.actionParser.DerivePermissionsForActions(session.UserID, guildID, "") + derivedPerms, err := h.actionParser.DerivePermissionsForActions(session.UserID, guildID, 0) if err != nil { return helpers.BadRequest("invalid_actions", err.Error()) } @@ -219,7 +235,7 @@ func (h *CustomBotsHandler) HandleUpdateCustomCommand(c *fiber.Ctx, req wire.Cus command, err := h.pg.Q.UpdateCustomCommand(c.Context(), pgmodel.UpdateCustomCommandParams{ ID: c.Params("commandID"), - GuildID: guildID, + GuildID: guildID.String(), Name: req.Name, Description: req.Description, Enabled: req.Enabled, @@ -251,13 +267,17 @@ func (h *CustomBotsHandler) HandleUpdateCustomCommand(c *fiber.Ctx, req wire.Cus } func (h *CustomBotsHandler) HandleDeleteCustomCommand(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - _, err := h.pg.Q.DeleteCustomCommand(c.Context(), pgmodel.DeleteCustomCommandParams{ - GuildID: guildID, + _, err = h.pg.Q.DeleteCustomCommand(c.Context(), pgmodel.DeleteCustomCommandParams{ + GuildID: guildID.String(), ID: c.Params("commandID"), }) if err != nil { @@ -273,7 +293,11 @@ func (h *CustomBotsHandler) HandleDeleteCustomCommand(c *fiber.Ctx) error { } func (h *CustomBotsHandler) HandleDeployCustomCommands(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -287,7 +311,7 @@ func (h *CustomBotsHandler) HandleDeployCustomCommands(c *fiber.Ctx) error { return helpers.Forbidden("insufficient_plan", "This feature is not available on your plan!") } - customBot, err := h.pg.Q.GetCustomBotByGuildID(c.Context(), guildID) + customBot, err := h.pg.Q.GetCustomBotByGuildID(c.Context(), guildID.String()) if err != nil { if err == sql.ErrNoRows { return helpers.NotFound("not_configured", "There is no custom bot configured right now, you need to configure one first.") @@ -295,7 +319,7 @@ func (h *CustomBotsHandler) HandleDeployCustomCommands(c *fiber.Ctx) error { return fmt.Errorf("Failed to retrieve custom bot: %w", err) } - commands, err := h.pg.Q.GetCustomCommands(c.Context(), guildID) + commands, err := h.pg.Q.GetCustomCommands(c.Context(), guildID.String()) if err != nil { return fmt.Errorf("Failed to retrieve custom commands: %w", err) } @@ -314,13 +338,13 @@ func (h *CustomBotsHandler) HandleDeployCustomCommands(c *fiber.Ctx) error { return fmt.Errorf("Failed to create custom bot session: %w", err) } - _, err = session.ApplicationCommandBulkOverwrite(customBot.ApplicationID, guildID, payload) + _, err = session.ApplicationCommandBulkOverwrite(customBot.ApplicationID, guildID.String(), payload) if err != nil { return fmt.Errorf("Failed to deploy commands: %w", err) } _, err = h.pg.Q.SetCustomCommandsDeployedAt(c.Context(), pgmodel.SetCustomCommandsDeployedAtParams{ - GuildID: guildID, + GuildID: guildID.String(), DeployedAt: sql.NullTime{ Time: time.Now().UTC(), Valid: true, @@ -335,6 +359,8 @@ func (h *CustomBotsHandler) HandleDeployCustomCommands(c *fiber.Ctx) error { }) } +// commandsToPayload converts a list of custom commands to a list of Discord application commands +// This still uses discordgo because I didn't have the nerve to convert it to disgo yet. func commandsToPayload(commands []pgmodel.CustomCommand) (error, []*discordgo.ApplicationCommand) { res := make([]*discordgo.ApplicationCommand, 0, len(commands)) diff --git a/embedg-server/api/handlers/custom_bots/handler.go b/embedg-server/api/handlers/custom_bots/handler.go index d198eda07..3b9e9a3f4 100644 --- a/embedg-server/api/handlers/custom_bots/handler.go +++ b/embedg-server/api/handlers/custom_bots/handler.go @@ -7,8 +7,6 @@ import ( "slices" - "github.com/disgoorg/disgo/cache" - "github.com/disgoorg/disgo/rest" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" @@ -17,6 +15,8 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" "github.com/merlinfuchs/embed-generator/embedg-server/store" "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" @@ -26,16 +26,16 @@ import ( type CustomBotsHandler struct { pg *postgres.PostgresStore - caches cache.Caches + embedg *embedg.EmbedGenerator am *access.AccessManager planStore store.PlanStore actionParser *parser.ActionParser } -func New(pg *postgres.PostgresStore, caches cache.Caches, am *access.AccessManager, planStore store.PlanStore, actionParser *parser.ActionParser) *CustomBotsHandler { +func New(pg *postgres.PostgresStore, embedg *embedg.EmbedGenerator, am *access.AccessManager, planStore store.PlanStore, actionParser *parser.ActionParser) *CustomBotsHandler { return &CustomBotsHandler{ pg: pg, - caches: caches, + embedg: embedg, am: am, planStore: planStore, actionParser: actionParser, @@ -43,7 +43,11 @@ func New(pg *postgres.PostgresStore, caches cache.Caches, am *access.AccessManag } func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.CustomBotConfigureRequestWire) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -57,7 +61,7 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust return helpers.Forbidden("insufficient_plan", "This feature is not available on your plan!") } - client := rest.New(rest.NewClient(req.Token)) + client := rest.NewRestClient(req.Token) if err != nil { return err } @@ -76,7 +80,7 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust } isMember := true - member, err := client.GetMember(util.ToID(guildID), user.ID) + member, err := client.GetMember(guildID, user.ID) if err != nil { if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeMissingAccess, discordgo.ErrCodeUnknownGuild) { isMember = false @@ -85,12 +89,12 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust } } - roles := h.caches.Roles(util.ToID(guildID)) + roles := h.embedg.Caches().Roles(guildID) hasPermissions := false if isMember { for role := range roles { - if slices.Contains(member.RoleIDs, role.ID) || role.ID == util.ToID(guildID) { + if slices.Contains(member.RoleIDs, role.ID) || role.ID == guildID { if role.Permissions&discordgo.PermissionManageWebhooks != 0 { hasPermissions = true break @@ -106,7 +110,7 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust customBot, err := h.pg.Q.UpsertCustomBot(c.Context(), pgmodel.UpsertCustomBotParams{ ID: util.UniqueID(), - GuildID: guildID, + GuildID: guildID.String(), ApplicationID: app.ID.String(), UserID: user.ID.String(), UserName: user.Username, @@ -134,7 +138,7 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust IsMember: isMember, HasPermissions: hasPermissions, HandledFirstInteraction: customBot.HandledFirstInteraction, - InviteURL: botInvite(customBot.ApplicationID, guildID), + InviteURL: botInvite(customBot.ApplicationID, guildID.String()), InteractionEndpointURL: interactionEndpointURL(customBot.ID), GatewayStatus: customBot.GatewayStatus, @@ -147,7 +151,11 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust } func (h *CustomBotsHandler) HandleUpdateCustomBotPresence(c *fiber.Ctx, req wire.CustomBotUpdatePresenceRequestWire) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -162,7 +170,7 @@ func (h *CustomBotsHandler) HandleUpdateCustomBotPresence(c *fiber.Ctx, req wire } _, err = h.pg.Q.UpdateCustomBotPresence(c.Context(), pgmodel.UpdateCustomBotPresenceParams{ - GuildID: guildID, + GuildID: guildID.String(), GatewayStatus: req.GatewayStatus, GatewayActivityType: sql.NullInt16{ Int16: int16(req.GatewayActivityType), @@ -186,12 +194,16 @@ func (h *CustomBotsHandler) HandleUpdateCustomBotPresence(c *fiber.Ctx, req wire } func (h *CustomBotsHandler) HandleDisableCustomBot(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - _, err := h.pg.Q.DeleteCustomBot(c.Context(), guildID) + _, err = h.pg.Q.DeleteCustomBot(c.Context(), guildID.String()) if err != nil { if err == sql.ErrNoRows { return helpers.NotFound("not_configured", "There is no custom bot configured right now") @@ -206,12 +218,16 @@ func (h *CustomBotsHandler) HandleDisableCustomBot(c *fiber.Ctx) error { } func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "Invalid guild ID") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - customBot, err := h.pg.Q.GetCustomBotByGuildID(c.Context(), guildID) + customBot, err := h.pg.Q.GetCustomBotByGuildID(c.Context(), guildID.String()) if err != nil { if err == sql.ErrNoRows { return helpers.NotFound("not_configured", "There is no custom bot configured right now") @@ -219,14 +235,14 @@ func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { return err } - client := rest.New(rest.NewClient(customBot.Token)) + client := rest.NewRestClient(customBot.Token) if err != nil { return err } isMember := true tokenValid := true - member, err := client.GetMember(util.ToID(guildID), util.ToID(customBot.UserID)) + member, err := client.GetMember(guildID, util.ToID(customBot.UserID)) if err != nil { if util.IsDiscordRestStatusCode(err, 401) { tokenValid = false @@ -245,7 +261,7 @@ func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { if member != nil { customBot, err = h.pg.Q.UpdateCustomBotUser(c.Context(), pgmodel.UpdateCustomBotUserParams{ - GuildID: guildID, + GuildID: guildID.String(), UserName: member.User.Username, UserDiscriminator: member.User.Discriminator, UserAvatar: userAvatar, @@ -255,12 +271,12 @@ func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { } } - roles := h.caches.Roles(util.ToID(guildID)) + roles := h.embedg.Caches().Roles(guildID) hasPermissions := false if member != nil { for role := range roles { - if slices.Contains(member.RoleIDs, role.ID) || role.ID == util.ToID(guildID) { + if slices.Contains(member.RoleIDs, role.ID) || role.ID == guildID { if role.Permissions&discordgo.PermissionManageWebhooks != 0 { hasPermissions = true break @@ -283,7 +299,7 @@ func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { IsMember: isMember, HasPermissions: hasPermissions, HandledFirstInteraction: customBot.HandledFirstInteraction, - InviteURL: botInvite(customBot.ApplicationID, guildID), + InviteURL: botInvite(customBot.ApplicationID, guildID.String()), InteractionEndpointURL: interactionEndpointURL(customBot.ID), GatewayStatus: customBot.GatewayStatus, diff --git a/embedg-server/api/handlers/custom_bots/interaction.go b/embedg-server/api/handlers/custom_bots/interaction.go index 4d021b605..a10ef430b 100644 --- a/embedg-server/api/handlers/custom_bots/interaction.go +++ b/embedg-server/api/handlers/custom_bots/interaction.go @@ -9,10 +9,12 @@ import ( "strings" "time" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" "github.com/gofiber/fiber/v2" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" "github.com/rs/zerolog/log" ) @@ -31,13 +33,13 @@ func (h *CustomBotsHandler) HandleCustomBotInteraction(c *fiber.Ctx) error { return helpers.Unauthorized("invalid_signature", "Invalid signature") } - interaction := &discordgo.InteractionCreate{} + interaction := &events.InteractionCreate{} err = c.BodyParser(interaction) if err != nil { return err } - if interaction.AppID != customBot.ApplicationID { + if interaction.ApplicationID().String() != customBot.ApplicationID { return fmt.Errorf("application id mismatch") } @@ -46,36 +48,35 @@ func (h *CustomBotsHandler) HandleCustomBotInteraction(c *fiber.Ctx) error { log.Error().Err(err).Msg("Failed to set custom bot handled first interaction") } - if interaction.Type == discordgo.InteractionPing { - return c.JSON(discordgo.InteractionResponse{ - Type: discordgo.InteractionResponsePong, + if interaction.Type() == discord.InteractionTypePing { + return c.JSON(discord.InteractionResponse{ + Type: discord.InteractionResponseTypePong, }) } handle := false - switch interaction.Type { - case discordgo.InteractionMessageComponent: - data := interaction.MessageComponentData() - if strings.HasPrefix(data.CustomID, "action:") { + switch i := interaction.Interaction.(type) { + case discord.ComponentInteraction: + if strings.HasPrefix(i.Data.CustomID(), "action:") { handle = true } - case discordgo.InteractionApplicationCommand: + case discord.ApplicationCommandInteraction: handle = true } if handle { - respCh := make(chan *discordgo.InteractionResponse) + respCh := make(chan *discord.InteractionResponse) ri := &handler.RestInteraction{ Inner: interaction.Interaction, - Session: h.bot.Session, // TODO?: Use custom bot session + Rest: h.embedg.Rest(), // TODO?: Use custom bot session InitialResponse: respCh, } go func() { - session, _ := discordgo.New("Bot " + customBot.Token) + client := rest.NewRestClient(customBot.Token) - err := h.bot.ActionHandler.HandleActionInteraction(session, ri) + err := h.embedg.ActionHandler().HandleActionInteraction(client, ri) if err != nil { log.Error().Err(err).Msg("Failed to handle action interaction") } diff --git a/embedg-server/api/handlers/interaction/handler.go b/embedg-server/api/handlers/interaction/handler.go index 8b798dd8c..66973233a 100644 --- a/embedg-server/api/handlers/interaction/handler.go +++ b/embedg-server/api/handlers/interaction/handler.go @@ -4,30 +4,24 @@ import ( "bytes" "crypto/ed25519" "encoding/hex" - "strings" "time" - "github.com/disgoorg/disgo/cache" - "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" "github.com/gofiber/fiber/v2" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" - "github.com/rs/zerolog/log" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/spf13/viper" ) type InteractionHandler struct { - caches cache.Caches - rest rest.Rest - actionHandler *handler.ActionHandler + embedg *embedg.EmbedGenerator } -func New(caches cache.Caches, rest rest.Rest, actionHandler *handler.ActionHandler) *InteractionHandler { +func New(embedg *embedg.EmbedGenerator) *InteractionHandler { return &InteractionHandler{ - caches: caches, - rest: rest, - actionHandler: actionHandler, + embedg: embedg, } } @@ -38,45 +32,27 @@ func (h *InteractionHandler) HandleBotInteraction(c *fiber.Ctx) error { return helpers.Unauthorized("invalid_signature", "Invalid signature") } - interaction := &discordgo.InteractionCreate{} + interaction := &events.InteractionCreate{} err := c.BodyParser(interaction) if err != nil { return err } - if interaction.Type == discordgo.InteractionPing { - return c.JSON(discordgo.InteractionResponse{ - Type: discordgo.InteractionResponsePong, + if interaction.Type() == discord.InteractionTypePing { + return c.JSON(discord.InteractionResponse{ + Type: discord.InteractionResponseTypePong, }) } - customAction := false - switch interaction.Type { - case discordgo.InteractionMessageComponent: - data := interaction.MessageComponentData() - if strings.HasPrefix(data.CustomID, "action:") { - customAction = true - } - } - - respCh := make(chan *discordgo.InteractionResponse) + respCh := make(chan *discord.InteractionResponse) ri := &handler.RestInteraction{ Inner: interaction.Interaction, - Session: h.bot.Session, + Rest: h.embedg.Rest(), InitialResponse: respCh, } - go func() { - if customAction { - err := h.actionHandler.HandleActionInteraction(h.bot.Session, ri) - if err != nil { - log.Error().Err(err).Msg("Failed to handle action interaction") - } - } else { - h.bot.HandlerInteraction(h.bot.Session, ri, interaction.Interaction.Data) - } - }() + h.embedg.HandleInteraction(ri) select { case resp := <-respCh: diff --git a/embedg-server/api/handlers/saved_messages/handler.go b/embedg-server/api/handlers/saved_messages/handler.go index 581536e47..72e9f76de 100644 --- a/embedg-server/api/handlers/saved_messages/handler.go +++ b/embedg-server/api/handlers/saved_messages/handler.go @@ -31,18 +31,23 @@ func New(pg *postgres.PostgresStore, am *access.AccessManager) *SavedMessagesHan func (h *SavedMessagesHandler) HandleListSavedMessages(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") var messages []pgmodel.SavedMessage var err error - if guildID != "" { + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } + if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - messages, err = h.pg.Q.GetSavedMessagesForGuild(c.Context(), sql.NullString{String: guildID, Valid: true}) + messages, err = h.pg.Q.GetSavedMessagesForGuild(c.Context(), sql.NullString{String: guildID.String(), Valid: true}) } else { - messages, err = h.pg.Q.GetSavedMessagesForCreator(c.Context(), session.UserID) + messages, err = h.pg.Q.GetSavedMessagesForCreator(c.Context(), session.UserID.String()) } if err != nil { @@ -63,9 +68,14 @@ func (h *SavedMessagesHandler) HandleListSavedMessages(c *fiber.Ctx) error { func (h *SavedMessagesHandler) HandleCreateSavedMessage(c *fiber.Ctx, req wire.SavedMessageCreateRequestWire) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") + + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } - if guildID != "" { if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -73,8 +83,8 @@ func (h *SavedMessagesHandler) HandleCreateSavedMessage(c *fiber.Ctx, req wire.S message, err := h.pg.Q.InsertSavedMessage(c.Context(), pgmodel.InsertSavedMessageParams{ ID: util.UniqueID(), - CreatorID: session.UserID, - GuildID: sql.NullString{String: guildID, Valid: guildID != ""}, + CreatorID: session.UserID.String(), + GuildID: sql.NullString{String: rawGuildID, Valid: rawGuildID != ""}, UpdatedAt: time.Now().UTC(), Name: req.Name, Description: sql.NullString{String: req.Description.String, Valid: req.Description.Valid}, @@ -94,9 +104,14 @@ func (h *SavedMessagesHandler) HandleCreateSavedMessage(c *fiber.Ctx, req wire.S func (h *SavedMessagesHandler) HandleUpdateSavedMessage(c *fiber.Ctx, req wire.SavedMessageUpdateRequestWire) error { session := c.Locals("session").(*session.Session) messageID := c.Params("messageID") - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") + + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } - if guildID != "" { if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -104,10 +119,10 @@ func (h *SavedMessagesHandler) HandleUpdateSavedMessage(c *fiber.Ctx, req wire.S var message pgmodel.SavedMessage var err error - if guildID != "" { + if rawGuildID != "" { message, err = h.pg.Q.UpdateSavedMessageForGuild(c.Context(), pgmodel.UpdateSavedMessageForGuildParams{ ID: messageID, - GuildID: sql.NullString{String: guildID, Valid: true}, + GuildID: sql.NullString{String: rawGuildID, Valid: true}, UpdatedAt: time.Now().UTC(), Name: req.Name, Description: sql.NullString{String: req.Description.String, Valid: req.Description.Valid}, @@ -116,7 +131,7 @@ func (h *SavedMessagesHandler) HandleUpdateSavedMessage(c *fiber.Ctx, req wire.S } else { message, err = h.pg.Q.UpdateSavedMessageForCreator(c.Context(), pgmodel.UpdateSavedMessageForCreatorParams{ ID: messageID, - CreatorID: session.UserID, + CreatorID: session.UserID.String(), UpdatedAt: time.Now().UTC(), Name: req.Name, Description: sql.NullString{String: req.Description.String, Valid: req.Description.Valid}, @@ -141,24 +156,29 @@ func (h *SavedMessagesHandler) HandleUpdateSavedMessage(c *fiber.Ctx, req wire.S func (h *SavedMessagesHandler) HandleDeleteSavedMessage(c *fiber.Ctx) error { session := c.Locals("session").(*session.Session) messageID := c.Params("messageID") - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") + + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } - if guildID != "" { if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } } var err error - if guildID != "" { + if rawGuildID != "" { err = h.pg.Q.DeleteSavedMessageForGuild(c.Context(), pgmodel.DeleteSavedMessageForGuildParams{ ID: messageID, - GuildID: sql.NullString{String: guildID, Valid: true}, + GuildID: sql.NullString{String: rawGuildID, Valid: true}, }) } else { err = h.pg.Q.DeleteSavedMessageForCreator(c.Context(), pgmodel.DeleteSavedMessageForCreatorParams{ ID: messageID, - CreatorID: session.UserID, + CreatorID: session.UserID.String(), }) } @@ -178,9 +198,14 @@ func (h *SavedMessagesHandler) HandleDeleteSavedMessage(c *fiber.Ctx) error { func (h *SavedMessagesHandler) HandleImportSavedMessages(c *fiber.Ctx, req wire.SavedMessagesImportRequestWire) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") + rawGuildID := c.Query("guild_id") + + if rawGuildID != "" { + guildID, err := util.ParseID(rawGuildID) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } - if guildID != "" { if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } @@ -191,8 +216,8 @@ func (h *SavedMessagesHandler) HandleImportSavedMessages(c *fiber.Ctx, req wire. for i, msg := range req.Messages { message, err := h.pg.Q.InsertSavedMessage(c.Context(), pgmodel.InsertSavedMessageParams{ ID: util.UniqueID(), - CreatorID: session.UserID, - GuildID: sql.NullString{String: guildID, Valid: guildID != ""}, + CreatorID: session.UserID.String(), + GuildID: sql.NullString{String: rawGuildID, Valid: rawGuildID != ""}, UpdatedAt: time.Now().UTC(), Name: msg.Name, Description: sql.NullString{String: msg.Description.String, Valid: msg.Description.Valid}, diff --git a/embedg-server/api/handlers/scheduled_messages/handler.go b/embedg-server/api/handlers/scheduled_messages/handler.go index e6bd5ea2f..c4b8e914d 100644 --- a/embedg-server/api/handlers/scheduled_messages/handler.go +++ b/embedg-server/api/handlers/scheduled_messages/handler.go @@ -34,13 +34,16 @@ func New(pg *postgres.PostgresStore, am *access.AccessManager, planStore store.P func (h *ScheduledMessageHandler) HandleCreateScheduledMessage(c *fiber.Ctx, req wire.ScheduledMessageCreateRequestWire) error { session := c.Locals("session").(*session.Session) - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - if err := h.am.CheckChannelAccessForRequest(c, req.ChannelID); err != nil { + if err := h.am.CheckChannelAccessForRequest(c, util.ToID(req.ChannelID)); err != nil { return err } @@ -83,8 +86,8 @@ func (h *ScheduledMessageHandler) HandleCreateScheduledMessage(c *fiber.Ctx, req msg, err := h.pg.Q.InsertScheduledMessage(c.Context(), pgmodel.InsertScheduledMessageParams{ ID: util.UniqueID(), - CreatorID: session.UserID, - GuildID: guildID, + CreatorID: session.UserID.String(), + GuildID: guildID.String(), ChannelID: req.ChannelID, MessageID: sql.NullString{ String: req.MessageID.String, @@ -131,13 +134,16 @@ func (h *ScheduledMessageHandler) HandleCreateScheduledMessage(c *fiber.Ctx, req } func (h *ScheduledMessageHandler) HandleListScheduledMessages(c *fiber.Ctx) error { - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - messages, err := h.pg.Q.GetScheduledMessages(c.Context(), guildID) + messages, err := h.pg.Q.GetScheduledMessages(c.Context(), guildID.String()) if err != nil { log.Error().Err(err).Msg("Failed to get scheduled messages") @@ -157,7 +163,10 @@ func (h *ScheduledMessageHandler) HandleListScheduledMessages(c *fiber.Ctx) erro func (h *ScheduledMessageHandler) HandleGetScheduledMessage(c *fiber.Ctx) error { messageID := c.Params("messageID") - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err @@ -165,7 +174,7 @@ func (h *ScheduledMessageHandler) HandleGetScheduledMessage(c *fiber.Ctx) error msg, err := h.pg.Q.GetScheduledMessage(c.Context(), pgmodel.GetScheduledMessageParams{ ID: messageID, - GuildID: guildID, + GuildID: guildID.String(), }) if err != nil { if err == sql.ErrNoRows { @@ -183,13 +192,16 @@ func (h *ScheduledMessageHandler) HandleGetScheduledMessage(c *fiber.Ctx) error func (h *ScheduledMessageHandler) HandleUpdateScheduledMessage(c *fiber.Ctx, req wire.ScheduledMessageUpdateRequestWire) error { messageID := c.Params("messageID") - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - if err := h.am.CheckChannelAccessForRequest(c, req.ChannelID); err != nil { + if err := h.am.CheckChannelAccessForRequest(c, util.ToID(req.ChannelID)); err != nil { return err } @@ -230,7 +242,7 @@ func (h *ScheduledMessageHandler) HandleUpdateScheduledMessage(c *fiber.Ctx, req msg, err := h.pg.Q.UpdateScheduledMessage(c.Context(), pgmodel.UpdateScheduledMessageParams{ ID: messageID, - GuildID: guildID, + GuildID: guildID.String(), ChannelID: req.ChannelID, MessageID: sql.NullString{ String: req.MessageID.String, @@ -281,15 +293,18 @@ func (h *ScheduledMessageHandler) HandleUpdateScheduledMessage(c *fiber.Ctx, req func (h *ScheduledMessageHandler) HandleDeleteScheduledMessage(c *fiber.Ctx) error { messageID := c.Params("messageID") - guildID := c.Query("guild_id") + guildID, err := util.ParseID(c.Query("guild_id")) + if err != nil { + return helpers.BadRequest("invalid_guild_id", "The guild_id field is invalid.") + } if err := h.am.CheckGuildAccessForRequest(c, guildID); err != nil { return err } - err := h.pg.Q.DeleteScheduledMessage(c.Context(), pgmodel.DeleteScheduledMessageParams{ + err = h.pg.Q.DeleteScheduledMessage(c.Context(), pgmodel.DeleteScheduledMessageParams{ ID: messageID, - GuildID: guildID, + GuildID: guildID.String(), }) if err != nil { diff --git a/embedg-server/api/handlers/send_message/handler.go b/embedg-server/api/handlers/send_message/handler.go index 50b085414..9b0daa10d 100644 --- a/embedg-server/api/handlers/send_message/handler.go +++ b/embedg-server/api/handlers/send_message/handler.go @@ -5,10 +5,10 @@ import ( "encoding/json" "fmt" - "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/snowflake/v2" "github.com/gofiber/fiber/v2" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/actions/template" @@ -17,6 +17,7 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/api/session" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/merlinfuchs/embed-generator/embedg-server/store" "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" @@ -24,8 +25,7 @@ import ( ) type SendMessageHandler struct { - caches cache.Caches - rest rest.Rest + embedg *embedg.EmbedGenerator pg *postgres.PostgresStore accessManager *access.AccessManager actionParser *parser.ActionParser @@ -33,16 +33,14 @@ type SendMessageHandler struct { } func New( - caches cache.Caches, - rest rest.Rest, + embedg *embedg.EmbedGenerator, pg *postgres.PostgresStore, accessManager *access.AccessManager, actionParser *parser.ActionParser, planStore store.PlanStore, ) *SendMessageHandler { return &SendMessageHandler{ - caches: caches, - rest: rest, + embedg: embedg, pg: pg, accessManager: accessManager, actionParser: actionParser, @@ -53,25 +51,25 @@ func New( func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.MessageSendToChannelRequestWire) error { session := c.Locals("session").(*session.Session) - if err := h.accessManager.CheckChannelAccessForRequest(c, req.ChannelID); err != nil { + if err := h.accessManager.CheckChannelAccessForRequest(c, util.ToID(req.ChannelID)); err != nil { return err } - channel, err := h.bot.State.Channel(req.ChannelID) - if err != nil { - return fmt.Errorf("Failed to get channel: %w", err) + channel, ok := h.embedg.Caches().Channel(util.ToID(req.ChannelID)) + if !ok { + return helpers.BadRequest("channel_not_found", "Channel not found") } - features, err := h.planStore.GetPlanFeaturesForGuild(c.Context(), channel.GuildID) + features, err := h.planStore.GetPlanFeaturesForGuild(c.Context(), channel.GuildID()) if err != nil { return fmt.Errorf("could not get plan features: %w", err) } templates := template.NewContext( "SEND_MESSAGE", features.MaxTemplateOps, - template.NewGuildProvider(h.bot.State, channel.GuildID, nil), - template.NewChannelProvider(h.bot.State, req.ChannelID, nil), - template.NewKVProvider(channel.GuildID, h.pg, features.MaxKVKeys), + template.NewGuildProvider(h.embedg.Caches(), channel.GuildID(), nil), + template.NewChannelProvider(h.embedg.Caches(), util.ToID(req.ChannelID), nil), + template.NewKVProvider(channel.GuildID(), h.pg, features.MaxKVKeys), ) data := &actions.MessageWithActions{} @@ -85,7 +83,7 @@ func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.M return fmt.Errorf("Failed to parse and execute message template: %w", err) } - params := &discordgo.WebhookParams{ + params := discord.WebhookMessageCreate{ Username: data.Username, AvatarURL: data.AvatarURL, ThreadName: req.ThreadName.String, @@ -98,7 +96,7 @@ func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.M params.TTS = data.TTS } - attachments := make([]*discordgo.MessageAttachment, len(req.Attachments)) + attachments := make([]discord.AttachmentUpdate, len(req.Attachments)) for i, attachment := range req.Attachments { dataURL, err := dataurl.DecodeString(attachment.DataURL) @@ -106,14 +104,14 @@ func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.M return helpers.BadRequest("invalid_attachments", "Failed to parse attachment data URL") } - params.Files = append(params.Files, &discordgo.File{ - Name: attachment.Name, - ContentType: dataURL.ContentType(), - Reader: bytes.NewReader(dataURL.Data), + params.Files = append(params.Files, &discord.File{ + Name: attachment.Name, + // ContentType: dataURL.ContentType(), + Reader: bytes.NewReader(dataURL.Data), }) - attachments[i] = &discordgo.MessageAttachment{ - ID: fmt.Sprintf("%d", i), + attachments[i] = discord.AttachmentKeep{ + ID: util.ID(i), } } @@ -122,9 +120,9 @@ func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.M return helpers.BadRequest("invalid_actions", err.Error()) } - var msg *discordgo.Message + var msg *discord.Message if req.MessageID.Valid { - msg, err = h.bot.EditMessageInChannel(c.Context(), req.ChannelID, req.MessageID.String, &discordgo.WebhookEdit{ + msg, err = h.embedg.UpdateMessageInChannel(c.Context(), util.ToID(req.ChannelID), util.ToID(req.MessageID.String), discord.WebhookMessageUpdate{ Content: ¶ms.Content, Embeds: ¶ms.Embeds, Components: ¶ms.Components, @@ -133,13 +131,13 @@ func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.M Attachments: &attachments, }) } else { - msg, err = h.bot.SendMessageToChannel(c.Context(), req.ChannelID, params) + msg, err = h.embedg.SendMessageToChannel(c.Context(), util.ToID(req.ChannelID), params) } if err != nil { return fmt.Errorf("Failed to send message: %w", err) } - permContext, err := h.actionParser.DerivePermissionsForActions(session.UserID, req.GuildID, req.ChannelID) + permContext, err := h.actionParser.DerivePermissionsForActions(session.UserID, util.ToID(req.GuildID), util.ToID(req.ChannelID)) if err != nil { return fmt.Errorf("Failed to create permission context: %w", err) } @@ -153,8 +151,8 @@ func (h *SendMessageHandler) HandleSendMessageToChannel(c *fiber.Ctx, req wire.M return c.JSON(wire.MessageSendResponseWire{ Success: true, Data: wire.MessageSendResponseDataWire{ - MessageID: msg.ID, - ChannelID: msg.ChannelID, + MessageID: msg.ID.String(), + ChannelID: msg.ChannelID.String(), }, }) } @@ -166,7 +164,7 @@ func (h *SendMessageHandler) HandleSendMessageToWebhook(c *fiber.Ctx, req wire.M return err } - params := &discordgo.WebhookParams{ + params := discord.WebhookMessageCreate{ Username: data.Username, AvatarURL: data.AvatarURL, AllowedMentions: data.AllowedMentions, @@ -178,7 +176,7 @@ func (h *SendMessageHandler) HandleSendMessageToWebhook(c *fiber.Ctx, req wire.M params.TTS = data.TTS } - attachments := make([]*discordgo.MessageAttachment, len(req.Attachments)) + attachments := make([]discord.AttachmentUpdate, len(req.Attachments)) for i, attachment := range req.Attachments { dataURL, err := dataurl.DecodeString(attachment.DataURL) @@ -186,14 +184,14 @@ func (h *SendMessageHandler) HandleSendMessageToWebhook(c *fiber.Ctx, req wire.M return helpers.BadRequest("invalid_attachments", "Failed to parse attachment data URL") } - params.Files = append(params.Files, &discordgo.File{ - Name: attachment.Name, - ContentType: dataURL.ContentType(), - Reader: bytes.NewReader(dataURL.Data), + params.Files = append(params.Files, &discord.File{ + Name: attachment.Name, + // ContentType: dataURL.ContentType(), + Reader: bytes.NewReader(dataURL.Data), }) - attachments[i] = &discordgo.MessageAttachment{ - ID: fmt.Sprintf("%d", i), + attachments[i] = discord.AttachmentKeep{ + ID: util.ID(i), } } @@ -209,33 +207,41 @@ func (h *SendMessageHandler) HandleSendMessageToWebhook(c *fiber.Ctx, req wire.M }) } - var msg *discordgo.Message + var threadID snowflake.ID if req.ThreadID.Valid { - if req.MessageID.Valid { - msg, err = h.bot.Session.WebhookThreadMessageEdit(req.WebhookID, req.WebhookToken, req.ThreadID.String, req.MessageID.String, &discordgo.WebhookEdit{ + threadID = util.ToID(req.ThreadID.String) + } + + var msg *discord.Message + if req.MessageID.Valid { + msg, err = h.embedg.Rest().UpdateWebhookMessage( + util.ToID(req.WebhookID), + req.WebhookToken, + util.ToID(req.MessageID.String), + discord.WebhookMessageUpdate{ Content: ¶ms.Content, Embeds: ¶ms.Embeds, Components: ¶ms.Components, AllowedMentions: params.AllowedMentions, Files: params.Files, Attachments: &attachments, - }) - } else { - msg, err = h.bot.Session.WebhookThreadExecute(req.WebhookID, req.WebhookToken, true, req.ThreadID.String, params) - } + }, + rest.UpdateWebhookMessageParams{ + ThreadID: threadID, + WithComponents: false, + }, + ) } else { - if req.MessageID.Valid { - msg, err = h.bot.Session.WebhookMessageEdit(req.WebhookID, req.WebhookToken, req.MessageID.String, &discordgo.WebhookEdit{ - Content: ¶ms.Content, - Embeds: ¶ms.Embeds, - Components: ¶ms.Components, - AllowedMentions: params.AllowedMentions, - Files: params.Files, - Attachments: &attachments, - }) - } else { - msg, err = h.bot.Session.WebhookExecute(req.WebhookID, req.WebhookToken, true, params) - } + msg, err = h.embedg.Rest().CreateWebhookMessage( + util.ToID(req.WebhookID), + req.WebhookToken, + params, + rest.CreateWebhookMessageParams{ + Wait: true, + ThreadID: threadID, + WithComponents: false, + }, + ) } if err != nil { return err @@ -244,8 +250,8 @@ func (h *SendMessageHandler) HandleSendMessageToWebhook(c *fiber.Ctx, req wire.M return c.JSON(wire.MessageSendResponseWire{ Success: true, Data: wire.MessageSendResponseDataWire{ - MessageID: msg.ID, - ChannelID: msg.ChannelID, + MessageID: msg.ID.String(), + ChannelID: msg.ChannelID.String(), }, }) } diff --git a/embedg-server/api/handlers/send_message/restore.go b/embedg-server/api/handlers/send_message/restore.go index a7b78a161..3679d8550 100644 --- a/embedg-server/api/handlers/send_message/restore.go +++ b/embedg-server/api/handlers/send_message/restore.go @@ -7,21 +7,23 @@ import ( "net/http" "strings" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" "github.com/gofiber/fiber/v2" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/api/wire" + "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/vincent-petithory/dataurl" "gopkg.in/guregu/null.v4" ) func (h *SendMessageHandler) HandleRestoreMessageFromChannel(c *fiber.Ctx, req wire.MessageRestoreFromChannelRequestWire) error { - if err := h.accessManager.CheckChannelAccessForRequest(c, req.ChannelID); err != nil { + if err := h.accessManager.CheckChannelAccessForRequest(c, util.ToID(req.ChannelID)); err != nil { return err } // We don't use a webhook here because we don't need to, but this means that some restored messages can't actually be edited - msg, err := h.bot.Session.ChannelMessage(req.ChannelID, req.MessageID) + msg, err := h.embedg.Rest().GetMessage(util.ToID(req.ChannelID), util.ToID(req.MessageID), rest.WithCtx(c.Context())) if err != nil { return fmt.Errorf("Failed to get message: %w", err) } @@ -31,7 +33,7 @@ func (h *SendMessageHandler) HandleRestoreMessageFromChannel(c *fiber.Ctx, req w return fmt.Errorf("Failed to unparse message components: %w", err) } - actionSets, err := h.actionParser.RetrieveActionsForMessage(c.Context(), req.MessageID) + actionSets, err := h.actionParser.RetrieveActionsForMessage(c.Context(), util.ToID(req.MessageID)) if err != nil { return fmt.Errorf("Failed to retrieve actions for message: %w", err) } @@ -39,7 +41,7 @@ func (h *SendMessageHandler) HandleRestoreMessageFromChannel(c *fiber.Ctx, req w data := &actions.MessageWithActions{ Content: msg.Content, Username: msg.Author.Username, - AvatarURL: msg.Author.AvatarURL(""), + AvatarURL: msg.Author.EffectiveAvatarURL(discord.WithSize(512)), Embeds: msg.Embeds, Components: components, Actions: actionSets, @@ -62,13 +64,14 @@ func (h *SendMessageHandler) HandleRestoreMessageFromChannel(c *fiber.Ctx, req w } func (h *SendMessageHandler) HandleRestoreMessageFromWebhook(c *fiber.Ctx, req wire.MessageRestoreFromWebhookRequestWire) error { - var msg *discordgo.Message - var err error + reqOpts := []rest.RequestOpt{ + rest.WithCtx(c.Context()), + } if req.ThreadID.Valid { - msg, err = h.bot.Session.WebhookThreadMessage(req.WebhookID, req.WebhookToken, req.ThreadID.String, req.MessageID) - } else { - msg, err = h.bot.Session.WebhookMessage(req.WebhookID, req.WebhookToken, req.MessageID) + reqOpts = append(reqOpts, rest.WithQueryParam("thread_id", req.ThreadID.String)) } + + msg, err := h.embedg.Rest().GetWebhookMessage(util.ToID(req.WebhookID), req.WebhookToken, util.ToID(req.MessageID), reqOpts...) if err != nil { return err } @@ -76,7 +79,7 @@ func (h *SendMessageHandler) HandleRestoreMessageFromWebhook(c *fiber.Ctx, req w data := &actions.MessageWithActions{ Content: msg.Content, Username: msg.Author.Username, - AvatarURL: msg.Author.AvatarURL(""), + AvatarURL: msg.Author.EffectiveAvatarURL(discord.WithSize(512)), Embeds: msg.Embeds, } @@ -98,18 +101,24 @@ func (h *SendMessageHandler) HandleRestoreMessageFromWebhook(c *fiber.Ctx, req w }) } -func downloadMessageAttachments(attachments []*discordgo.MessageAttachment) (files []*wire.MessageAttachmentWire) { +func downloadMessageAttachments(attachments []discord.Attachment) (files []*wire.MessageAttachmentWire) { filesC := make(chan *wire.MessageAttachmentWire) // TODO: can this block forever? for _, attachment := range attachments { - go func(attachment *discordgo.MessageAttachment) { + go func(attachment discord.Attachment) { if attachment.Size > 8*1024*1024 { filesC <- nil return } + // We don't know what to do with attachments without a content type + if attachment.ContentType == nil { + filesC <- nil + return + } + resp, err := http.Get(attachment.URL) if err != nil { filesC <- nil @@ -122,13 +131,13 @@ func downloadMessageAttachments(attachments []*discordgo.MessageAttachment) (fil return } - parts := strings.Split(attachment.ContentType, "/") + parts := strings.Split(*attachment.ContentType, "/") if len(parts) != 2 { filesC <- nil return } - dataURL := dataurl.New(body, attachment.ContentType) + dataURL := dataurl.New(body, *attachment.ContentType) filesC <- &wire.MessageAttachmentWire{ Name: attachment.Filename, Description: null.String{}, diff --git a/embedg-server/api/managers.go b/embedg-server/api/managers.go index 9aa518b74..dea8c8bb6 100644 --- a/embedg-server/api/managers.go +++ b/embedg-server/api/managers.go @@ -34,8 +34,7 @@ func createManagers(stores *Stores, embedg *embedg.EmbedGenerator) *managers { scheduledMessages := scheduled_messages.NewScheduledMessageManager( stores.PG, actionParser, - embedg.Caches(), - embedg.Rest(), + embedg, premiumManager, ) diff --git a/embedg-server/api/routes.go b/embedg-server/api/routes.go index 4208648e8..eb0933bc6 100644 --- a/embedg-server/api/routes.go +++ b/embedg-server/api/routes.go @@ -74,8 +74,7 @@ func registerRoutes(app *fiber.App, stores *Stores, embedg *embedg.EmbedGenerato guildsGroup.Get("/:guildID/branding", guildsHanlder.HandleGetGuildBranding) sendMessageHandler := send_message.New( - embedg.Caches(), - embedg.Rest(), + embedg, stores.PG, managers.access, managers.actionParser, @@ -93,7 +92,7 @@ func registerRoutes(app *fiber.App, stores *Stores, embedg *embedg.EmbedGenerato customBotHandler := custom_bots.New( stores.PG, - embedg.Caches(), + embedg, managers.access, managers.premium, managers.actionParser, @@ -110,7 +109,7 @@ func registerRoutes(app *fiber.App, stores *Stores, embedg *embedg.EmbedGenerato app.Post("/api/custom-bot/commands/deploy", sessionMiddleware.SessionRequired(), customBotHandler.HandleDeployCustomCommands) app.Post("/api/gateway/:customBotID", customBotHandler.HandleCustomBotInteraction) - interactionHandler := interaction.New(embedg.Caches(), embedg.Rest(), managers.actionHandler) + interactionHandler := interaction.New(embedg) app.Post("/api/gateway", interactionHandler.HandleBotInteraction) imagesHandler := images.New(stores.PG, managers.access, managers.premium, stores.Blob) diff --git a/embedg-server/custom_bots/bot.go b/embedg-server/custom_bots/bot.go index d4270b843..dddc249b3 100644 --- a/embedg-server/custom_bots/bot.go +++ b/embedg-server/custom_bots/bot.go @@ -1,56 +1,80 @@ package custom_bots import ( + "context" "fmt" + "log/slog" + "strconv" + "time" - "github.com/merlinfuchs/discordgo" + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/disgo/sharding" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" "github.com/rs/zerolog/log" "gopkg.in/guregu/null.v4" ) type CustomBot struct { Presence CustomBotPresence `json:"status"` - Session *discordgo.Session + Client *bot.Client } func NewCustomBot(token string, presence CustomBotPresence) (*CustomBot, error) { - session, err := discordgo.New("Bot " + token) + client, err := disgo.New(token, + bot.WithShardManagerConfigOpts( + sharding.WithAutoScaling(false), + sharding.WithGatewayConfigOpts( + gateway.WithIntents(), + gateway.WithPresenceOpts(presence.PresenceOpts()...), + ), + ), + bot.WithEventManagerConfigOpts( + bot.WithAsyncEventsEnabled(), + ), + bot.WithRestClient(rest.NewRestClient(token)), + bot.WithCacheConfigOpts( + cache.WithCaches(), + ), + bot.WithEventListenerFunc(func(e *events.Ready) { + slog.Info( + "Shard is ready", + slog.String("shard_id", strconv.Itoa(e.ShardID())), + slog.String("user_id", e.User.ID.String()), + slog.String("username", e.User.Username), + ) + }), + ) if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } - session.StateEnabled = false - session.Identify.Intents = 0 - session.Identify.Presence = discordgo.GatewayStatusUpdate{ - Status: presence.Status, - Game: presence.Activity(), - } - session.SyncEvents = true - session.ShouldReconnectOnError = false - - err = session.Open() + err = client.OpenShardManager(context.Background()) if err != nil { return nil, fmt.Errorf("failed to open session: %w", err) } bot := &CustomBot{ Presence: presence, - Session: session, + Client: client, } return bot, nil } func (b *CustomBot) UpdatePresence(p CustomBotPresence) { - if b.Session == nil { + if b.Client == nil { return } - activity := p.Activity() - err := b.Session.UpdateStatusComplex(discordgo.UpdateStatusData{ - Status: p.Status, - Activities: []*discordgo.Activity{&activity}, - }) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := b.Client.SetPresence(ctx, p.PresenceOpts()...) if err != nil { log.Error().Err(err).Msg("Failed to update custom bot presence") } else { @@ -66,11 +90,32 @@ type CustomBotPresence struct { ActivityURL null.String `json:"activity_url"` } -func (p CustomBotPresence) Activity() discordgo.Activity { - return discordgo.Activity{ - Type: discordgo.ActivityType(p.ActivityType.Int64), - Name: p.ActivityName.String, - State: p.ActivityState.String, - URL: p.ActivityURL.String, +func (p CustomBotPresence) PresenceOpts() []gateway.PresenceOpt { + + opts := []gateway.PresenceOpt{ + gateway.WithOnlineStatus(discord.OnlineStatus(p.Status)), } + if p.ActivityType.Valid { + extraOpts := []gateway.ActivityOpt{} + if p.ActivityState.Valid { + extraOpts = append(extraOpts, gateway.WithActivityState(p.ActivityState.String)) + } + + switch discord.ActivityType(p.ActivityType.Int64) { + case discord.ActivityTypeCustom: + opts = append(opts, gateway.WithCustomActivity(p.ActivityName.String, extraOpts...)) + case discord.ActivityTypeGame: + opts = append(opts, gateway.WithPlayingActivity(p.ActivityName.String, extraOpts...)) + case discord.ActivityTypeWatching: + opts = append(opts, gateway.WithWatchingActivity(p.ActivityName.String, extraOpts...)) + case discord.ActivityTypeStreaming: + opts = append(opts, gateway.WithStreamingActivity(p.ActivityName.String, p.ActivityURL.String, extraOpts...)) + case discord.ActivityTypeListening: + opts = append(opts, gateway.WithListeningActivity(p.ActivityName.String, extraOpts...)) + case discord.ActivityTypeCompeting: + opts = append(opts, gateway.WithCompetingActivity(p.ActivityName.String, extraOpts...)) + } + } + + return opts } diff --git a/embedg-server/custom_bots/manager.go b/embedg-server/custom_bots/manager.go index d186f0aef..479decc13 100644 --- a/embedg-server/custom_bots/manager.go +++ b/embedg-server/custom_bots/manager.go @@ -5,8 +5,9 @@ import ( "errors" "time" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" "github.com/gorilla/websocket" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" @@ -56,14 +57,14 @@ func (m *CustomBotManager) lazyCustomBotGatewayTask() { ActivityURL: null.String{NullString: customBot.GatewayActivityUrl}, } - if bot, ok := m.bots[customBot.ID]; ok { - if bot.Presence != presence { - bot.UpdatePresence(presence) + if instance, ok := m.bots[customBot.ID]; ok { + if instance.Presence != presence { + instance.UpdatePresence(presence) } continue } - bot, err := NewCustomBot(customBot.Token, presence) + instance, err := NewCustomBot(customBot.Token, presence) if err != nil { var wsErr *websocket.CloseError if errors.As(err, &wsErr) && wsErr.Code == 4004 { @@ -81,22 +82,23 @@ func (m *CustomBotManager) lazyCustomBotGatewayTask() { continue } - bot.Session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + instance.Client.AddEventListeners(bot.NewListenerFunc(func(i *events.InteractionCreate) { err = m.pg.Q.SetCustomBotHandledFirstInteraction(context.Background(), customBot.ID) if err != nil { log.Error().Err(err).Msg("Failed to set custom bot handled first interaction") } - err := m.actionHandler.HandleActionInteraction(s, &handler.GatewayInteraction{ - Session: s, - Inner: i.Interaction, + err := m.actionHandler.HandleActionInteraction(instance.Client, &handler.GatewayInteraction{ + Rest: instance.Client.Rest, + Inner: i.Interaction, }) if err != nil { log.Error().Err(err).Msg("Failed to handle action interaction from custom bot gateway") } - }) + })) - bot.Session.AddHandler(func(s *discordgo.Session, i *discordgo.Disconnect) { + // TODO: Re-implement token reset detection + /* instance.Client.AddEventListeners(bot.NewListenerFunc(func(i *events.Disconnect) { // Normally DiscordGo would handle reconnection, but it doesn't have any logic to detect a token reset and will just keep trying to reconnect with the old token // We only make a single reconnect attempt, if that fails we hand it off to the background task to spawn a new session // The background task will detect if the token is invalid and mark the custom bot accordingly @@ -107,9 +109,9 @@ func (m *CustomBotManager) lazyCustomBotGatewayTask() { delete(m.bots, customBot.ID) } - }) + })) */ - m.bots[customBot.ID] = bot + m.bots[customBot.ID] = instance newBots++ } diff --git a/embedg-server/embedg/commands.go b/embedg-server/embedg/commands.go index b6299716b..f426c4934 100644 --- a/embedg-server/embedg/commands.go +++ b/embedg-server/embedg/commands.go @@ -6,7 +6,9 @@ import ( "log/slog" "regexp" + "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/handler" "github.com/disgoorg/disgo/handler/middleware" "github.com/disgoorg/disgo/rest" @@ -14,6 +16,7 @@ import ( "github.com/disgoorg/snowflake/v2" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" + actionhandler "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/spf13/viper" ) @@ -240,6 +243,13 @@ func (g *EmbedGenerator) SyncCommands() error { } func (g *EmbedGenerator) registerHandlers() { + g.client.AddEventListeners(bot.NewListenerFunc(func(e *events.InteractionCreate) { + g.HandleInteraction(&actionhandler.GatewayInteraction{ + Rest: g.client.Rest, + Inner: e.Interaction, + }) + })) + r := g.clientRouter r.Use(middleware.Logger) @@ -540,5 +550,6 @@ func (g *EmbedGenerator) handleUserFormatMentionContextCommand(e *handler.Comman } func (g *EmbedGenerator) handleEmbedCommand(e *handler.CommandEvent) error { + // TODO: Implement with components return nil } diff --git a/embedg-server/embedg/events.go b/embedg-server/embedg/events.go new file mode 100644 index 000000000..f9f4599f2 --- /dev/null +++ b/embedg-server/embedg/events.go @@ -0,0 +1,7 @@ +package embedg + +import ( + "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" +) + +func (g *EmbedGenerator) HandleInteraction(i handler.Interaction) {} diff --git a/embedg-server/embedg/helpers.go b/embedg-server/embedg/helpers.go index 2ec535863..9ed72680c 100644 --- a/embedg-server/embedg/helpers.go +++ b/embedg-server/embedg/helpers.go @@ -1,10 +1,24 @@ package embedg import ( + "context" "fmt" "strings" + + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-server/util" ) +func (g *EmbedGenerator) SendMessageToChannel(ctx context.Context, channelID util.ID, params discord.WebhookMessageCreate) (*discord.Message, error) { + // TODO: Implement + return nil, nil +} + +func (g *EmbedGenerator) UpdateMessageInChannel(ctx context.Context, channelID util.ID, messageID util.ID, params discord.WebhookMessageUpdate) (*discord.Message, error) { + // TODO: Implement + return nil, nil +} + func emojiImageURL(emoji string, animated bool) string { // Convert unicode emoji to Twemoji URL var codepoints []string diff --git a/embedg-server/scheduled_messages/manager.go b/embedg-server/scheduled_messages/manager.go index 9d566d6bb..652ac3655 100644 --- a/embedg-server/scheduled_messages/manager.go +++ b/embedg-server/scheduled_messages/manager.go @@ -7,15 +7,14 @@ import ( "fmt" "time" - "github.com/disgoorg/disgo/cache" - "github.com/disgoorg/disgo/rest" - "github.com/merlinfuchs/discordgo" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/actions/template" "github.com/merlinfuchs/embed-generator/embedg-server/api/helpers" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-server/embedg" "github.com/merlinfuchs/embed-generator/embedg-server/store" "github.com/merlinfuchs/embed-generator/embedg-server/util" "github.com/rs/zerolog/log" @@ -23,8 +22,7 @@ import ( type ScheduledMessageManager struct { pg *postgres.PostgresStore - caches cache.Caches - rest rest.Rest + embedg *embedg.EmbedGenerator actionParser *parser.ActionParser planStore store.PlanStore } @@ -32,14 +30,12 @@ type ScheduledMessageManager struct { func NewScheduledMessageManager( pg *postgres.PostgresStore, actionParser *parser.ActionParser, - caches cache.Caches, - rest rest.Rest, + embedg *embedg.EmbedGenerator, planStore store.PlanStore, ) *ScheduledMessageManager { m := &ScheduledMessageManager{ pg: pg, - caches: caches, - rest: rest, + embedg: embedg, actionParser: actionParser, planStore: planStore, } @@ -126,9 +122,9 @@ func (m *ScheduledMessageManager) SendScheduledMessage(ctx context.Context, sche templates := template.NewContext( "SCHEDULED_MESSAGE", features.MaxTemplateOps, - template.NewGuildProvider(m.caches, scheduledMessage.GuildID, nil), - template.NewChannelProvider(m.caches, scheduledMessage.ChannelID, nil), - template.NewKVProvider(scheduledMessage.GuildID, m.pg, features.MaxKVKeys), + template.NewGuildProvider(m.embedg.Caches(), util.ToID(scheduledMessage.GuildID), nil), + template.NewChannelProvider(m.embedg.Caches(), util.ToID(scheduledMessage.ChannelID), nil), + template.NewKVProvider(util.ToID(scheduledMessage.GuildID), m.pg, features.MaxKVKeys), ) data := &actions.MessageWithActions{} @@ -141,7 +137,7 @@ func (m *ScheduledMessageManager) SendScheduledMessage(ctx context.Context, sche return fmt.Errorf("Failed to parse and execute message template: %w", err) } - params := &discordgo.WebhookParams{ + params := discord.WebhookMessageCreate{ Content: data.Content, Username: data.Username, AvatarURL: data.AvatarURL, @@ -157,12 +153,12 @@ func (m *ScheduledMessageManager) SendScheduledMessage(ctx context.Context, sche return helpers.BadRequest("invalid_actions", err.Error()) } - msg, err := m.bot.SendMessageToChannel(ctx, scheduledMessage.ChannelID, params) + msg, err := m.embedg.SendMessageToChannel(ctx, util.ToID(scheduledMessage.ChannelID), params) if err != nil { return fmt.Errorf("Failed to send message: %w", err) } - permContext, err := m.actionParser.DerivePermissionsForActions(scheduledMessage.CreatorID, scheduledMessage.GuildID, scheduledMessage.ChannelID) + permContext, err := m.actionParser.DerivePermissionsForActions(util.ToID(scheduledMessage.CreatorID), util.ToID(scheduledMessage.GuildID), util.ToID(scheduledMessage.ChannelID)) if err != nil { return fmt.Errorf("Failed to create permission context: %w", err) } diff --git a/embedg-server/util/guilded.go b/embedg-server/util/guilded.go index 868ab100f..1cad8646e 100644 --- a/embedg-server/util/guilded.go +++ b/embedg-server/util/guilded.go @@ -6,10 +6,11 @@ import ( "io" "net/http" + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/discordgo" ) -func ExecuteGuildedWebhook(webhookID, webhookToken string, params *discordgo.WebhookParams) error { +func ExecuteGuildedWebhook(webhookID, webhookToken string, params discord.WebhookMessageCreate) error { webhookURL := fmt.Sprintf("https://media.guilded.gg/webhooks/%s/%s", webhookID, webhookToken) files := params.Files From c32a1ab78be2d0aead1f8f3ba63089c67d16f942 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Mon, 27 Oct 2025 15:02:01 +0100 Subject: [PATCH 4/8] refactor action parser to use disgo --- embedg-server/actions/data.go | 31 ++-- embedg-server/actions/parser/parse.go | 201 ++++++++++++-------------- embedg-server/custom_bots/manager.go | 2 +- embedg-server/util/guilded.go | 14 +- 4 files changed, 120 insertions(+), 128 deletions(-) diff --git a/embedg-server/actions/data.go b/embedg-server/actions/data.go index 55d6f386f..b3c82860c 100644 --- a/embedg-server/actions/data.go +++ b/embedg-server/actions/data.go @@ -4,7 +4,6 @@ import ( "slices" "github.com/disgoorg/disgo/discord" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/util" ) @@ -25,20 +24,20 @@ func (m MessageWithActions) ComponentsV2Enabled() bool { } type ComponentWithActions struct { - ID int `json:"id,omitempty"` - Type discordgo.ComponentType `json:"type"` - Disabled bool `json:"disabled,omitempty"` - Spoiler bool `json:"spoiler,omitempty"` + ID int `json:"id,omitempty"` + Type discord.ComponentType `json:"type"` + Disabled bool `json:"disabled,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` // Action Row & Section & Container Components []ComponentWithActions `json:"components,omitempty"` // Button - Style discordgo.ButtonStyle `json:"style,omitempty"` - Label string `json:"label,omitempty"` - Emoji *discordgo.ComponentEmoji `json:"emoji,omitempty"` - URL string `json:"url,omitempty"` - ActionSetID string `json:"action_set_id,omitempty"` + Style discord.ButtonStyle `json:"style,omitempty"` + Label string `json:"label,omitempty"` + Emoji *discord.ComponentEmoji `json:"emoji,omitempty"` + URL string `json:"url,omitempty"` + ActionSetID string `json:"action_set_id,omitempty"` // Select Menu Placeholder string `json:"placeholder,omitempty"` @@ -75,11 +74,11 @@ type UnfurledMediaItem struct { } type ComponentSelectOptionWithActions struct { - Label string `json:"label"` - Description string `json:"description"` - Emoji *discordgo.ComponentEmoji `json:"emoji"` - Default bool `json:"default"` - ActionSetID string `json:"action_set_id"` + Label string `json:"label"` + Description string `json:"description"` + Emoji *discord.ComponentEmoji `json:"emoji"` + Default bool `json:"default"` + ActionSetID string `json:"action_set_id"` } type ComponentMediaGalleryItem struct { @@ -139,5 +138,5 @@ func (a *ActionDerivedPermissions) CanManageRole(roleID util.ID) bool { return true } - return a.HasGuildPermission(discordgo.PermissionManageRoles) && slices.Contains(a.AllowedRoleIDs, roleID) + return a.HasGuildPermission(discord.PermissionManageRoles) && slices.Contains(a.AllowedRoleIDs, roleID) } diff --git a/embedg-server/actions/parser/parse.go b/embedg-server/actions/parser/parse.go index f8f4929d2..9d7e271cc 100644 --- a/embedg-server/actions/parser/parse.go +++ b/embedg-server/actions/parser/parse.go @@ -8,7 +8,6 @@ import ( "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" @@ -38,22 +37,27 @@ func (m *ActionParser) ParseMessageComponents(data []actions.ComponentWithAction return nil, err } - components = append(components, parsed) + // Convert to LayoutComponent + if layoutComp, ok := parsed.(discord.LayoutComponent); ok { + components = append(components, layoutComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as LayoutComponent", parsed) + } } return components, nil } -func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, allowedComponentTypes []int) (discordgo.MessageComponent, error) { +func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, allowedComponentTypes []int) (discord.Component, error) { if !slices.Contains(allowedComponentTypes, int(data.Type)) { return nil, fmt.Errorf("component type %d not allowed, you need to upgrade to a premium plan to use this component", data.Type) } switch data.Type { - case discordgo.ActionsRowComponent: - ar := discordgo.ActionsRow{ + case discord.ComponentTypeActionRow: + ar := discord.ActionRowComponent{ ID: data.ID, - Components: make([]discordgo.MessageComponent, 0, len(data.Components)), + Components: make([]discord.InteractiveComponent, 0, len(data.Components)), } for _, component := range data.Components { @@ -62,13 +66,17 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, return nil, err } - ar.Components = append(ar.Components, parsed) + if interactiveComp, ok := parsed.(discord.InteractiveComponent); ok { + ar.Components = append(ar.Components, interactiveComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as InteractiveComponent", parsed) + } } return ar, nil - case discordgo.ButtonComponent: - if data.Style == discordgo.LinkButton { - return discordgo.Button{ + case discord.ComponentTypeButton: + if data.Style == discord.ButtonStyleLink { + return discord.ButtonComponent{ Label: data.Label, Style: data.Style, Disabled: data.Disabled, @@ -76,7 +84,7 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, Emoji: data.Emoji, }, nil } else { - return discordgo.Button{ + return discord.ButtonComponent{ CustomID: "action:" + data.ActionSetID, Label: data.Label, Style: data.Style, @@ -84,10 +92,10 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, Emoji: data.Emoji, }, nil } - case discordgo.SelectMenuComponent: - options := make([]discordgo.SelectMenuOption, len(data.Options)) + case discord.ComponentTypeStringSelectMenu: + options := make([]discord.StringSelectMenuOption, len(data.Options)) for x, option := range data.Options { - options[x] = discordgo.SelectMenuOption{ + options[x] = discord.StringSelectMenuOption{ Label: option.Label, Value: "action:" + option.ActionSetID, Description: option.Description, @@ -96,8 +104,7 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, } } - return discordgo.SelectMenu{ - MenuType: discordgo.StringSelectMenu, + return discord.StringSelectMenuComponent{ CustomID: "action:options:" + util.UniqueID(), Placeholder: data.Placeholder, MinValues: data.MinValues, @@ -105,9 +112,9 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, Options: options, Disabled: data.Disabled, }, nil - case discordgo.SectionComponent: - se := discordgo.Section{ - Components: make([]discordgo.MessageComponent, 0, len(data.Components)), + case discord.ComponentTypeSection: + se := discord.SectionComponent{ + Components: make([]discord.SectionSubComponent, 0, len(data.Components)), } for _, component := range data.Components { @@ -116,7 +123,11 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, return nil, err } - se.Components = append(se.Components, parsed) + if sectionSubComp, ok := parsed.(discord.SectionSubComponent); ok { + se.Components = append(se.Components, sectionSubComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as SectionSubComponent", parsed) + } } if data.Accessory != nil { @@ -124,76 +135,64 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, if err != nil { return nil, err } - se.Accessory = parsed + if sectionAccessoryComp, ok := parsed.(discord.SectionAccessoryComponent); ok { + se.Accessory = sectionAccessoryComp + } else { + return nil, fmt.Errorf("component type %T cannot be used as SectionAccessoryComponent", parsed) + } } return se, nil - case discordgo.TextDisplayComponent: - return discordgo.TextDisplay{ + case discord.ComponentTypeTextDisplay: + return discord.TextDisplayComponent{ Content: data.Content, }, nil - case discordgo.ThumbnailComponent: + case discord.ComponentTypeThumbnail: if data.Media == nil { return nil, errors.New("media is required for thumbnail component") } - var description *string - if data.Description != "" { - description = &data.Description - } - - return discordgo.Thumbnail{ - Media: discordgo.UnfurledMediaItem{URL: data.Media.URL}, - Description: description, + return discord.ThumbnailComponent{ + Media: discord.UnfurledMediaItem{URL: data.Media.URL}, + Description: data.Description, Spoiler: data.Spoiler, }, nil - case discordgo.MediaGalleryComponent: - items := make([]discordgo.MediaGalleryItem, len(data.Items)) + case discord.ComponentTypeMediaGallery: + items := make([]discord.MediaGalleryItem, len(data.Items)) for x, item := range data.Items { - var description *string - if item.Description != "" { - description = &item.Description - } - - items[x] = discordgo.MediaGalleryItem{ - Media: discordgo.UnfurledMediaItem{URL: item.Media.URL}, - Description: description, + items[x] = discord.MediaGalleryItem{ + Media: discord.UnfurledMediaItem{URL: item.Media.URL}, + Description: item.Description, Spoiler: item.Spoiler, } } - return discordgo.MediaGallery{ + return discord.MediaGalleryComponent{ Items: items, }, nil - case discordgo.FileComponentType: + case discord.ComponentTypeFile: if data.File == nil { return nil, errors.New("file is required for file component") } - return discordgo.FileComponent{ - File: discordgo.UnfurledMediaItem{URL: data.File.URL}, + return discord.FileComponent{ + File: discord.UnfurledMediaItem{URL: data.File.URL}, Spoiler: data.Spoiler, }, nil - case discordgo.SeparatorComponent: - var spacing *discordgo.SeparatorSpacingSize - if data.Spacing != 0 { - s := discordgo.SeparatorSpacingSize(data.Spacing) - spacing = &s + case discord.ComponentTypeSeparator: + var divider *bool + if data.Divider { + divider = &data.Divider } - return discordgo.Separator{ - Divider: &data.Divider, - Spacing: spacing, + return discord.SeparatorComponent{ + Divider: divider, + Spacing: discord.SeparatorSpacingSize(data.Spacing), }, nil - case discordgo.ContainerComponent: - var accentColor *int - if data.AccentColor != 0 { - accentColor = &data.AccentColor - } - - c := discordgo.Container{ - Components: make([]discordgo.MessageComponent, 0, len(data.Components)), - AccentColor: accentColor, + case discord.ComponentTypeContainer: + c := discord.ContainerComponent{ + Components: make([]discord.ContainerSubComponent, 0, len(data.Components)), + AccentColor: data.AccentColor, Spoiler: data.Spoiler, } @@ -203,7 +202,11 @@ func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, return nil, err } - c.Components = append(c.Components, parsed) + if containerSubComp, ok := parsed.(discord.ContainerSubComponent); ok { + c.Components = append(c.Components, containerSubComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as ContainerSubComponent", parsed) + } } return c, nil @@ -226,11 +229,11 @@ func (m *ActionParser) UnparseMessageComponents(data []discord.LayoutComponent) return res, nil } -func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (actions.ComponentWithActions, error) { +func (m *ActionParser) UnparseMessageComponent(data discord.Component) (actions.ComponentWithActions, error) { switch c := data.(type) { - case *discordgo.ActionsRow: + case discord.ActionRowComponent: ar := actions.ComponentWithActions{ - Type: discordgo.ActionsRowComponent, + Type: discord.ComponentTypeActionRow, Components: make([]actions.ComponentWithActions, 0, len(c.Components)), } @@ -243,9 +246,9 @@ func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (ac } return ar, nil - case *discordgo.Button: + case discord.ButtonComponent: return actions.ComponentWithActions{ - Type: discordgo.ButtonComponent, + Type: discord.ComponentTypeButton, Disabled: c.Disabled, Style: c.Style, Label: c.Label, @@ -253,7 +256,7 @@ func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (ac URL: c.URL, ActionSetID: strings.TrimPrefix(c.CustomID, "action:"), }, nil - case *discordgo.SelectMenu: + case discord.StringSelectMenuComponent: options := make([]actions.ComponentSelectOptionWithActions, 0, len(c.Options)) for _, option := range c.Options { options = append(options, actions.ComponentSelectOptionWithActions{ @@ -266,16 +269,16 @@ func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (ac } return actions.ComponentWithActions{ - Type: discordgo.SelectMenuComponent, + Type: discord.ComponentTypeStringSelectMenu, Disabled: c.Disabled, Placeholder: c.Placeholder, MinValues: c.MinValues, MaxValues: c.MaxValues, Options: options, }, nil - case *discordgo.Section: + case discord.SectionComponent: se := actions.ComponentWithActions{ - Type: discordgo.SectionComponent, + Type: discord.ComponentTypeSection, Components: make([]actions.ComponentWithActions, 0, len(c.Components)), } @@ -296,64 +299,49 @@ func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (ac } return se, nil - case *discordgo.TextDisplay: + case discord.TextDisplayComponent: return actions.ComponentWithActions{ - Type: discordgo.TextDisplayComponent, + Type: discord.ComponentTypeTextDisplay, Content: c.Content, }, nil - case *discordgo.Thumbnail: - description := "" - if c.Description != nil { - description = *c.Description - } - + case discord.ThumbnailComponent: return actions.ComponentWithActions{ - Type: discordgo.ThumbnailComponent, + Type: discord.ComponentTypeThumbnail, Media: &actions.UnfurledMediaItem{URL: c.Media.URL}, - Description: description, + Description: c.Description, }, nil - case *discordgo.MediaGallery: + case discord.MediaGalleryComponent: items := make([]actions.ComponentMediaGalleryItem, 0, len(c.Items)) for _, item := range c.Items { - var description string - if item.Description != nil { - description = *item.Description - } - items = append(items, actions.ComponentMediaGalleryItem{ Media: actions.UnfurledMediaItem{URL: item.Media.URL}, - Description: description, + Description: item.Description, Spoiler: item.Spoiler, }) } return actions.ComponentWithActions{ - Type: discordgo.MediaGalleryComponent, + Type: discord.ComponentTypeMediaGallery, Items: items, }, nil - case *discordgo.FileComponent: + case discord.FileComponent: return actions.ComponentWithActions{ - Type: discordgo.FileComponentType, + Type: discord.ComponentTypeFile, File: &actions.UnfurledMediaItem{URL: c.File.URL}, Spoiler: c.Spoiler, }, nil - case *discordgo.Separator: + case discord.SeparatorComponent: var divider bool if c.Divider != nil { divider = *c.Divider } - var spacing int - if c.Spacing != nil { - spacing = int(*c.Spacing) - } - return actions.ComponentWithActions{ - Type: discordgo.SeparatorComponent, + Type: discord.ComponentTypeSeparator, Divider: divider, - Spacing: spacing, + Spacing: int(c.Spacing), }, nil - case *discordgo.Container: + case discord.ContainerComponent: components := make([]actions.ComponentWithActions, 0, len(c.Components)) for _, comp := range c.Components { parsed, err := m.UnparseMessageComponent(comp) @@ -363,15 +351,10 @@ func (m *ActionParser) UnparseMessageComponent(data discord.LayoutComponent) (ac components = append(components, parsed) } - var accentColor int - if c.AccentColor != nil { - accentColor = *c.AccentColor - } - return actions.ComponentWithActions{ - Type: discordgo.ContainerComponent, + Type: discord.ComponentTypeContainer, Components: components, - AccentColor: accentColor, + AccentColor: c.AccentColor, Spoiler: c.Spoiler, }, nil default: diff --git a/embedg-server/custom_bots/manager.go b/embedg-server/custom_bots/manager.go index 479decc13..db7d7a8f3 100644 --- a/embedg-server/custom_bots/manager.go +++ b/embedg-server/custom_bots/manager.go @@ -88,7 +88,7 @@ func (m *CustomBotManager) lazyCustomBotGatewayTask() { log.Error().Err(err).Msg("Failed to set custom bot handled first interaction") } - err := m.actionHandler.HandleActionInteraction(instance.Client, &handler.GatewayInteraction{ + err := m.actionHandler.HandleActionInteraction(instance.Client.Rest, &handler.GatewayInteraction{ Rest: instance.Client.Rest, Inner: i.Interaction, }) diff --git a/embedg-server/util/guilded.go b/embedg-server/util/guilded.go index 1cad8646e..fbec336da 100644 --- a/embedg-server/util/guilded.go +++ b/embedg-server/util/guilded.go @@ -14,9 +14,19 @@ func ExecuteGuildedWebhook(webhookID, webhookToken string, params discord.Webhoo webhookURL := fmt.Sprintf("https://media.guilded.gg/webhooks/%s/%s", webhookID, webhookToken) files := params.Files - params.Files = make([]*discordgo.File, 0) + params.Files = make([]*discord.File, 0) + + // Convert discord.File to discordgo.File + discordgoFiles := make([]*discordgo.File, 0, len(files)) + for _, file := range files { + discordgoFiles = append(discordgoFiles, &discordgo.File{ + Name: file.Name, + ContentType: "", // discord.File doesn't have ContentType + Reader: file.Reader, + }) + } - contentType, body, err := discordgo.MultipartBodyWithJSON(params, files) + contentType, body, err := discordgo.MultipartBodyWithJSON(params, discordgoFiles) if err != nil { return fmt.Errorf("failed to construct request body: %w", err) } From fd662043d44e10ff5672ac24d6764c6c779015fa Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Mon, 27 Oct 2025 16:02:49 +0100 Subject: [PATCH 5/8] refactor action handler to to use disgo --- embedg-server/actions/handler/handle.go | 424 ++++++++++++------------ 1 file changed, 213 insertions(+), 211 deletions(-) diff --git a/embedg-server/actions/handler/handle.go b/embedg-server/actions/handler/handle.go index 4ef39256e..6a09ebf5e 100644 --- a/embedg-server/actions/handler/handle.go +++ b/embedg-server/actions/handler/handle.go @@ -9,12 +9,12 @@ import ( "strconv" "strings" + "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" - "github.com/merlinfuchs/discordgo" + "github.com/disgoorg/snowflake/v2" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/actions/template" - "github.com/merlinfuchs/embed-generator/embedg-server/actions/variables" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" "github.com/merlinfuchs/embed-generator/embedg-server/store" @@ -44,21 +44,25 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e var rawActions []byte var rawDerivedPerms pqtype.NullRawMessage - if interaction.Type == discordgo.InteractionMessageComponent { - data := interaction.MessageComponentData() + if interaction.Type() == discord.InteractionTypeComponent { + compInteraction := interaction.(discord.ComponentInteraction) + data := compInteraction.Data - if !strings.HasPrefix(data.CustomID, "action:") { + if !strings.HasPrefix(data.CustomID(), "action:") { return nil } - actionSetID := data.CustomID[7:] + actionSetID := data.CustomID()[7:] if strings.HasPrefix(actionSetID, "options:") { - actionSetID = data.Values[0][7:] + // Handle select menu values + if selectData, ok := data.(discord.StringSelectMenuInteractionData); ok { + actionSetID = selectData.Values[0][7:] + } } col, err := m.pg.Q.GetMessageActionSet(context.TODO(), pgmodel.GetMessageActionSetParams{ - MessageID: interaction.Message.ID, + MessageID: compInteraction.Message.ID.String(), SetID: actionSetID, }) if err != nil { @@ -71,14 +75,16 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } rawActions = col.Actions rawDerivedPerms = col.DerivedPermissions - } else if interaction.Type == discordgo.InteractionApplicationCommand { - data := interaction.ApplicationCommandData() - fullName := data.Name - for _, opt := range data.Options { - if opt.Type == discordgo.ApplicationCommandOptionSubCommand { + } else if interaction.Type() == discord.InteractionTypeApplicationCommand { + appCommandInteraction := interaction.(discord.ApplicationCommandInteraction) + slashData := appCommandInteraction.SlashCommandInteractionData() + fullName := slashData.CommandName() + for _, opt := range slashData.All() { + if opt.Type == discord.ApplicationCommandOptionTypeSubCommand { + fullName += " " + opt.Name + } else if opt.Type == discord.ApplicationCommandOptionTypeSubCommandGroup { fullName += " " + opt.Name - } else if opt.Type == discordgo.ApplicationCommandOptionSubCommandGroup { - fullName += " " + opt.Name + " " + opt.Options[0].Name + // TODO: Handle subcommand group options properly } } @@ -120,150 +126,199 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } // DEPRECATED: This has been replaced by templates, it's only here for backwards compatibility - variables := variables.NewContext( - variables.NewInteractionVariables(interaction), - variables.NewGuildVariables(interaction.GuildID, s.State, nil), - variables.NewChannelVariables(interaction.ChannelID, s.State, nil), - ) - - features, err := m.planStore.GetPlanFeaturesForGuild(context.TODO(), interaction.GuildID) + // TODO: Refactor variables to use disgo types + // variables := variables.NewContext( + // variables.NewInteractionVariables(interaction), + // variables.NewGuildVariables(interaction.GuildID().String(), s.State, nil), + // variables.NewChannelVariables(interaction.ChannelID, s.State, nil), + // ) + + features, err := m.planStore.GetPlanFeaturesForGuild(context.TODO(), *interaction.GuildID()) if err != nil { return fmt.Errorf("could not get plan features: %w", err) } templates := template.NewContext( "HANDLE_ACTION", features.MaxTemplateOps, - template.NewInteractionProvider(s.State, interaction), - template.NewKVProvider(interaction.GuildID, m.pg, features.MaxKVKeys), + template.NewInteractionProvider(nil, interaction), // TODO: Fix caches access + template.NewKVProvider(*interaction.GuildID(), m.pg, features.MaxKVKeys), ) for _, action := range actionSet.Actions { switch action.Type { case actions.ActionTypeTextResponse: - var flags discordgo.MessageFlags + var flags discord.MessageFlags if !action.Public { - flags = discordgo.MessageFlagsEphemeral + flags = discord.MessageFlagEphemeral } - content, ok := executeTemplate(i, templates, variables.FillString(action.Text)) + content, ok := executeTemplate(i, templates, action.Text) // TODO: Fix variables.FillString if !ok { return nil } - allowedMentions := []discordgo.AllowedMentionType{ - discordgo.AllowedMentionTypeUsers, + allowedMentions := []discord.AllowedMentionType{ + discord.AllowedMentionTypeUsers, } if action.AllowRoleMentions { allowedMentions = append( allowedMentions, - discordgo.AllowedMentionTypeRoles, - discordgo.AllowedMentionTypeEveryone, + discord.AllowedMentionTypeRoles, + discord.AllowedMentionTypeEveryone, ) } - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: content, Flags: flags, - AllowedMentions: &discordgo.MessageAllowedMentions{ + AllowedMentions: &discord.AllowedMentions{ Parse: allowedMentions, }, }) case actions.ActionTypeToggleRole: - if !legacyPermissions && !derivedPerms.CanManageRole(action.TargetID) { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("The user that has created this message doesn't have permissions to toggle the role <@&%s>.", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil + if !legacyPermissions { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + if !derivedPerms.CanManageRole(roleID) { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("The user that has created this message doesn't have permissions to toggle the role <@&%s>.", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + return nil + } } hasRole := false - for _, roleID := range interaction.Member.Roles { - if roleID == action.TargetID { - hasRole = true + if member := interaction.Member(); member != nil { + for _, roleID := range member.RoleIDs { + if roleID.String() == action.TargetID { + hasRole = true + break + } } } var err error - if hasRole { - err = s.GuildMemberRoleRemove(interaction.GuildID, interaction.Member.User.ID, action.TargetID) - if err == nil { - if !action.DisableDefaultResponse { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("Removed role <@&%s>", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, - }) - } + if member := interaction.Member(); member != nil { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err } - } else { - err = s.GuildMemberRoleAdd(interaction.GuildID, interaction.Member.User.ID, action.TargetID) - if err == nil { - if !action.DisableDefaultResponse { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("Added role <@&%s>", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, - }) + + if hasRole { + err = rest.RemoveMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Removed role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } + } else { + err = rest.AddMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Added role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } } } } if err != nil { log.Error().Err(err).Msg("Failed to toggle role") - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: roleErrorMessage, - Flags: discordgo.MessageFlagsEphemeral, + Flags: discord.MessageFlagEphemeral, }) } case actions.ActionTypeAddRole: - if !legacyPermissions && !derivedPerms.CanManageRole(action.TargetID) { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("The user that has created this message doesn't have permissions to assign the role <@&%s>.", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil + if !legacyPermissions { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + if !derivedPerms.CanManageRole(roleID) { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("The user that has created this message doesn't have permissions to assign the role <@&%s>.", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + return nil + } } - err := s.GuildMemberRoleAdd(interaction.GuildID, interaction.Member.User.ID, action.TargetID) - if err == nil { - if !action.DisableDefaultResponse { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("Added role <@&%s>", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, + if member := interaction.Member(); member != nil { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + + err = rest.AddMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Added role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } else { + log.Error().Err(err).Msg("Failed to add role") + i.Respond(discord.MessageCreate{ + Content: roleErrorMessage, + Flags: discord.MessageFlagEphemeral, }) } - } else { - log.Error().Err(err).Msg("Failed to add role") - i.Respond(&discordgo.InteractionResponseData{ - Content: roleErrorMessage, - Flags: discordgo.MessageFlagsEphemeral, - }) } case actions.ActionTypeRemoveRole: - if !legacyPermissions && !derivedPerms.CanManageRole(action.TargetID) { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("The user that has created this message doesn't have permissions to remove the role <@&%s>.", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil + if !legacyPermissions { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + if !derivedPerms.CanManageRole(roleID) { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("The user that has created this message doesn't have permissions to remove the role <@&%s>.", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + return nil + } } - err := s.GuildMemberRoleRemove(interaction.GuildID, interaction.Member.User.ID, action.TargetID) - if err == nil { - if !action.DisableDefaultResponse { - i.Respond(&discordgo.InteractionResponseData{ - Content: fmt.Sprintf("Removed role <@&%s>", action.TargetID), - Flags: discordgo.MessageFlagsEphemeral, + if member := interaction.Member(); member != nil { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + + err = rest.RemoveMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Removed role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } else { + log.Error().Err(err).Msg("Failed to remove role") + i.Respond(discord.MessageCreate{ + Content: roleErrorMessage, + Flags: discord.MessageFlagEphemeral, }) } - } else { - log.Error().Err(err).Msg("Failed to remove role") - i.Respond(&discordgo.InteractionResponseData{ - Content: roleErrorMessage, - Flags: discordgo.MessageFlagsEphemeral, - }) } case actions.ActionTypeSavedMessageResponse: msg, err := m.pg.Q.GetSavedMessageForGuild(context.TODO(), pgmodel.GetSavedMessageForGuildParams{ - GuildID: sql.NullString{Valid: true, String: interaction.GuildID}, + GuildID: sql.NullString{Valid: true, String: interaction.GuildID().String()}, ID: action.TargetID, }) if err != nil { @@ -276,17 +331,17 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e return err } - variables.FillMessage(data) + // TODO: Fix variables system - variables.FillMessage(data) if !executeTemplateMessage(i, templates, data) { return nil } - var flags discordgo.MessageFlags + var flags discord.MessageFlags if !action.Public { - flags = discordgo.MessageFlagsEphemeral + flags = discord.MessageFlagEphemeral } - var components []discordgo.MessageComponent + var components []discord.LayoutComponent if !legacyPermissions { components, err = m.parser.ParseMessageComponents(data.Components, features.ComponentTypes) if err != nil { @@ -294,30 +349,30 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } } - allowedMentions := []discordgo.AllowedMentionType{ - discordgo.AllowedMentionTypeUsers, + allowedMentions := []discord.AllowedMentionType{ + discord.AllowedMentionTypeUsers, } if action.AllowRoleMentions { allowedMentions = append( allowedMentions, - discordgo.AllowedMentionTypeRoles, - discordgo.AllowedMentionTypeEveryone, + discord.AllowedMentionTypeRoles, + discord.AllowedMentionTypeEveryone, ) } // We need to get the message id of the response, so it has to be a followup response if !i.HasResponded() { - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Flags: flags, - }, discordgo.InteractionResponseDeferredChannelMessageWithSource) + }) } - newMsg := i.Respond(&discordgo.InteractionResponseData{ + newMsg := i.Respond(discord.MessageCreate{ Content: data.Content, Embeds: data.Embeds, Components: components, Flags: flags, - AllowedMentions: &discordgo.MessageAllowedMentions{ + AllowedMentions: &discord.AllowedMentions{ Parse: allowedMentions, }, }) @@ -329,94 +384,33 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } } case actions.ActionTypeTextDM: - dmChannel, err := s.UserChannelCreate(interaction.Member.User.ID) - if err != nil { - i.Respond(&discordgo.InteractionResponseData{ - Content: "Failed to send DM", - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil - } - - content, ok := executeTemplate(i, templates, variables.FillString(action.Text)) - if !ok { - return nil - } - - _, err = s.ChannelMessageSend(dmChannel.ID, content) - if err != nil { - i.Respond(&discordgo.InteractionResponseData{ - Content: "Failed to send DM", - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil - } - - i.Respond(&discordgo.InteractionResponseData{ - Content: "You have received a DM!", - Flags: discordgo.MessageFlagsEphemeral, + // TODO: Fix DM functionality - need to implement with disgo + i.Respond(discord.MessageCreate{ + Content: "DM functionality not yet implemented with disgo", + Flags: discord.MessageFlagEphemeral, }) case actions.ActionTypeSavedMessageDM: - msg, err := m.pg.Q.GetSavedMessageForGuild(context.TODO(), pgmodel.GetSavedMessageForGuildParams{ - GuildID: sql.NullString{Valid: true, String: interaction.GuildID}, - ID: action.TargetID, - }) - if err != nil { - return err - } - - data := &actions.MessageWithActions{} - err = json.Unmarshal(msg.Data, data) - if err != nil { - return err - } - - variables.FillMessage(data) - if !executeTemplateMessage(i, templates, data) { - return nil - } - - dmChannel, err := s.UserChannelCreate(interaction.Member.User.ID) - if err != nil { - i.Respond(&discordgo.InteractionResponseData{ - Content: "Failed to send DM", - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil - } - - _, err = s.ChannelMessageSendComplex(dmChannel.ID, &discordgo.MessageSend{ - Content: data.Content, - Embeds: data.Embeds, - }) - if err != nil { - i.Respond(&discordgo.InteractionResponseData{ - Content: "Failed to send DM", - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil - } - - i.Respond(&discordgo.InteractionResponseData{ - Content: "You have received a DM!", - Flags: discordgo.MessageFlagsEphemeral, + // TODO: Fix DM functionality - need to implement with disgo + i.Respond(discord.MessageCreate{ + Content: "DM functionality not yet implemented with disgo", + Flags: discord.MessageFlagEphemeral, }) case actions.ActionTypeTextEdit: - content, ok := executeTemplate(i, templates, variables.FillString(action.Text)) + content, ok := executeTemplate(i, templates, action.Text) // TODO: Fix variables system if !ok { return nil } - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: content, - }, discordgo.InteractionResponseUpdateMessage) + }) case actions.ActionTypeSavedMessageEdit: - if interaction.Type != discordgo.InteractionMessageComponent { + if interaction.Type() != discord.InteractionTypeComponent { continue } msg, err := m.pg.Q.GetSavedMessageForGuild(context.TODO(), pgmodel.GetSavedMessageForGuildParams{ - GuildID: sql.NullString{Valid: true, String: interaction.GuildID}, + GuildID: sql.NullString{Valid: true, String: interaction.GuildID().String()}, ID: action.TargetID, }) if err != nil { @@ -429,12 +423,12 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e return err } - variables.FillMessage(data) + // TODO: Fix variables system - variables.FillMessage(data) if !executeTemplateMessage(i, templates, data) { return nil } - var components []discordgo.MessageComponent + var components []discord.LayoutComponent if !legacyPermissions { components, err = m.parser.ParseMessageComponents(data.Components, features.ComponentTypes) if err != nil { @@ -442,32 +436,34 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } } - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: data.Content, Embeds: data.Embeds, Components: components, - }, discordgo.InteractionResponseUpdateMessage) + }) if !legacyPermissions { - ephemeral := interaction.Message.Flags&discordgo.MessageFlagsEphemeral != 0 - err = m.parser.CreateActionsForMessage(context.TODO(), data.Actions, derivedPerms, interaction.Message.ID, ephemeral) - if err != nil { - log.Error().Err(err).Msg("failed to create actions for message") - return err + if compInteraction, ok := interaction.(discord.ComponentInteraction); ok { + ephemeral := compInteraction.Message.Flags&discord.MessageFlagEphemeral != 0 + err = m.parser.CreateActionsForMessage(context.TODO(), data.Actions, derivedPerms, compInteraction.Message.ID, ephemeral) + if err != nil { + log.Error().Err(err).Msg("failed to create actions for message") + return err + } } } case actions.ActionTypePermissionCheck: perms, _ := strconv.ParseInt(action.Permissions, 10, 64) - if interaction.Member.Permissions&perms != perms { + if member := interaction.Member(); member != nil && member.Permissions&discord.Permissions(perms) != discord.Permissions(perms) { responseText := "You don't have the required permissions to use this component or command." if action.DisableDefaultResponse { responseText = action.Text } - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: responseText, - Flags: discordgo.MessageFlagsEphemeral, + Flags: discord.MessageFlagEphemeral, }) return nil } @@ -478,13 +474,19 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } if len(action.RoleIDs) != 0 { - for _, roleID := range action.RoleIDs { - if !slices.Contains(interaction.Member.Roles, roleID) { - i.Respond(&discordgo.InteractionResponseData{ - Content: responseText, - Flags: discordgo.MessageFlagsEphemeral, - }) - return nil + if member := interaction.Member(); member != nil { + for _, roleID := range action.RoleIDs { + roleIDSnowflake, err := snowflake.Parse(roleID) + if err != nil { + continue + } + if !slices.Contains(member.RoleIDs, roleIDSnowflake) { + i.Respond(discord.MessageCreate{ + Content: responseText, + Flags: discord.MessageFlagEphemeral, + }) + return nil + } } } } @@ -492,12 +494,12 @@ func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) e } if !i.HasResponded() { - if interaction.Type == discordgo.InteractionMessageComponent { - i.Respond(nil, discordgo.InteractionResponseDeferredMessageUpdate) + if interaction.Type() == discord.InteractionTypeComponent { + i.Respond(discord.MessageCreate{}) } else { - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: "No response", - Flags: discordgo.MessageFlagsEphemeral, + Flags: discord.MessageFlagEphemeral, }) } } @@ -509,9 +511,9 @@ func executeTemplate(i Interaction, templates *template.TemplateContext, text st res, err := templates.ParseAndExecute(text) if err != nil { log.Error().Err(err).Msg("Failed to execute template") - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: fmt.Sprintf("Failed to execute template variables:\n```%s```", err.Error()), - Flags: discordgo.MessageFlagsEphemeral, + Flags: discord.MessageFlagEphemeral, }) return "", false } @@ -521,9 +523,9 @@ func executeTemplate(i Interaction, templates *template.TemplateContext, text st func executeTemplateMessage(i Interaction, templates *template.TemplateContext, m *actions.MessageWithActions) bool { if err := templates.ParseAndExecuteMessage(m); err != nil { log.Error().Err(err).Msg("Failed to execute template") - i.Respond(&discordgo.InteractionResponseData{ + i.Respond(discord.MessageCreate{ Content: fmt.Sprintf("Failed to execute template variables:\n```%s```", err.Error()), - Flags: discordgo.MessageFlagsEphemeral, + Flags: discord.MessageFlagEphemeral, }) return false } From a3412cd428eb452086510fbe8af1c7fb314d4234 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Mon, 27 Oct 2025 16:41:46 +0100 Subject: [PATCH 6/8] cleanup old discordgo code --- embedg-server/actions/parser/permissions.go | 3 +- embedg-server/api/access/access.go | 6 +- embedg-server/api/access/helpers.go | 11 +- .../api/handlers/custom_bots/handler.go | 5 +- embedg-server/bot/bot.go | 80 --- embedg-server/bot/commands.go | 669 ------------------ embedg-server/bot/components.go | 348 --------- embedg-server/bot/entitlements.go | 110 --- embedg-server/bot/listeners.go | 71 -- embedg-server/bot/logo-512.png | Bin 9747 -> 0 bytes embedg-server/bot/rest/cache.go | 115 --- embedg-server/bot/rest/client.go | 17 - embedg-server/bot/sharding/manager.go | 281 -------- embedg-server/bot/sharding/monitor.go | 62 -- embedg-server/bot/sharding/shard.go | 118 --- embedg-server/bot/util.go | 335 --------- embedg-server/custom_bots/bot.go | 14 +- embedg-server/embedg/bot.go | 17 +- embedg-server/embedg/commands.go | 7 +- embedg-server/embedg/rest/rest.go | 3 +- 20 files changed, 29 insertions(+), 2243 deletions(-) delete mode 100644 embedg-server/bot/bot.go delete mode 100644 embedg-server/bot/commands.go delete mode 100644 embedg-server/bot/components.go delete mode 100644 embedg-server/bot/entitlements.go delete mode 100644 embedg-server/bot/listeners.go delete mode 100644 embedg-server/bot/logo-512.png delete mode 100644 embedg-server/bot/rest/cache.go delete mode 100644 embedg-server/bot/rest/client.go delete mode 100644 embedg-server/bot/sharding/manager.go delete mode 100644 embedg-server/bot/sharding/monitor.go delete mode 100644 embedg-server/bot/sharding/shard.go delete mode 100644 embedg-server/bot/util.go diff --git a/embedg-server/actions/parser/permissions.go b/embedg-server/actions/parser/permissions.go index bacf11821..c198a7ace 100644 --- a/embedg-server/actions/parser/permissions.go +++ b/embedg-server/actions/parser/permissions.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/disgoorg/disgo/discord" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions" "github.com/merlinfuchs/embed-generator/embedg-server/api/access" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" @@ -85,7 +84,7 @@ func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actio case actions.ActionTypeTextResponse, actions.ActionTypeTextDM, actions.ActionTypeTextEdit: break case actions.ActionTypeAddRole, actions.ActionTypeRemoveRole, actions.ActionTypeToggleRole: - if permissions&discordgo.PermissionManageRoles == 0 { + if permissions&discord.PermissionManageRoles == 0 { return fmt.Errorf("You have no permission to manage roles in the channel %s", channelID) } diff --git a/embedg-server/api/access/access.go b/embedg-server/api/access/access.go index 4199e7e4f..bf934d2f2 100644 --- a/embedg-server/api/access/access.go +++ b/embedg-server/api/access/access.go @@ -34,11 +34,11 @@ type ChannelAccess struct { } func (c *ChannelAccess) UserAccess() bool { - return c.UserPermissions&(discordgo.PermissionManageWebhooks|discordgo.PermissionAdministrator) != 0 + return c.UserPermissions&(discord.PermissionManageWebhooks|discord.PermissionAdministrator) != 0 } func (c *ChannelAccess) BotAccess() bool { - return c.BotPermissions&(discordgo.PermissionManageWebhooks|discordgo.PermissionAdministrator) != 0 + return c.BotPermissions&(discord.PermissionManageWebhooks|discord.PermissionAdministrator) != 0 } func (m *AccessManager) GetGuildAccessForUser(userID util.ID, guildID util.ID) (GuildAccess, error) { @@ -151,7 +151,7 @@ func (m *AccessManager) ComputeUserPermissionsForChannel(userID util.ID, channel if guild.OwnerID == userID { // Owner has access to all channels - return discordgo.PermissionAll, nil + return discord.PermissionsAll, nil } member, err := m.GetGuildMember(guild.ID, userID) diff --git a/embedg-server/api/access/helpers.go b/embedg-server/api/access/helpers.go index 09eec9869..50033f9ad 100644 --- a/embedg-server/api/access/helpers.go +++ b/embedg-server/api/access/helpers.go @@ -2,13 +2,12 @@ package access import ( "github.com/disgoorg/disgo/discord" - "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/util" ) func memberPermissions(guild *discord.Guild, roles []discord.Role, channel discord.GuildChannel, userID util.ID, roleIDs []util.ID) (apermissions discord.Permissions) { if userID == guild.OwnerID { - apermissions = discordgo.PermissionAll + apermissions = discord.PermissionsAll return } @@ -28,8 +27,8 @@ func memberPermissions(guild *discord.Guild, roles []discord.Role, channel disco } } - if apermissions&discordgo.PermissionAdministrator == discordgo.PermissionAdministrator { - apermissions |= discordgo.PermissionAll + if apermissions&discord.PermissionAdministrator == discord.PermissionAdministrator { + apermissions |= discord.PermissionsAll return // Administrator bypasses all overrides } @@ -75,8 +74,8 @@ func memberPermissions(guild *discord.Guild, roles []discord.Role, channel disco } } - if apermissions&discordgo.PermissionAdministrator == discordgo.PermissionAdministrator { - apermissions |= discordgo.PermissionAll + if apermissions&discord.PermissionAdministrator == discord.PermissionAdministrator { + apermissions |= discord.PermissionsAll } return apermissions diff --git a/embedg-server/api/handlers/custom_bots/handler.go b/embedg-server/api/handlers/custom_bots/handler.go index 3b9e9a3f4..b8cb6f2a2 100644 --- a/embedg-server/api/handlers/custom_bots/handler.go +++ b/embedg-server/api/handlers/custom_bots/handler.go @@ -7,6 +7,7 @@ import ( "slices" + "github.com/disgoorg/disgo/discord" "github.com/gofiber/fiber/v2" "github.com/merlinfuchs/discordgo" "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" @@ -95,7 +96,7 @@ func (h *CustomBotsHandler) HandleConfigureCustomBot(c *fiber.Ctx, req wire.Cust if isMember { for role := range roles { if slices.Contains(member.RoleIDs, role.ID) || role.ID == guildID { - if role.Permissions&discordgo.PermissionManageWebhooks != 0 { + if role.Permissions&discord.PermissionManageWebhooks != 0 { hasPermissions = true break } @@ -277,7 +278,7 @@ func (h *CustomBotsHandler) HandleGetCustomBot(c *fiber.Ctx) error { if member != nil { for role := range roles { if slices.Contains(member.RoleIDs, role.ID) || role.ID == guildID { - if role.Permissions&discordgo.PermissionManageWebhooks != 0 { + if role.Permissions&discord.PermissionManageWebhooks != 0 { hasPermissions = true break } diff --git a/embedg-server/bot/bot.go b/embedg-server/bot/bot.go deleted file mode 100644 index 16137a7bd..000000000 --- a/embedg-server/bot/bot.go +++ /dev/null @@ -1,80 +0,0 @@ -package bot - -import ( - _ "embed" - - "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" - "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" - "github.com/merlinfuchs/embed-generator/embedg-server/bot/rest" - "github.com/merlinfuchs/embed-generator/embedg-server/bot/sharding" - "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" -) - -//go:embed logo-512.png -var logoFile []byte - -type Bot struct { - *sharding.ShardManager - pg *postgres.PostgresStore - ActionHandler *handler.ActionHandler - ActionParser *parser.ActionParser - - State *discordgo.State - Rest *rest.RestClientWithCache -} - -func New(token string, pg *postgres.PostgresStore) (*Bot, error) { - manager, err := sharding.New("Bot " + token) - if err != nil { - return nil, err - } - - manager.Intents = discordgo.IntentGuilds | discordgo.IntentGuildMessages | discordgo.IntentGuildEmojis - manager.Presence = &discordgo.GatewayStatusUpdate{ - Game: discordgo.Activity{ - Name: viper.GetString("discord.activity_name"), - Type: discordgo.ActivityTypeWatching, - }, - Status: string(discordgo.StatusOnline), - } - - b := &Bot{ - ShardManager: manager, - pg: pg, - State: discordgo.NewState(), - Rest: rest.NewRestClientWithCache(manager.Session), - } - - b.AddHandler(onReady) - b.AddHandler(onConnect) - b.AddHandler(onDisconnect) - b.AddHandler(onResumed) - b.AddHandler(b.onInteractionCreate) - b.AddHandler(b.onRawEvent) - b.AddHandler(b.onInterface) - - b.AddHandler(b.onMessageDelete) - b.AddHandler(b.onGuildMemberUpdate) - b.AddHandler(b.onGuildMemberRemove) - - go b.lazyTierTask() - - return b, nil -} - -func (b *Bot) Start() error { - err := b.RegisterCommand() - if err != nil { - log.Fatal().Err(err).Msg("Failed to register command") - return err - } - - err = b.ShardManager.Start() - if err != nil { - log.Fatal().Err(err).Msg("Failed to open discord session") - } - return err -} diff --git a/embedg-server/bot/commands.go b/embedg-server/bot/commands.go deleted file mode 100644 index 29348787b..000000000 --- a/embedg-server/bot/commands.go +++ /dev/null @@ -1,669 +0,0 @@ -package bot - -import ( - "context" - "encoding/json" - "fmt" - "regexp" - "strings" - "time" - - "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/actions" - "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" - "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" - "github.com/merlinfuchs/embed-generator/embedg-server/util" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" -) - -var MessageIDRe = regexp.MustCompile("https?://(?:canary\\.|ptb\\.)?discord\\.com/channels/[0-9]+/([0-9]+)/([0-9]+)") - -func (b *Bot) RegisterCommand() error { - _, err := b.Session.ApplicationCommandBulkOverwrite(viper.GetString("discord.client_id"), "", []*discordgo.ApplicationCommand{ - { - Name: "help", - Description: "Show help", - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - discordgo.ApplicationIntegrationUserInstall, - }, - }, - { - Name: "invite", - Description: "Invite the Embed Generator bot to your server", - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - }, - }, - { - Name: "website", - Description: "Open the Embed Generator website", - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - discordgo.ApplicationIntegrationUserInstall, - }, - }, - { - Name: "format", - Description: "Get the API format for mentions, channels, roles, & custom emojis", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "text", - Description: "Get the API format for a text with multiple mentions, channels, & custom emojis", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "text", - Description: "The text that you want to format (usually containing mentions or custom emojis)", - Required: true, - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "user", - Description: "Get the API format for mentioning a user", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionUser, - Name: "user", - Description: "The user you want to mention", - Required: true, - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "channel", - Description: "Get the API format for mentioning a channel", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionChannel, - Name: "channel", - Description: "The channel you want to mention", - Required: true, - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "role", - Description: "Get the API format for mentioning a role", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionRole, - Name: "role", - Description: "The role you want to mention", - Required: true, - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "emoji", - Description: "Get the API format for a standard or custom emoji", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "emoji", - Description: "The standard or custom emoji you want to use", - Required: true, - }, - }, - }, - }, - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - discordgo.ApplicationIntegrationUserInstall, - }, - }, - { - Name: "image", - Description: "Get the image URL for different entities", - DMPermission: util.Ptr(false), - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "avatar", - Description: "Get the avatar URL for a user", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionUser, - Name: "user", - Description: "The user you want to get the avatar for", - Required: true, - }, - { - Type: discordgo.ApplicationCommandOptionBoolean, - Name: "static", - Description: "Whether animated avatars should be converted to static images", - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "icon", - Description: "Get the icon URL for this server", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionBoolean, - Name: "static", - Description: "Whether animated icons should be converted to static images", - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "emoji", - Description: "Get the image URL for a custom or standard emoji", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "emoji", - Description: "The custom emoji you want the image URL for", - Required: true, - }, - { - Type: discordgo.ApplicationCommandOptionBoolean, - Name: "static", - Description: "Whether animated emojis should be converted to static images", - }, - }, - }, - }, - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - }, - }, - { - - Name: "message", - Description: "Get JSON for or restore a message on Embed Generator", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "restore", - Description: "Restore a message on Embed Generator", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "message_id_or_url", - Description: "ID or URL of the message you want to restore", - Required: true, - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionSubCommand, - Name: "dump", - Description: "Get the JSON code for a message", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "message_id_or_url", - Description: "ID or URL of the message you want to restore", - Required: true, - }, - }, - }, - }, - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - }, - }, - { - Type: discordgo.MessageApplicationCommand, - Name: "Restore Message", - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - discordgo.ApplicationIntegrationUserInstall, - }, - }, - { - Type: discordgo.MessageApplicationCommand, - Name: "Dump Message", - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - discordgo.ApplicationIntegrationUserInstall, - }, - }, - { - Type: discordgo.UserApplicationCommand, - Name: "Avatar Url", - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - discordgo.ApplicationIntegrationUserInstall, - }, - }, - { - Name: "embed", - Description: "Create an embed message", - DMPermission: util.Ptr(false), - DefaultMemberPermissions: util.Ptr(int64(discordgo.PermissionManageWebhooks)), - IntegrationTypes: &[]discordgo.ApplicationIntegrationType{ - discordgo.ApplicationIntegrationGuildInstall, - }, - }, - }) - return err -} - -func (b *Bot) HandlerInteraction(s *discordgo.Session, i handler.Interaction, data discordgo.InteractionData) { - switch i.Interaction().Type { - case discordgo.InteractionMessageComponent: - data := i.Interaction().MessageComponentData() - if strings.HasPrefix(data.CustomID, "action:") { - err := b.ActionHandler.HandleActionInteraction(b.Session, i) - if err != nil { - log.Error().Err(err).Msg("Failed to handle action interaction") - } - } else { - b.handleComponentInteraction(b.Session, i, data) - } - case discordgo.InteractionModalSubmit: - data := i.Interaction().ModalSubmitData() - b.handleModalInteraction(b.Session, i, data) - case discordgo.InteractionApplicationCommand: - data := i.Interaction().ApplicationCommandData() - b.handleCommandInteraction(b.Session, i, data) - } -} - -func (b *Bot) handleCommandInteraction(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - switch data.Name { - case "invite": - b.handleInviteCommand(s, i, data) - case "website": - b.handleWebsiteCommand(s, i, data) - case "help": - b.handleHelpCommand(s, i, data) - case "format": - b.handleFormatCommand(s, i, data) - case "image": - b.handleImageCommand(s, i, data) - case "message": - b.handleMessageCommand(s, i, data) - case "Restore Message": - b.handleRestoreContextCommand(s, i, data) - case "Dump Message": - b.handleJSONContextCommand(s, i, data) - case "Avatar Url": - b.handleAvatarUrlContextCommand(s, i, data) - case "embed": - b.handleEmbedCommand(s, i, data) - } -} - -func (b *Bot) handleHelpCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - helpResponse(s, i) -} - -func (b *Bot) handleInviteCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - helpResponse(s, i) -} - -func (b *Bot) handleWebsiteCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - helpResponse(s, i) -} - -func helpResponse(s *discordgo.Session, i handler.Interaction) { - fancyResponse(s, i, "**The best way to generate rich embed messages for your Discord Server!**\n\nhttps://www.youtube.com/watch?v=DnFP0MRJPIg", []*discordgo.MessageEmbed{}, []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Style: discordgo.LinkButton, - Label: "Website", - URL: "https://message.style", - }, - discordgo.Button{ - Style: discordgo.LinkButton, - Label: "Invite Bot", - URL: util.BotInviteURL(), - }, - discordgo.Button{ - Style: discordgo.LinkButton, - Label: "Discord Server", - URL: viper.GetString("links.discord"), - }, - }, - }, - }) -} - -func (b *Bot) handleFormatCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - subCMD := data.Options[0] - - switch subCMD.Name { - case "text": - value := subCMD.Options[0].StringValue() - textResponse(s, i, fmt.Sprintf("API format for the provided text: ```%s```", value)) - case "user": - user := subCMD.Options[0].UserValue(nil) - textResponse(s, i, fmt.Sprintf("API format for <@%s>: ```<@%s>```", user.ID, user.ID)) - case "channel": - channel := subCMD.Options[0].ChannelValue(nil) - textResponse(s, i, fmt.Sprintf("API format for <#%s>: ```<#%s>```", channel.ID, channel.ID)) - case "role": - role := subCMD.Options[0].RoleValue(nil, i.Interaction().GuildID) - textResponse(s, i, fmt.Sprintf("API format for <@&%s>: ```<@&%s>```", role.ID, role.ID)) - case "emoji": - emoji := subCMD.Options[0].StringValue() - // TODO - textResponse(s, i, fmt.Sprintf("API format for %s: ```%s```", emoji, emoji)) - } -} - -func (b *Bot) handleImageCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - subCMD := data.Options[0] - - makeStatic := func(url string, option int) string { - if len(subCMD.Options) <= option { - return url - } - - static := subCMD.Options[option].BoolValue() - if static { - return strings.Replace(url, ".gif", ".png", 1) - } - - return url - } - - switch subCMD.Name { - case "avatar": - userID := subCMD.Options[0].UserValue(nil).ID - user := data.Resolved.Users[userID] - - avatarURL := makeStatic(user.AvatarURL("1024"), 1) - imageUrlResponse(s, i, avatarURL) - case "icon": - guild, err := b.State.Guild(i.Interaction().GuildID) - if err != nil { - log.Error().Err(err).Msg("Failed to get server") - textResponse(s, i, "Failed to get server.") - return - } - if guild.Icon == "" { - textResponse(s, i, "This server has no icon.") - return - } - iconURL := makeStatic(guild.IconURL("1024"), 1) - imageUrlResponse(s, i, iconURL) - case "emoji": - // emoji := subCMD.Options[0].StringValue() - // TODO: get emoji id from regex - return - } -} - -func (b *Bot) handleMessageCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - subCMD := data.Options[0] - - messageID := subCMD.Options[0].StringValue() - - match := MessageIDRe.FindStringSubmatch(messageID) - if match != nil { - messageID = match[2] - } - - message, err := s.ChannelMessage(i.Interaction().ChannelID, messageID) - if err != nil { - if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMessage) { - textResponse(s, i, "Message not found.") - return - } - log.Error().Err(err).Msg("Failed to get message") - textResponse(s, i, "Failed to get message.") - return - } - - components, err := b.ActionParser.UnparseMessageComponents(message.Components) - if err != nil { - log.Error().Err(err).Msg("Failed to unparse message components") - textResponse(s, i, "Failed to unparse message components.") - return - } - - actionSets, err := b.ActionParser.RetrieveActionsForMessage(context.TODO(), messageID) - if err != nil { - log.Error().Err(err).Msg("Failed to retrieve actions for message") - textResponse(s, i, "Failed to retrieve actions for message.") - return - } - - messageDump, err := json.MarshalIndent(actions.MessageWithActions{ - Username: message.Author.Username, - AvatarURL: message.Author.AvatarURL("1024"), - Content: message.Content, - Embeds: message.Embeds, - Components: components, - Actions: actionSets, - }, "", " ") - if err != nil { - log.Error().Err(err).Msg("Failed to marshal message dump") - textResponse(s, i, "Failed to marshal message dump.") - return - } - - switch subCMD.Name { - case "restore": - msg, err := b.pg.Q.InsertSharedMessage(context.TODO(), pgmodel.InsertSharedMessageParams{ - ID: util.UniqueID(), - CreatedAt: time.Now().UTC(), - ExpiresAt: time.Now().UTC().Add(time.Hour * 24), - Data: messageDump, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to insert shared message") - textResponse(s, i, "Failed to create shared message.") - return - } - - url := fmt.Sprintf("%s/editor/share/%s", viper.GetString("app.public_url"), msg.ID) - textResponse(s, i, fmt.Sprintf("Click this link to restore the message: [message.style](<%s>)", url)) - case "dump": - paste, err := util.CreateVaultBinPaste(string(messageDump), "json") - if err != nil { - log.Error().Err(err).Msg("Failed to create vaultb.in paste") - textResponse(s, i, "Failed to create vaultb.in paste.") - return - } - - textResponse(s, i, fmt.Sprintf("You can find the JSON code here: <%s>", paste.URL())) - } -} - -func (b *Bot) handleRestoreContextCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - messageID := data.TargetID - message := data.Resolved.Messages[messageID] - - components, err := b.ActionParser.UnparseMessageComponents(message.Components) - if err != nil { - log.Error().Err(err).Msg("Failed to unparse message components") - textResponse(s, i, "Failed to unparse message components.") - return - } - - actionSets, err := b.ActionParser.RetrieveActionsForMessage(context.TODO(), messageID) - if err != nil { - log.Error().Err(err).Msg("Failed to retrieve actions for message") - textResponse(s, i, "Failed to retrieve actions for message.") - return - } - - messageDump, err := json.MarshalIndent(actions.MessageWithActions{ - Username: message.Author.Username, - AvatarURL: message.Author.AvatarURL("1024"), - Content: message.Content, - Embeds: message.Embeds, - Components: components, - Actions: actionSets, - }, "", " ") - if err != nil { - log.Error().Err(err).Msg("Failed to marshal message dump") - textResponse(s, i, "Failed to marshal message dump.") - return - } - - msg, err := b.pg.Q.InsertSharedMessage(context.TODO(), pgmodel.InsertSharedMessageParams{ - ID: util.UniqueID(), - CreatedAt: time.Now().UTC(), - ExpiresAt: time.Now().UTC().Add(time.Hour * 24), - Data: messageDump, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to insert shared message") - textResponse(s, i, "Failed to create shared message.") - return - } - - url := fmt.Sprintf("%s/editor/share/%s", viper.GetString("app.public_url"), msg.ID) - textResponse(s, i, fmt.Sprintf("Click this link to restore the message: [message.style](<%s>)", url)) -} - -func (b *Bot) handleJSONContextCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - messageID := data.TargetID - message := data.Resolved.Messages[messageID] - - components, err := b.ActionParser.UnparseMessageComponents(message.Components) - if err != nil { - log.Error().Err(err).Msg("Failed to unparse message components") - textResponse(s, i, "Failed to unparse message components.") - return - } - - actionSets, err := b.ActionParser.RetrieveActionsForMessage(context.TODO(), messageID) - if err != nil { - log.Error().Err(err).Msg("Failed to retrieve actions for message") - textResponse(s, i, "Failed to retrieve actions for message.") - return - } - - messageDump, err := json.MarshalIndent(actions.MessageWithActions{ - Username: message.Author.Username, - AvatarURL: message.Author.AvatarURL("1024"), - Content: message.Content, - Embeds: message.Embeds, - Components: components, - Actions: actionSets, - }, "", " ") - if err != nil { - log.Error().Err(err).Msg("Failed to marshal message dump") - textResponse(s, i, "Failed to marshal message dump.") - return - } - - paste, err := util.CreateVaultBinPaste(string(messageDump), "json") - if err != nil { - log.Error().Err(err).Msg("Failed to create vaultb.in paste") - textResponse(s, i, "Failed to create vaultb.in paste.") - return - } - - textResponse(s, i, fmt.Sprintf("You can find the JSON code here: <%s>", paste.URL())) -} - -func (b *Bot) handleAvatarUrlContextCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - userId := data.TargetID - user := data.Resolved.Users[userId] - - imageUrlResponse(s, i, user.AvatarURL("1024")) -} - -func (b *Bot) handleEmbedCommand(s *discordgo.Session, i handler.Interaction, data discordgo.ApplicationCommandInteractionData) { - fancyResponse(s, i, "If you want to have more options to customize your message go to [message.style]()!", []*discordgo.MessageEmbed{}, embedEditComponent()) -} - -func textResponse(s *discordgo.Session, i handler.Interaction, content string) { - i.Respond(&discordgo.InteractionResponseData{ - Content: content, - Flags: discordgo.MessageFlagsEphemeral, - }) -} - -func imageUrlResponse(s *discordgo.Session, i handler.Interaction, url string) { - i.Respond(&discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Embeds: []*discordgo.MessageEmbed{ - { - Description: url, - Image: &discordgo.MessageEmbedImage{ - URL: url, - }, - }, - }, - }) -} - -func fancyResponse(s *discordgo.Session, i handler.Interaction, content string, embeds []*discordgo.MessageEmbed, components []discordgo.MessageComponent) { - i.Respond(&discordgo.InteractionResponseData{ - Content: content, - Flags: discordgo.MessageFlagsEphemeral, - Embeds: embeds, - Components: components, - }) -} - -func embedEditComponent() []discordgo.MessageComponent { - return []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Author", - CustomID: "embed:author", - }, - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Title", - CustomID: "embed:title", - }, - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Description", - CustomID: "embed:description", - }, - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Color", - CustomID: "embed:color", - }, - }, - }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Image", - CustomID: "embed:image", - }, - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Thumbnail", - CustomID: "embed:thumbnail", - }, - discordgo.Button{ - Style: discordgo.PrimaryButton, - Label: "Set Footer", - CustomID: "embed:footer", - }, - }, - }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Style: discordgo.DangerButton, - Label: "Cancel", - CustomID: "embed:cancel", - }, - discordgo.Button{ - Style: discordgo.SuccessButton, - Label: "Submit", - CustomID: "embed:submit", - }, - }, - }, - } -} diff --git a/embedg-server/bot/components.go b/embedg-server/bot/components.go deleted file mode 100644 index 86ed04695..000000000 --- a/embedg-server/bot/components.go +++ /dev/null @@ -1,348 +0,0 @@ -package bot - -import ( - "context" - "fmt" - "strconv" - "strings" - - "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" -) - -func (b *Bot) handleComponentInteraction(s *discordgo.Session, i handler.Interaction, data discordgo.MessageComponentInteractionData) { - if strings.HasPrefix(data.CustomID, "embed:") { - b.handleEmbedComponentInteraction(s, i, data) - } else { - textResponse(s, i, "This component is not supported anymore. Please update the message at to fix this.") - } -} - -func (b *Bot) handleEmbedComponentInteraction(s *discordgo.Session, i handler.Interaction, data discordgo.MessageComponentInteractionData) { - var currentEmbed discordgo.MessageEmbed - if len(i.Interaction().Message.Embeds) > 0 { - currentEmbed = *i.Interaction().Message.Embeds[0] - } - - switch data.CustomID { - case "embed:cancel": - i.Respond(&discordgo.InteractionResponseData{ - Content: "You have cancalled the embed editor.", - }, discordgo.InteractionResponseUpdateMessage) - case "embed:submit": - modalResponse(s, i, "Send Embed", "embed:send", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "username", - Label: "Username", - MaxLength: 80, - Placeholder: "Embed Generator", - Style: discordgo.TextInputShort, - }, - }, - }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "avatar_url", - Label: "Avatar URL", - Placeholder: "https://example.com/image.png", - Style: discordgo.TextInputShort, - }, - }, - }, - }) - case "embed:author": - var currentAuthor discordgo.MessageEmbedAuthor - if currentEmbed.Author != nil { - currentAuthor = *currentEmbed.Author - } - - modalResponse(s, i, "Edit Embed Author", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:author:name", - Label: "Name", - MaxLength: 256, - Value: currentAuthor.Name, - Style: discordgo.TextInputShort, - }, - }, - }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:author:url", - Label: "URL", - Placeholder: "https://example.com", - Value: currentAuthor.URL, - Style: discordgo.TextInputShort, - }, - }, - }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:author:icon_url", - Label: "Icon URL", - Placeholder: "https://example.com/image.png", - Value: currentAuthor.IconURL, - Style: discordgo.TextInputShort, - }, - }, - }, - }) - case "embed:title": - modalResponse(s, i, "Edit Embed Title", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:title", - Label: "Title", - MaxLength: 256, - Value: currentEmbed.Title, - Style: discordgo.TextInputShort, - }, - }, - }, - }) - case "embed:description": - modalResponse(s, i, "Edit Embed Description", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:description", - Label: "Description", - Value: currentEmbed.Title, - Style: discordgo.TextInputParagraph, - }, - }, - }, - }) - case "embed:color": - modalResponse(s, i, "Edit Embed Color", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:color", - Label: "Color", - MaxLength: 7, - Value: fmt.Sprintf("#%06x", currentEmbed.Color), - Style: discordgo.TextInputShort, - }, - }, - }, - }) - case "embed:image": - var currentImage discordgo.MessageEmbedImage - if currentEmbed.Image != nil { - currentImage = *currentEmbed.Image - } - - modalResponse(s, i, "Edit Embed Image", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:image", - Label: "Image URL", - Value: currentImage.URL, - Placeholder: "https://example.com/image.png", - Style: discordgo.TextInputShort, - }, - }, - }, - }) - case "embed:thumbnail": - var currentThumbnail discordgo.MessageEmbedThumbnail - if currentEmbed.Thumbnail != nil { - currentThumbnail = *currentEmbed.Thumbnail - } - - modalResponse(s, i, "Edit Embed Image", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:thumbnail", - Label: "Thumbnail URL", - Value: currentThumbnail.URL, - Placeholder: "https://example.com/image.png", - Style: discordgo.TextInputShort, - }, - }, - }, - }) - case "embed:footer": - var currentFooter discordgo.MessageEmbedFooter - if currentEmbed.Footer != nil { - currentFooter = *currentEmbed.Footer - } - - modalResponse(s, i, "Edit Embed Footer", "embed:update", []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:footer:text", - Label: "Text", - MaxLength: 2048, - Value: currentFooter.Text, - Style: discordgo.TextInputShort, - }, - }, - }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.TextInput{ - CustomID: "embed:footer:icon_url", - Label: "Icon URL", - Placeholder: "https://example.com/image.png", - Value: currentFooter.IconURL, - Style: discordgo.TextInputShort, - }, - }, - }, - }) - } -} - -func (b *Bot) handleModalInteraction(s *discordgo.Session, i handler.Interaction, data discordgo.ModalSubmitInteractionData) { - var currentEmbed discordgo.MessageEmbed - if len(i.Interaction().Message.Embeds) > 0 { - currentEmbed = *i.Interaction().Message.Embeds[0] - } - - switch data.CustomID { - case "embed:update": - for _, comp := range data.Components { - if comp.Type() != discordgo.ActionsRowComponent { - continue - } - - row := comp.(*discordgo.ActionsRow) - for _, comp := range row.Components { - if comp.Type() != discordgo.TextInputComponent { - continue - } - - input := comp.(*discordgo.TextInput) - switch input.CustomID { - case "embed:author:name": - if currentEmbed.Author == nil { - currentEmbed.Author = &discordgo.MessageEmbedAuthor{ - Name: input.Value, - } - } else { - currentEmbed.Author.Name = input.Value - } - case "embed:author:url": - if currentEmbed.Author == nil { - currentEmbed.Author = &discordgo.MessageEmbedAuthor{ - URL: input.Value, - } - } else { - currentEmbed.Author.URL = input.Value - } - case "embed:author:icon_url": - if currentEmbed.Author == nil { - currentEmbed.Author = &discordgo.MessageEmbedAuthor{ - IconURL: input.Value, - } - } else { - currentEmbed.Author.IconURL = input.Value - } - case "embed:title": - currentEmbed.Title = input.Value - case "embed:description": - currentEmbed.Description = input.Value - case "embed:color": - if len(input.Value) == 7 { - color, err := strconv.ParseInt(input.Value[1:], 16, 0) - if err == nil { - currentEmbed.Color = int(color) - } - } - case "embed:image": - if currentEmbed.Image == nil { - currentEmbed.Image = &discordgo.MessageEmbedImage{ - URL: input.Value, - } - } else { - currentEmbed.Image.URL = input.Value - } - case "embed:thumbnail": - if currentEmbed.Thumbnail == nil { - currentEmbed.Thumbnail = &discordgo.MessageEmbedThumbnail{ - URL: input.Value, - } - } else { - currentEmbed.Thumbnail.URL = input.Value - } - case "embed:footer:text": - if currentEmbed.Footer == nil { - currentEmbed.Footer = &discordgo.MessageEmbedFooter{ - Text: input.Value, - } - } else { - currentEmbed.Footer.Text = input.Value - } - case "embed:footer:icon_url": - if currentEmbed.Footer == nil { - currentEmbed.Footer = &discordgo.MessageEmbedFooter{ - IconURL: input.Value, - } - } else { - currentEmbed.Footer.IconURL = input.Value - } - } - } - } - case "embed:send": - var username string - var avatarURL string - - for _, comp := range data.Components { - if comp.Type() != discordgo.ActionsRowComponent { - continue - } - - row := comp.(*discordgo.ActionsRow) - for _, comp := range row.Components { - if comp.Type() != discordgo.TextInputComponent { - continue - } - - input := comp.(*discordgo.TextInput) - switch input.CustomID { - case "username": - username = input.Value - case "avatar_url": - avatarURL = input.Value - } - } - } - - _, err := b.SendMessageToChannel(context.Background(), i.Interaction().ChannelID, &discordgo.WebhookParams{ - Username: username, - AvatarURL: avatarURL, - Embeds: []*discordgo.MessageEmbed{¤tEmbed}, - }) - if err != nil { - textResponse(s, i, fmt.Sprintf("Failed to send message: `%e`", err)) - return - } - } - - i.Respond(&discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{¤tEmbed}, - Components: embedEditComponent(), - }, discordgo.InteractionResponseUpdateMessage) -} - -func modalResponse(s *discordgo.Session, i handler.Interaction, title string, customID string, components []discordgo.MessageComponent) { - i.Respond(&discordgo.InteractionResponseData{ - Title: title, - CustomID: customID, - Components: components, - }, discordgo.InteractionResponseModal) -} diff --git a/embedg-server/bot/entitlements.go b/embedg-server/bot/entitlements.go deleted file mode 100644 index 15c83038d..000000000 --- a/embedg-server/bot/entitlements.go +++ /dev/null @@ -1,110 +0,0 @@ -package bot - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "time" - - "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" - "gopkg.in/guregu/null.v4" -) - -type Entitlement struct { - ID string `json:"id"` - UserID string `json:"user_id"` - GuildID string `json:"guild_id"` - SKUID string `json:"sku_id"` - Deleted bool `json:"deleted"` - StartsAt null.Time `json:"starts_at"` - EndsAt null.Time `json:"ends_at"` - Consumed bool `json:"consumed"` -} - -func (b *Bot) HandleEntitlementEvent(e *Entitlement) { - _, err := b.pg.Q.UpsertEntitlement(context.Background(), pgmodel.UpsertEntitlementParams{ - ID: e.ID, - UserID: sql.NullString{ - String: e.UserID, - Valid: e.UserID != "", - }, - GuildID: sql.NullString{ - String: e.GuildID, - Valid: e.GuildID != "", - }, - UpdatedAt: time.Now().UTC(), - Deleted: e.Deleted, - SkuID: e.SKUID, - StartsAt: e.StartsAt.NullTime, - EndsAt: e.EndsAt.NullTime, - Consumed: e.Consumed, - }) - if err != nil { - log.Error().Err(err).Str("guild_id", e.GuildID).Str("user_id", e.UserID).Msg("Failed to create entitlement") - } -} - -func (b *Bot) lazyTierTask() { - for { - err := b.retrieveDiscordTiers(context.Background()) - if err != nil { - log.Error().Err(err).Msg("Failed to retrieve discord tiers") - } - - time.Sleep(time.Minute) - } -} - -func (b *Bot) retrieveDiscordTiers(ctx context.Context) error { - clientID := viper.GetString("discord.client_id") - - after := "0" - for { - url := fmt.Sprintf("https://discord.com/api/v10/applications/%s/entitlements?limit=100&after=%s&exclude_ended=true", clientID, after) - - resp, err := b.Session.Request("GET", url, nil) - if err != nil { - return fmt.Errorf("failed to do request: %w", err) - } - - entitlements := []Entitlement{} - err = json.Unmarshal(resp, &entitlements) - if err != nil { - return fmt.Errorf("failed to decode response body: %w", err) - } - - if len(entitlements) == 0 { - break - } - - for _, e := range entitlements { - after = e.ID - - _, err := b.pg.Q.UpsertEntitlement(context.Background(), pgmodel.UpsertEntitlementParams{ - ID: e.ID, - UserID: sql.NullString{ - String: e.UserID, - Valid: e.UserID != "", - }, - GuildID: sql.NullString{ - String: e.GuildID, - Valid: e.GuildID != "", - }, - UpdatedAt: time.Now().UTC(), - Deleted: e.Deleted, - SkuID: e.SKUID, - StartsAt: e.StartsAt.NullTime, - EndsAt: e.EndsAt.NullTime, - Consumed: e.Consumed, - }) - if err != nil { - log.Error().Err(err).Str("guild_id", e.GuildID).Str("user_id", e.UserID).Msg("Failed to create entitlement") - } - } - } - - return nil -} diff --git a/embedg-server/bot/listeners.go b/embedg-server/bot/listeners.go deleted file mode 100644 index a493ed2ab..000000000 --- a/embedg-server/bot/listeners.go +++ /dev/null @@ -1,71 +0,0 @@ -package bot - -import ( - "context" - "database/sql" - "encoding/json" - - "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" - "github.com/rs/zerolog/log" -) - -func onReady(s *discordgo.Session, r *discordgo.Ready) { - log.Info().Msgf("Shard %d is ready", s.ShardID) -} - -func onConnect(s *discordgo.Session, c *discordgo.Connect) { - log.Info().Msgf("Shard %d connected", s.ShardID) -} - -func onDisconnect(s *discordgo.Session, d *discordgo.Disconnect) { - log.Info().Msgf("Shard %d disconnected", s.ShardID) -} - -func onResumed(s *discordgo.Session, r *discordgo.Resumed) { - log.Info().Msgf("Shard %d resumed", s.ShardID) -} - -func (b *Bot) onMessageDelete(_ *discordgo.Session, msg *discordgo.MessageDelete) { - err := b.pg.Q.DeleteMessageActionSetsForMessage(context.TODO(), msg.ID) - if err != nil && err != sql.ErrNoRows { - log.Error().Err(err).Msg("Failed to delete action set for deleted message") - } -} - -func (b *Bot) onGuildMemberUpdate(_ *discordgo.Session, g *discordgo.GuildMemberUpdate) { - b.Rest.InvalidateMemberCache(g.GuildID, g.User.ID) -} - -func (b *Bot) onGuildMemberRemove(_ *discordgo.Session, g *discordgo.GuildMemberRemove) { - b.Rest.InvalidateMemberCache(g.GuildID, g.User.ID) -} - -func (b *Bot) onInteractionCreate(_ *discordgo.Session, i *discordgo.InteractionCreate) { - gi := &handler.GatewayInteraction{ - Session: b.Session, - Inner: i.Interaction, - } - - b.HandlerInteraction(b.Session, gi, i.Interaction.Data) -} - -func (b *Bot) onRawEvent(_ *discordgo.Session, e *discordgo.Event) { - if e.Type == "ENTITLEMENT_CREATE" || e.Type == "ENTITLEMENT_UPDATE" || e.Type == "ENTITLEMENT_DELETE" { - entitlement := &Entitlement{} - err := json.Unmarshal(e.RawData, entitlement) - if err != nil { - log.Error().Err(err).Msg("Failed to unmarshal entitlement") - return - } - - b.HandleEntitlementEvent(entitlement) - } -} - -func (b *Bot) onInterface(_ *discordgo.Session, i interface{}) { - err := b.State.OnInterface(b.Session, i) - if err != nil { - log.Error().Err(err).Msg("Failed to handle interface for state") - } -} diff --git a/embedg-server/bot/logo-512.png b/embedg-server/bot/logo-512.png deleted file mode 100644 index 5002a57d8c9b22aab27aef77fa073ad2837ab08d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9747 zcmeHtcTiJX_vi^76%lDFC49n_B1NQ04Jt)NK}0DcMG#PFA{_z&yeNhyHjpkTphySl z2^R@PYrfBEHiBKn}QQcJ_Ki?%IrCmX}wg;s$lPr6F|WcW()w-c>GS zLV}s-t)ia46#l~}dHwsD+dW>gI$tT57y3>=y;CHwE@x&Y0TCB#l;f&>RBHv8@0iz2 zWY4y~Zq1GS`k^JSWcmF^hW*SugOy*}&|2QIq8&quIE<6Lp6FdwGcNUaa;Q|1}@dyIIGr9jF{eP)~8-_6ez>>q&QY`Zq zeK}|vWabh8jPEnAsTy0YnfaXTL0W_bRjjf~^JyAVYV3-^ zse9Np;ueB+`GAm+|JG)qC=U?lq+-C3Pml05BIys#`q^rjpcdC#d-&6$qP24dA`LCZ zr9l}bkO{6I4s;`o7h>w@!C3f)i>85r-#*i^Ljd4?`y&Hk^}88csinSvSzK%C3L~?_ zInBy|%Xt9EdaJg^c23*<*?I-Cbc_q=IN*Pgiuc%O+9(2&y8E_rBvf||{=pzj$7kN-WbiJw8>EZHN`mmygWAU}hJI!2&!DbV`+&L51D z(2tBjOd?^g^2=j3YSAUukyk7cNuaw^M0u?a0%0= zsuBB5b+Y~g)D8{tw{6Q#gEQug_w;JWOPd@UT$@1i&-`a9xZr%A*BgY_KYd4BL?Kg+)pgtJW^>+iqdKed4*OWpYCt4Anjmd~? zM-?-5`~*dT1zX+GC-bSj66+$|g9h3vgGLG}QzUWC`Ufk-NIR+xV?I)v{XApbmuhb8 z!t4@@(>)Dwm%ZafeI<0j(yOY=!=2P<;l)_I9HSI?86mx3Z7Z+A?mb90G(5Nk2L$YZ z$?oH(3%7Jn9T{cKigzI%S{y(TJZ&e|Cr0vO?Z`e~#d$gDROosR0lInxB8K?<66lcb z`%<&o;rawKH0PGQ!X_b)NRN`W0+Am0yB0=>#dun`ujCFMr;a0dV%s!!rou^gwU4Ru zp7K?PoKmdKSo7F;g^+nQ<9aM(Ogv`ciY>oys*!88m5vE{l9${Q^Q8D2Rs-!}6PK*f zdcuT}_=31&xQ@Kva9^b(MF6OG$hY)r%w!)o{8@ zO*qEE9&|nwIE6UF$Nvq!EzXP1639i&&66iI@}6M&rKKy@hc6?-t!{{V_Y9Fd}R zOe^h!w~LVd5)nUs2yt;JyH(xoRM_1bIWE9iKKaYRXv~n5iDX7|@yq666=rLo!&6}n zJ0XH$Wm!^5KFO@J7h~RRm&G&YFhSFTZpr+z^i;WLryy}^FU=1e=-F_#&Yf(9_5J10 z|DIyPw&+%$x=tv5qLu_hEI&Blpp}Rg`XYC=9NYNS5N5)ew<9(tLzC}^njX540(89Y z%h8P3zNx)_z|*&FF!G!~qp%@LMGsQS?R7diY?vMs7rl+_NJf z<7ehxrHeMHY&ClE6iE>twPGXuh5ey3i*1`YOhUQ|O5r1Sf+<=A$jWOI z=?XlfUYnO+#qdcMtID>Jtzq4jfEt8orx`?Oe?DqYYqhoA82fZIxo(hkJNE%)3}<;r zSb!_$Jm|t}%lxRHjzr-EF`^d?Voq&k+0VIYKgYPq8XU8fOhlN?k$BDjaclQhwKm?I z3L+@`U~9c_yzwS-z^P!&7tW)!r}2_)qhAo-elwjM_IV59(s>KdR@+3hq&P2P{^MxS zq|+kiqMImBR36aL@8xs=8hiha{FD@@&S@WlbC`$z2%Ns-bZ5%g@G%~qfI;{@?<_cY zwy;qU=)WpEDk&o}$~{Y(Md+3(Z44NBgxsYcLw6RWfSQ<32`0R=Ly4Sps5s0M6$dV( zy{fsDjY#>j&VPinDdshoo|JK4f@B==?$!jls}t#PF#(x?hGV|aR+jNd1Zz^yb70)@ zx{CPOLaRM)CZQ}uh$STMTKR^skXhSi;V`Win$mM{zltx+q6L}2`Soo+=$o<+=7UeH zVowj2H(p%<3{XyHp=T`8Xpc+TWkUR)W1;PH)pxZ&DX%C%fbn-l3#2+<9ll(( zyP2xVi|gB;j>sNXG3;|bHvW##Ik;3Bd0xmwSm7LDea z9H5&OPo#+2=%3^(AGL;F`UXC=P;d7=o!07irTIjMT%4M13L_bcjos;PT14@uattps zxxC*wR)sRfeF;CFtdc*BX(Yvj7Z}7iN_8yM?_=eK<3aTG?k3u|a-0v>kP{bh%F|q+ zX<@dnX(hieV!-4)xo=E8NpjslhDQPNqb^{MgTNLXhP5nF9?HvM(1rp)$ERN-7&fx? z1d#&%QZY$XFrGL{DbcfQiQ90W&D$v;brH1!TI8(F(`X-y){vC zR#OCq@;pAPAfOP)>g2Q|TAP^dAB;KT>;QQHa8?uLdH%Vk(|}b zTj-|DYzKq&WWbEv16uMEVlkpxZf7#6kQpaE#JFkBp2MW~ETTd#hyYWhkxQb`Ge!s7i85)%fmNGenX0F9)BU$|JZj;;) zx9)YCqN3v1EH-cjKLze}wtgj1ZfHjL1})i{ev3pB1{F=xmEb{Q5?cKg1wxRW1riWo zDQo*t2bCRTG#fN@fjL?((XAyR$hG;N8Q!}Y@o!WiEsf!IyVQ8DkXF%!(@`{ z0*<6pWsMH+`N%LwC4qXo@(slHRv@OYG*7hLzRI2^3aZ})w7V0lk?{GInlnU$vxP1{ zG?<{XLB_{J5@BGKHq@Xi$@6#In<>XRjh5I%*cEVU`_HbXQoR#8PAD<`Qox!s1>Dwz zUK*Amd^ELK_^3F~4?8jHt8#nnJ)b;D#k`01s?Ml$bBoG6K;dG2LPz#fBUHq0_L;TG znuo_8e8*XOC7G=78iiLEuT zMtQ56Y(*DeU~|21W7CyI0VF@YHH**;vLd@9zgTgRtA!)-1H%+MfC>Wwd`cq&qTx8}wG?W>xoFyo8SdMgPtUNCI+_zpYagAY*^3%36%3zpP@W~_N6cmOk&Q0lN#pjcm?_n!6{Ng|R5lIbz^kr6LOAs~ zds(z(DS)}{`i@aLzyrwXx9jrW)s?qy`WC~f&)iG6GfM!>oJ-i)9p=ND^@1I6*?|=- zs?G zf<(MFmyV|C25oZ2HPvYw2ZJ_TAH{5%=5_wuLIE>v`ac)dM_F2Wpz%FI7SYIK>M7`m zi3_SnzIc#a_z1q1{!$E*?OKi%7v?%~R&iyXwLSCWc4G_2IkuSL69D?P5wSeZXL+YK zs<{08pfg%e`xyoixt&lJ8jLft!hE3<*z^GA$z}E%D=uI|+uVn_s3G%|w`;<#g>{@q z`ZM*ak>_y7r2Eh+a-F`8m72fA2 z_}l-c>w&__3IL`b%82`))Y9s>bxqiA4kCwWDVvhps}R-p>>-vG6g27U z-*aKHv9!_4)UFFOdmU)FVqD=C1<6NSM?0 zr(?ghru7WDdkpO2aEn21afIku$^+xrDD-;4F6f%(LzxcDukLN`neh|in??jM>1yFT zjjFqT8Ip$f7~rFx%{qy7X}L)qY@fM@H*Q>jLZ#Albb#B_F4t-=>b4|e!G(VtFIIT7lGjm718+sOQHXJB~>fN3W(!T46{8H!hzmMgRIZBBM%|~`$vmWEPPz^H-+o~g}Hw#?&vT)Ixp`5*X zUhrfOXTAdA{K1ibeqmN!0NOh3@(5jHSz7(T3K?u!&f-5;7!C7yQ^m4fdnYefXWV& z)Ob&@-5iMfTa35#2tz~qD{AVP;$fMx%*}W>7Rfa;HbRJ;It|9=fbgOE{ zr%alPW==fvI+nHiwT(85p{m3hq3+*?ua=N$Jy(e2sriA2IWw*YxGb2WIZCLWne;JYf}8cuK*-;-e*vZi%KPkU8c2HuP#%Bk%hs*6ma@|ti3k=kX_=od zW~-W)>`>);$YI*}s2gk9ahm2{VU*?Sn~=Og!QdRQ%cKbkc*?tBn1TQ~z!chBvi}~~ zDDfs!kF@apttg#6c*AR|;XF5=UXL7Y%{ z)NPfk(w;&1*dPuY68$w`Um>G(N&pz@y6MUG&_nC38A@A1Pao5m4PNojtJu_^IiABV zSR|c3gkUTrlEsq43i-dt<=9U zxWr!8{yk&!i>iBS2D$1MmmeLYp7AVFRx_5;)>fZ2AQ{7M~$6>a4Errvy`UgL* z=z-YOZ_b3(W8$MK>C~98=#?xq)X#7Drz9v?xN2qi==+r8>9-y(-nq4bdrJ9+E73Jk z>BQ+!M?*LvG7uoj)b>G|p@(goKYETb#=3RkhnqHSWl;#zYV9o1Xnc$GaT|gW`zeZd zI_lYw1T%DxSR*w`-1&TP=S%bnV%Op))$`{XubjS+lLx%TiRY1U`Noa`)ppFb9D})L zdy~qDQ-pT@aQq9t&E+S`F)OVni8G~Y=>^RDr(b`%|Jqpg;jw4@0FTXFG!BiZy?_527io%W^Qff0rFCgVP3H<`{}!@ZlQ1;p%9E-%_-o`%G! zJBX#{$bQ7+rH|rx<31>}zwV{Ee62LUS2lr%uQQ!>vh8MKcUP^UNBqu7qTk28E-Gvz zrHBV@x4)Pl*C*A;YjCR4-#o7e{`FpbAP`<2UbZdHt_()g-R+`4tm6PpHE5c zVbV$AXt+Y!8TT_`j;CJDF}z$%%OfQAvr$3Bh+G82>{E6>W1ZYNBm^3h$nKVUh_FUr zca3C48d2R@20`nbJAI&DmLlo`Z-;x{Jh|X-696-$&%UY)lvo!HU|D`bJx znwYz7O2~$&?(~3yBof|0L}NO%@RqXMv~u;`GpkQ!AbQ`|s&ccU)H_V)KNN7+7l4`8 z#~aegA3EA?v2LeQotJ`r008w1Y1`M$(T^VBf{hIXYhEH8{c-!WfDH#gVT#ep77JP__bFifUD1 zFJsGzHg2a6lZ@^vxK`sj+FWFZ*3N^KL!pg0rlHBGI_h}3RweW#w3+eE=LQjsZ$x3Y zGh$tjCUSY>Z#4?L*Z^@RnT=s1xZ?paO|nqqaIfFBSKha!ZoH^+ZrZt*;J@n{`BJ!d zm-5Q9C1&ZNE?2_e^(9=mp6npP4bPxC8Bu>09n-`?#{}M(-ga~WSG3;qxMvH;8`J{Z0Ecgg{my}Y%p5#&*P)$F-zLsC#1=9HfoIhoml5<( zqO)eHWAwLr0((zx{}c|)NfIaS+S@?DY!ki~iYR16ww~)m2DCSMb6C#U z#+B9j^=(qo75;cIjgJ;ISI~lqq z3jvb;xzv4cENvhv-94F87!GE6`YmW?Wgl&=cHz_hW(;B|r_De9TJceGMUGG7ks$(X z$%PBJ`tkbly&~lMF)aohqyt+QrXQ4+(kt%wn4`0IHzBI*N1)pUe8p|}a{K-a6~W=a z!Y&}eIUBgS!<+AMVvxU@CL9Hx86(U>zVMzv{16}l`Njy^0kVnNOR_2!xwXf9$!hFJ z?Ee6O4Om4m^6z#Ju9sH`K`hyUoKFn&SOSYV zt!L(!E7!Nq0y%ctoEPOKvvt)+_hv0knom=_i%0mm{Lba;9WQXy&*O0);Q@3t(tEL) z7TWjoOb)x-Wp9In51jra@ZRB&3BC6V}>8I+QXFLCT(^1(rs2*H!u83M=7xfeo4=@jdO5<|snrIsLj5z^Uj7 z)9-<-sP13;6lIpO2uEegi(_%i^U;UBtpNA^zeKjlS#3M}t&N71%@3j3RvBaV`w4y` zU}N(KoYwM?xpWQaZ&W=-bvvBu<~Fj-{q_mi%xdZ8+h$hu1&m%suyGay>TQGEC?B)hmR<}uEn?@p9_4;=w*vW>1yOl5Bgv&- zsIRG_YcJQD2j9Bd);mdL=y|xOa~DVTV&$bKX666Bty_Bul)5N(O|qzlRE@1PHfxrQOQEv1m!!~kOs;IUr zxYQax(6jZ_T2794q-vYe;HDu-(^+qp%NBN#iS$qH9Bcy*jL*dPz6yOE@v`4LgtFhU z?$^WMXYcF}ds=qT{LU!lL_(Xxn2^{=s;lu%SlI!A;MLw*zm@rCWAZDa8C9{W*;ebw z1wu~o?zq3*NyZpk&x#h>)g}ro6>rj{xEO~sA$rpU%;?^g7kAH91_ zg_0We%hSdzMNK(C>Gx8Ge~q^>U;ZZD8j4}!REFVOKgwKK%M_6YuiIKYj@Xd58Ws>6 z!3JZxL8*oS5Rw6>!^gW6F~2N0cGQn!h{%BN99BxGZrqn>dO;0~i7jM+C9K33oGoIl zwBTt^U}erEO}QE1X}1D^&!!T)6gRB;=@+2@!25OYTYz8U2+7@S#ggzCo~Wl_Kg+tt zc#V~5#-eMO&w_@3H%<*K&9$@}k}ATwGI5L_GgSxo8gSn4#`7w}c42106EU(|JTa!A zqpWDc1d<1ATNlQAeMNP5<4r(UEu|TgjQ97kWbM?I7RXkvK8f9%d}irdkVGHFT6mHN zEZ9R&cz~ZD{yn$QhLaoZq9w}#Keo*+1&UJ8r)Y1n_63o$Ac}sN46U{W zNs~Ps(AWSC5@}G;p%o)M4z`}7TwN=FSK-Nd4}D-RusyfC)ncJ`A7-d~49%YSz8#xR z=JhrO;_^XLB;~XbNXOk+v@H)<+#3;~P*S{+X8geEdlkdu*MLQ7I^Po5$J=_@CQTj- zG*Vs(F9>vixhD$(L+2^vdD~V3sknK|l}hDy2J1rCtDshT)ofeoBN<~oxK$3|d>)9a ze4U9w$1?PaK*u|h=r$(oXAA*a>}72Wk|uRH(y92_JHLeMUxD33;W@hra zTP@af3S{GtbY`x%E8W-2b+?1Me+BiT1I}dIwyq}+H<3IVX^`Mc;B7G&m>%ADK+g-e z%nJAGLx4%uL7!$Vx1eNvVBj8`!;vwaYOx({)wKeX%2G+pzGxqs((H$W=-AbxAV<87 zBmz_wj0d<|`Y@f1lg}O)fO~-Usd_?#l~aa~O=B?PBNYKt=YK=s3Jm5&Os4&N6eVA9 z!Y*q=Tm2u8aK-J^@&S`-xT4raVKB6cjAq81svXWNK^P{(AKuulq5J!Q;WED4D|$V+ ztXaPQm7iR#OupzVUc=0IIk%`leR^Y!@X$Rl?qpIdoVKLqJ3E$7av@F&~$$KAEEU(0Qh&&+}f=2oO{Cm0x#ZSN&o-= diff --git a/embedg-server/bot/rest/cache.go b/embedg-server/bot/rest/cache.go deleted file mode 100644 index e391c492b..000000000 --- a/embedg-server/bot/rest/cache.go +++ /dev/null @@ -1,115 +0,0 @@ -package rest - -import ( - "context" - "fmt" - "io" - "sync" - "time" - - "github.com/jellydator/ttlcache/v3" - "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/util" -) - -func memberCacheKey(guildID string, userID string) string { - return fmt.Sprintf("member:%s:%s", guildID, userID) -} - -// pendingRequest represents an ongoing request for a specific cache key -type pendingRequest struct { - waitGroup sync.WaitGroup - result interface{} - err error -} - -type RestClientWithCache struct { - session *discordgo.Session - - memberCache *ttlcache.Cache[string, *discordgo.Member] - - // Single mutex to protect all pending request maps - pendingMutex sync.RWMutex - pendingReqs map[string]*pendingRequest -} - -func NewRestClientWithCache(session *discordgo.Session) *RestClientWithCache { - rest := &RestClientWithCache{ - session: session, - memberCache: ttlcache.New(ttlcache.WithTTL[string, *discordgo.Member](5 * time.Minute)), - pendingReqs: make(map[string]*pendingRequest), - } - - go rest.memberCache.Start() - - return rest -} - -// getOrSet executes a fetch function with concurrency control -func getOrSet[T any](c *RestClientWithCache, cacheKey string, cache *ttlcache.Cache[string, T], fetchFunc func() (T, error)) (T, error) { - var zero T - - // Check cache first - cacheItem := cache.Get(cacheKey) - if cacheItem != nil { - return cacheItem.Value(), nil - } - - // Check if there's already a pending request - c.pendingMutex.Lock() - if pending, exists := c.pendingReqs[cacheKey]; exists { - c.pendingMutex.Unlock() - pending.waitGroup.Wait() - if pending.err != nil { - return zero, pending.err - } - return pending.result.(T), nil - } - - // Create new pending request - pending := &pendingRequest{} - pending.waitGroup.Add(1) - c.pendingReqs[cacheKey] = pending - c.pendingMutex.Unlock() - - // Perform the actual request - result, err := fetchFunc() - - // Clean up pending request and set result - c.pendingMutex.Lock() - delete(c.pendingReqs, cacheKey) - c.pendingMutex.Unlock() - - pending.result = result - pending.err = err - pending.waitGroup.Done() - - if err != nil { - return zero, err - } - - cache.Set(cacheKey, result, 0) - return result, nil -} - -func (c *RestClientWithCache) Request(ctx context.Context, method string, url string, body io.Reader, options ...discordgo.RequestOption) ([]byte, error) { - options = append(options, discordgo.WithContext(ctx)) - return c.session.Request(method, url, body, options...) -} - -func (c *RestClientWithCache) GuildMember(ctx context.Context, guildID string, userID string) (*discordgo.Member, error) { - return getOrSet(c, memberCacheKey(guildID, userID), c.memberCache, func() (*discordgo.Member, error) { - member, err := c.session.GuildMember(guildID, userID, discordgo.WithContext(ctx)) - if err != nil { - if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMember, discordgo.ErrCodeUnknownGuild, discordgo.ErrCodeMissingAccess) { - return nil, ErrNotFound - } - return nil, err - } - return member, nil - }) -} - -func (c *RestClientWithCache) InvalidateMemberCache(guildID string, userID string) { - c.memberCache.Delete(memberCacheKey(guildID, userID)) -} diff --git a/embedg-server/bot/rest/client.go b/embedg-server/bot/rest/client.go deleted file mode 100644 index 77d482251..000000000 --- a/embedg-server/bot/rest/client.go +++ /dev/null @@ -1,17 +0,0 @@ -package rest - -import ( - "context" - "errors" - "io" - - "github.com/merlinfuchs/discordgo" -) - -var ErrNotFound = errors.New("not found") - -type RestClient interface { - Request(ctx context.Context, method string, url string, body io.Reader, options ...discordgo.RequestOption) ([]byte, error) - - GuildMember(ctx context.Context, guildID string, userID string) (*discordgo.Member, error) -} diff --git a/embedg-server/bot/sharding/manager.go b/embedg-server/bot/sharding/manager.go deleted file mode 100644 index 7538d9d5b..000000000 --- a/embedg-server/bot/sharding/manager.go +++ /dev/null @@ -1,281 +0,0 @@ -package sharding - -import ( - "strconv" - "sync" - "time" - - "github.com/merlinfuchs/discordgo" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" -) - -// Manager facilitates the management of Shards. -type ShardManager struct { - sync.RWMutex - - // Discord gateway. - Session *discordgo.Session - // Discord intent. - Intents discordgo.Intent - // Shards managed by this Manager. - Shards []*Shard - // Total Shard count. - ShardCount int - // Number of shards that can identify concurrently. - ShardConcurrency int - - Presence *discordgo.GatewayStatusUpdate - FirstShardID int - LastShardID int - - // Event handlers. - handlers []interface{} - - // Discord bot token. - token string - - stopped bool - stopCh chan struct{} -} - -func (m *ShardManager) ShardList() []*Shard { - m.RLock() - defer m.RUnlock() - - return m.Shards -} - -// AddHandler registers an event handler for all Shards. -func (m *ShardManager) AddHandler(handler interface{}) { - m.Lock() - defer m.Unlock() - - m.handlers = append(m.handlers, handler) - - for _, shard := range m.Shards { - shard.AddHandler(handler) - } -} - -// GuildCount returns the amount of guilds that a Manager's Shards are -// handling. -func (m *ShardManager) GuildCount() (count int) { - m.RLock() - defer m.RUnlock() - - for _, shard := range m.Shards { - count += shard.GuildCount() - } - - return -} - -// New creates a new Manager with the recommended number of shards. -// After calling New, call Start to begin connecting the shards. -// -// Example: -// mgr := shards.New("Bot TOKEN") -func New(token string) (cluster *ShardManager, err error) { - // Initialize the Manager with provided bot token. - cluster = &ShardManager{ - token: token, - stopCh: make(chan struct{}), - } - - // Initialize the gateway. - cluster.Session, err = discordgo.New(token) - if err != nil { - return nil, err - } - - cluster.Session.LogLevel = viper.GetInt("discord.log_level") - // The session will never connect to the gateway, but we use it for global state - cluster.Session.StateEnabled = true - - // Set recommended shard count. - resp, err := cluster.Session.GatewayBot() - if err != nil { - return - } - - if resp.Shards < 1 { - cluster.ShardCount = 1 - } else { - cluster.ShardCount = resp.Shards - } - - if resp.SessionStartLimit.MaxConcurrency < 1 { - cluster.ShardConcurrency = 1 - } else { - cluster.ShardConcurrency = resp.SessionStartLimit.MaxConcurrency - } - - if cluster.LastShardID == 0 { - cluster.LastShardID = cluster.ShardCount - 1 - } - - return -} - -// SetShardCount sets the shard count. -// The new shard count won't take effect until the Manager is restarted. -func (m *ShardManager) SetShardCount(count int) { - m.Lock() - defer m.Unlock() - - if count > 0 { - m.ShardCount = count - } -} - -// SessionForDM returns the proper session for sending and receiving -// DM's. -func (m *ShardManager) SessionForDM() *discordgo.Session { - m.RLock() - defer m.RUnlock() - - // Per Discord documentation, Shard 0 is the only shard which can - // send and receive DM's. - // - // See https://discord.com/developers/docs/topics/gateway#sharding - return m.Shards[0].Session -} - -// SessionForGuild returns the proper session for the specified guild. -func (m *ShardManager) SessionForGuild(guildID int64) *discordgo.Session { - m.RLock() - defer m.RUnlock() - - // Formula to determine which shard handles a guild, from Discord - // docs. - // - // See https://discord.com/developers/docs/topics/gateway#sharding - return m.Shards[(guildID>>22)%int64(m.ShardCount)].Session -} - -// Restart restarts the Manager, and rescales if necessary, all with -// zero downtime. -func (m *ShardManager) Restart() (nMgr *ShardManager, err error) { - // Lock the old Manager for reading. - m.RLock() - - // Create a new Manager using our token. - mgr, err := New(m.token) - if err != nil { - m.RUnlock() - return m, err - } - - mgr.FirstShardID = m.FirstShardID - mgr.LastShardID = m.LastShardID - mgr.ShardCount = m.ShardCount - mgr.ShardConcurrency = m.ShardConcurrency - mgr.Intents = m.Intents - - // We have no need to lock the old Manager at this point, and - // starting the new one will take some time. - m.RUnlock() - - // Start the new Manager so that it can begin handling events. - err = mgr.Start() - if err != nil { - return m, err - } - - // Apply the same handlers. - for _, handler := range m.handlers { - mgr.AddHandler(handler) - } - - // Shutdown the old Manager. The new Manager is already handling - // events. - m.Shutdown() - - return mgr, nil -} - -// Start starts the Manager. -func (m *ShardManager) Start() (err error) { - m.Lock() - defer m.Unlock() - - // Initialize Shards. - m.Shards = []*Shard{} - for i := m.FirstShardID; i <= m.LastShardID; i++ { - m.Shards = append(m.Shards, &Shard{ - ID: i, - ShardCount: m.ShardCount, - Presence: m.Presence, - }) - } - - // Start Shards concurrently if allowed. - // TODO: fix shutdown and locking mechanism - for d := 0; d < m.ShardConcurrency; d++ { - go func(divider int) { - // Add event handlers to Shards and connect them. - for id, shard := range m.Shards { - if m.stopped { - return - } - - if id%m.ShardConcurrency != divider { - continue - } - - // Add handlers to this shard. - for _, handler := range m.handlers { - shard.AddHandler(handler) - } - // Connect shard. - err = shard.Start(m.token, m.Intents) - if err != nil { - log.Error().Err(err).Msg("error starting shard") - return - } - // Ratelimit shard connections. - if id != len(m.Shards)-1 { - time.Sleep(time.Second * 5) - } - } - }(d) - } - - go m.monitorShards() - return -} - -// Shutdown gracefully terminates the Manager. -func (m *ShardManager) Shutdown() { - m.Lock() - defer m.Unlock() - - m.stopped = true - close(m.stopCh) - - wg := sync.WaitGroup{} - for _, shard := range m.Shards { - wg.Add(1) - go func(s *Shard) { - if err := s.Stop(); err != nil { - log.Error().Err(err).Msg("error stopping shard") - } - wg.Done() - }(shard) - } - - wg.Wait() -} - -func (m *ShardManager) ShardForGuild(guildID string) *Shard { - gid, _ := strconv.ParseUint(guildID, 10, 64) - shardID := (gid >> 22) % uint64(m.ShardCount) - - for _, shard := range m.Shards { - if shard.ID == int(shardID) { - return shard - } - } - return nil -} diff --git a/embedg-server/bot/sharding/monitor.go b/embedg-server/bot/sharding/monitor.go deleted file mode 100644 index a83204b78..000000000 --- a/embedg-server/bot/sharding/monitor.go +++ /dev/null @@ -1,62 +0,0 @@ -package sharding - -import ( - "time" - - "github.com/rs/zerolog/log" -) - -func (m *ShardManager) monitorShards() { - ticker := time.NewTicker(time.Minute * 30) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.checkShards() - case <-m.stopCh: - return - } - } -} - -func (m *ShardManager) checkShards() { - m.RLock() - defer m.RUnlock() - - log.Info().Msg("Checking for suspicious shards") - - for _, shard := range m.Shards { - if shard.Session == nil { - go m.restartShard(shard) - continue - } - - if time.Since(shard.Session.LastHeartbeatAck) > 15*time.Minute { - go m.restartShard(shard) - continue - } - - if time.Since(shard.Session.LastHeartbeatSent) > 10*time.Second && - shard.Session.LastHeartbeatAck.Before(shard.Session.LastHeartbeatSent) { - go m.restartShard(shard) - continue - } - } -} - -func (m *ShardManager) restartShard(shard *Shard) { - log.Info().Int("shard_id", shard.ID).Msg("Restarting suspicious shard") - - if err := shard.Kill(); err != nil { - log.Error().Err(err).Msg("Failed to stop suspicious shard for reconnect") - } - - log.Info().Int("shard_id", shard.ID).Msg("Suspicious shard stopped") - - if err := shard.Start(m.token, m.Intents); err != nil { - log.Error().Err(err).Msg("Failed to start suspicious shard for reconnect") - } - - log.Info().Int("shard_id", shard.ID).Msg("Suspicious shard restarted") -} diff --git a/embedg-server/bot/sharding/shard.go b/embedg-server/bot/sharding/shard.go deleted file mode 100644 index 9ccc25827..000000000 --- a/embedg-server/bot/sharding/shard.go +++ /dev/null @@ -1,118 +0,0 @@ -package sharding - -import ( - "sync" - - "github.com/merlinfuchs/discordgo" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" -) - -// A Shard represents a shard. -type Shard struct { - sync.RWMutex - - // The Discord session handling this Shard. - Session *discordgo.Session - // This Shard's ID. - ID int - // Total Shard count. - ShardCount int - Presence *discordgo.GatewayStatusUpdate - - // Event handlers. - handlers []interface{} -} - -// AddHandler registers an event handler for a Shard. -// -// Shouldn't be called after Init or results in undefined behavior. -func (s *Shard) AddHandler(handler interface{}) { - s.Lock() - defer s.Unlock() - - s.handlers = append(s.handlers, handler) -} - -// GuildCount returns the amount of guilds that a Shard is handling. -func (s *Shard) GuildCount() (count int) { - s.RLock() - defer s.RUnlock() - - if s.Session != nil { - s.Session.State.RLock() - count += len(s.Session.State.Guilds) - s.Session.State.RUnlock() - } - - return -} - -// Init initializes a shard with a bot token, its Shard ID, the total -// amount of shards, and a Discord intent. -func (s *Shard) Start(token string, intent discordgo.Intent) (err error) { - s.Lock() - defer s.Unlock() - - // Create the session. - s.Session, err = discordgo.New(token) - if err != nil { - return - } - - s.Session.LogLevel = viper.GetInt("discord.log_level") - - s.Session.SyncEvents = false - - // Shard the session. - s.Session.ShardCount = s.ShardCount - s.Session.ShardID = s.ID - - // Identify our intent. - s.Session.Identify.Intents = intent - - // State is handled outside of the shard - s.Session.StateEnabled = false - - if s.Presence != nil { - s.Session.Identify.Presence = *s.Presence - } - - // Add handlers to the session. - for _, handler := range s.handlers { - s.Session.AddHandler(handler) - } - - // Connect the shard. - err = s.Session.Open() - - return -} - -// Stop stops a shard. -func (s *Shard) Stop() (err error) { - s.Lock() - defer s.Unlock() - - // Close the session. - if s.Session != nil { - err = s.Session.Close() - } - - return -} - -func (s *Shard) Kill() (err error) { - log.Info().Int("shard_id", s.ID).Msg("Killing shard") - s.Lock() - defer s.Unlock() - log.Info().Int("shard_id", s.ID).Msg("Shard locked") - - if s.Session != nil { - err = s.Session.Kill() - } - - log.Info().Int("shard_id", s.ID).Msg("Shard killed") - - return -} diff --git a/embedg-server/bot/util.go b/embedg-server/bot/util.go deleted file mode 100644 index 855182f90..000000000 --- a/embedg-server/bot/util.go +++ /dev/null @@ -1,335 +0,0 @@ -package bot - -import ( - "context" - "database/sql" - "fmt" - - "github.com/merlinfuchs/discordgo" - "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" - "github.com/merlinfuchs/embed-generator/embedg-server/util" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" - "github.com/vincent-petithory/dataurl" -) - -// SendMessageToChannel sends a message to a channel, either using a webhook or using the configured custom bot. -func (b *Bot) SendMessageToChannel(ctx context.Context, channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) { - channel, err := b.State.Channel(channelID) - if err != nil { - return nil, fmt.Errorf("Failed to get channel: %w", err) - } - - useCustomBot := false - customBot, err := b.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID) - if err != nil { - if err != sql.ErrNoRows { - log.Error().Err(err).Msg("failed to get custom bot for message username and avatar") - } - } else if params.Username == "" && params.AvatarURL == "" { - useCustomBot = true - } else { - if params.Username == "" { - params.Username = customBot.UserName - } - if params.AvatarURL == "" { - params.AvatarURL = util.DiscordAvatarURL(customBot.UserID, customBot.UserDiscriminator, customBot.UserAvatar.String) - } - } - - var newMessage *discordgo.Message - - if useCustomBot { - session, err := getCustomBotSession(&customBot) - if err != nil { - return nil, fmt.Errorf("Failed to create custom bot session: %w", err) - } - - newMessage, err = session.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ - Content: params.Content, - Embeds: params.Embeds, - TTS: params.TTS, - Components: params.Components, - Files: params.Files, - AllowedMentions: params.AllowedMentions, - Flags: params.Flags, - }, discordgo.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("Failed to send message: %w", err) - } - } else { - webhook, err := b.FindWebhookForChannel(ctx, channelID) - if err != nil { - return nil, fmt.Errorf("Failed to find webhook: %w", err) - } - - threadID := "" - if webhook.ChannelID != channelID { - // The webhook was requested for a thread, but belongs to the parent channel - threadID = channelID - } - - if threadID != "" { - newMessage, err = b.Session.WebhookThreadExecute( - webhook.ID, - webhook.Token, - true, - threadID, - params, - discordgo.WithContext(ctx), - ) - } else { - newMessage, err = b.Session.WebhookExecute( - webhook.ID, - webhook.Token, - true, - params, - discordgo.WithContext(ctx), - ) - } - if err != nil { - return nil, fmt.Errorf("Failed to send message: %w", err) - } - } - - return newMessage, nil -} - -// EditMessageInChannel edits a message in a channel, either sent by a webhook or by the configured custom bot. -func (b *Bot) EditMessageInChannel(ctx context.Context, channelID string, messageID string, params *discordgo.WebhookEdit) (*discordgo.Message, error) { - channel, err := b.State.Channel(channelID) - if err != nil { - return nil, fmt.Errorf("Failed to get channel: %w", err) - } - - useCustomBot := false - customBot, err := b.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID) - if err != nil { - if err != sql.ErrNoRows { - log.Error().Err(err).Msg("failed to get custom bot for message username and avatar") - } - } - - msg, err := b.Session.ChannelMessage(channelID, messageID) - if err != nil { - return nil, fmt.Errorf("Failed to get message from channel: %w", err) - } - - if msg.WebhookID == "" { - if msg.Author.ID == customBot.UserID { - useCustomBot = true - } else { - return nil, fmt.Errorf("Message wasn't sent by a webhook and can therefore not be edited.") - } - } - - var newMessage *discordgo.Message - - if useCustomBot { - session, err := getCustomBotSession(&customBot) - if err != nil { - return nil, fmt.Errorf("Failed to create custom bot session: %w", err) - } - - newMessage, err = session.ChannelMessageEditComplex(&discordgo.MessageEdit{ - Channel: channelID, - ID: messageID, - Content: params.Content, - Embeds: params.Embeds, - Components: params.Components, - Files: params.Files, - AllowedMentions: params.AllowedMentions, - Attachments: params.Attachments, - }, discordgo.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("Failed to edt message: %w", err) - } - } else { - webhook, err := b.GetWebhookForChannel(ctx, channelID, msg.WebhookID) - if err != nil { - return nil, fmt.Errorf("Failed to get webhook: %w", err) - } - - threadID := "" - if webhook.ChannelID != channelID { - // The webhook was requested for a thread, but belongs to the parent channel - threadID = channelID - } - - if threadID != "" { - newMessage, err = b.Session.WebhookThreadMessageEdit( - webhook.ID, - webhook.Token, - threadID, - messageID, - params, - discordgo.WithContext(ctx), - ) - } else { - newMessage, err = b.Session.WebhookMessageEdit( - webhook.ID, - webhook.Token, - messageID, - params, - discordgo.WithContext(ctx), - ) - } - if err != nil { - return nil, fmt.Errorf("Failed to edit message: %w", err) - } - } - - return newMessage, nil -} - -// FindWebhookForChannel returns a webhook for the given channel that was created by the bot or the configured custom bot. -func (b *Bot) FindWebhookForChannel(ctx context.Context, channelID string) (*discordgo.Webhook, error) { - channel, err := b.State.Channel(channelID) - if err != nil { - return nil, fmt.Errorf("Failed to get channel: %w", err) - } - - if channel.Type == discordgo.ChannelTypeGuildNewsThread || channel.Type == discordgo.ChannelTypeGuildPublicThread || channel.Type == discordgo.ChannelTypeGuildPrivateThread { - channel, err = b.State.Channel(channel.ParentID) - if err != nil { - return nil, fmt.Errorf("Failed to get parent channel: %w", err) - } - } - - customBot, err := b.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID) - if err != nil && err != sql.ErrNoRows { - return nil, fmt.Errorf("Failed to get custom bot: %w", err) - } - - session := b.Session - if customBot.Token != "" { - session, err = discordgo.New("Bot " + customBot.Token) - if err != nil { - return nil, fmt.Errorf("Failed to create custom bot session: %w", err) - } - } - - webhooks, err := session.ChannelWebhooks(channel.ID, discordgo.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("Failed to get webhooks: %w", err) - } - - clientID := viper.GetString("discord.client_id") - if customBot.ApplicationID != "" { - clientID = customBot.ApplicationID - } - - for _, webhook := range webhooks { - if webhook.ApplicationID == clientID { - return webhook, nil - } - } - - username := "Embed Generator" - if customBot.UserName != "" { - username = customBot.UserName - } - - logoDataURL := dataurl.New(logoFile, "image/png") - webhook, err := session.WebhookCreate(channel.ID, username, logoDataURL.String(), discordgo.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("Failed to create webhook: %w", err) - } - - return webhook, nil -} - -// GetWebhookForChannel returns the webhook for the given channel if available. -func (b *Bot) GetWebhookForChannel(ctx context.Context, channelID string, webhookID string) (*discordgo.Webhook, error) { - channel, err := b.State.Channel(channelID) - if err != nil { - return nil, fmt.Errorf("Failed to get channel: %w", err) - } - - if channel.Type == discordgo.ChannelTypeGuildNewsThread || channel.Type == discordgo.ChannelTypeGuildPublicThread || channel.Type == discordgo.ChannelTypeGuildPrivateThread { - channel, err = b.State.Channel(channel.ParentID) - if err != nil { - return nil, fmt.Errorf("Failed to get parent channel: %w", err) - } - } - - var webhook *discordgo.Webhook - - // First retrieve the webhooks with the main bot session - webhooks, err := b.Session.ChannelWebhooks(channel.ID, discordgo.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("Failed to get webhooks: %w", err) - } - - for _, w := range webhooks { - if w.ID == webhookID { - webhook = w - } - } - - // We have found the webhook, but it belongs to another application - // so let's try with the custom bot session if any - if webhook != nil && webhook.Token == "" { - customBot, err := b.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID) - if err != nil && err != sql.ErrNoRows { - return nil, fmt.Errorf("Failed to get custom bot: %w", err) - } - - if customBot.Token != "" { - session, err := discordgo.New("Bot " + customBot.Token) - if err != nil { - return nil, fmt.Errorf("Failed to create custom bot session: %w", err) - } - - webhooks, err := session.ChannelWebhooks(channel.ID, discordgo.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("Failed to get webhooks: %w", err) - } - - for _, w := range webhooks { - if w.ID == webhookID { - webhook = w - } - } - } - } - - if webhook == nil { - return nil, fmt.Errorf("No webhook found that matches the given ID.") - } - - if webhook.Token == "" { - return nil, fmt.Errorf("The webhook belongs to another application and can't be used by Embed Generator.") - } - - return webhook, nil -} - -// GetSessionForGuild returns the session for the given guild. -// If a custom bot is configured for the guild, the token of the custom bot will be used to create the session. -func (b *Bot) GetSessionForGuild(ctx context.Context, guildId string) (*discordgo.Session, error) { - customBot, err := b.pg.Q.GetCustomBotByGuildID(ctx, guildId) - if err != nil && err != sql.ErrNoRows { - return nil, fmt.Errorf("Failed to get custom bot: %w", err) - } - - session := b.Session - if customBot.Token != "" { - return getCustomBotSession(&customBot) - } - - return session, nil -} - -func getCustomBotSession(b *pgmodel.CustomBot) (*discordgo.Session, error) { - if b.Token == "" { - return nil, fmt.Errorf("No token available for custom bot") - } - - session, err := discordgo.New("Bot " + b.Token) - if err != nil { - return nil, fmt.Errorf("Failed to create custom bot session: %w", err) - } - - return session, nil -} diff --git a/embedg-server/custom_bots/bot.go b/embedg-server/custom_bots/bot.go index dddc249b3..fc288e5dd 100644 --- a/embedg-server/custom_bots/bot.go +++ b/embedg-server/custom_bots/bot.go @@ -3,8 +3,6 @@ package custom_bots import ( "context" "fmt" - "log/slog" - "strconv" "time" "github.com/disgoorg/disgo" @@ -27,6 +25,7 @@ type CustomBot struct { func NewCustomBot(token string, presence CustomBotPresence) (*CustomBot, error) { client, err := disgo.New(token, bot.WithShardManagerConfigOpts( + sharding.WithShardCount(1), sharding.WithAutoScaling(false), sharding.WithGatewayConfigOpts( gateway.WithIntents(), @@ -41,12 +40,11 @@ func NewCustomBot(token string, presence CustomBotPresence) (*CustomBot, error) cache.WithCaches(), ), bot.WithEventListenerFunc(func(e *events.Ready) { - slog.Info( - "Shard is ready", - slog.String("shard_id", strconv.Itoa(e.ShardID())), - slog.String("user_id", e.User.ID.String()), - slog.String("username", e.User.Username), - ) + log.Info(). + Int("shard_id", e.ShardID()). + Str("user_id", e.User.ID.String()). + Str("username", e.User.Username). + Msg("Custom bot has connected to the gateway and is ready") }), ) if err != nil { diff --git a/embedg-server/embedg/bot.go b/embedg-server/embedg/bot.go index 51feeb584..3da93c272 100644 --- a/embedg-server/embedg/bot.go +++ b/embedg-server/embedg/bot.go @@ -3,8 +3,6 @@ package embedg import ( "context" "fmt" - "log/slog" - "strconv" "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" @@ -18,6 +16,7 @@ import ( actionsparser "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" + "github.com/rs/zerolog/log" ) type EmbedGeneratorConfig struct { @@ -31,7 +30,6 @@ type EmbedGenerator struct { client *bot.Client clientRouter handler.Router - rest *rest.RestClient pg *postgres.PostgresStore actionHandler *actionshandler.ActionHandler @@ -46,6 +44,7 @@ func NewEmbedGenerator( clientRouter := handler.New() client, err := disgo.New(cfg.DiscordToken, + bot.WithRest(rest.NewRestClient(cfg.DiscordToken)), bot.WithShardManagerConfigOpts( sharding.WithAutoScaling(false), sharding.WithGatewayConfigOpts( @@ -64,7 +63,6 @@ func NewEmbedGenerator( bot.WithEventManagerConfigOpts( bot.WithAsyncEventsEnabled(), ), - bot.WithRestClient(rest.NewRestClient(cfg.DiscordToken)), bot.WithCacheConfigOpts( cache.WithCaches( cache.FlagGuilds, @@ -74,12 +72,11 @@ func NewEmbedGenerator( ), ), bot.WithEventListenerFunc(func(e *events.Ready) { - slog.Info( - "Shard is ready", - slog.String("shard_id", strconv.Itoa(e.ShardID())), - slog.String("user_id", e.User.ID.String()), - slog.String("username", e.User.Username), - ) + log.Info(). + Int("shard_id", e.ShardID()). + Str("user_id", e.User.ID.String()). + Str("username", e.User.Username). + Msg("Embed Generator has connected to the gateway and is ready") }), bot.WithEventListeners(clientRouter), ) diff --git a/embedg-server/embedg/commands.go b/embedg-server/embedg/commands.go index f426c4934..69c054bc0 100644 --- a/embedg-server/embedg/commands.go +++ b/embedg-server/embedg/commands.go @@ -3,7 +3,6 @@ package embedg import ( "encoding/json" "fmt" - "log/slog" "regexp" "github.com/disgoorg/disgo/bot" @@ -18,6 +17,7 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/actions" actionhandler "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" "github.com/merlinfuchs/embed-generator/embedg-server/util" + "github.com/rs/zerolog/log" "github.com/spf13/viper" ) @@ -382,10 +382,7 @@ func (g *EmbedGenerator) handleImageIconCommand(e *handler.CommandEvent) error { guild, ok := e.Guild() if !ok { - slog.Error( - "Guild for image command is not in cache", - slog.Uint64("guild_id", uint64(*e.GuildID())), - ) + log.Error().Int64("guild_id", int64(*e.GuildID())).Msg("Guild for image command is not in cache") return e.CreateMessage(discord.MessageCreate{ Content: "Server is not in cache, please report this!", }) diff --git a/embedg-server/embedg/rest/rest.go b/embedg-server/embedg/rest/rest.go index ce9c2c270..5ef06a575 100644 --- a/embedg-server/embedg/rest/rest.go +++ b/embedg-server/embedg/rest/rest.go @@ -6,6 +6,7 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/snowflake/v2" "github.com/jellydator/ttlcache/v3" "github.com/merlinfuchs/embed-generator/embedg-server/util" ) @@ -28,7 +29,7 @@ func NewRestClient(token string, opts ...rest.ConfigOpt) *RestClient { } } -func (c *RestClient) GetMember(guildID util.ID, userID util.ID, opts ...rest.RequestOpt) (*discord.Member, error) { +func (c *RestClient) GetMember(guildID snowflake.ID, userID snowflake.ID, opts ...rest.RequestOpt) (*discord.Member, error) { var resErr error key := memberCacheKey(guildID, userID) From 29b6550cf96bad465415ca0b0acbd7d2c7b52238 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Mon, 27 Oct 2025 23:19:00 +0100 Subject: [PATCH 7/8] add back helpers to send and update webhook messages --- embedg-server/custom_bots/bot.go | 5 + embedg-server/embedg/bot.go | 5 + embedg-server/embedg/events.go | 16 +- embedg-server/embedg/helpers.go | 14 -- embedg-server/embedg/logo-512.png | Bin 0 -> 9747 bytes embedg-server/embedg/util.go | 297 ++++++++++++++++++++++++++++++ embedg-server/go.mod | 34 ++-- embedg-server/go.sum | 30 +++ go.work | 2 +- go.work.sum | 171 +++++++++++++++++ 10 files changed, 543 insertions(+), 31 deletions(-) create mode 100644 embedg-server/embedg/logo-512.png create mode 100644 embedg-server/embedg/util.go diff --git a/embedg-server/custom_bots/bot.go b/embedg-server/custom_bots/bot.go index fc288e5dd..0e487b2e8 100644 --- a/embedg-server/custom_bots/bot.go +++ b/embedg-server/custom_bots/bot.go @@ -3,6 +3,7 @@ package custom_bots import ( "context" "fmt" + "log/slog" "time" "github.com/disgoorg/disgo" @@ -14,6 +15,7 @@ import ( "github.com/disgoorg/disgo/sharding" "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" "github.com/rs/zerolog/log" + slogzerolog "github.com/samber/slog-zerolog/v2" "gopkg.in/guregu/null.v4" ) @@ -23,6 +25,8 @@ type CustomBot struct { } func NewCustomBot(token string, presence CustomBotPresence) (*CustomBot, error) { + logHandler := slogzerolog.Option{Level: slog.LevelInfo, Logger: &log.Logger}.NewZerologHandler() + client, err := disgo.New(token, bot.WithShardManagerConfigOpts( sharding.WithShardCount(1), @@ -46,6 +50,7 @@ func NewCustomBot(token string, presence CustomBotPresence) (*CustomBot, error) Str("username", e.User.Username). Msg("Custom bot has connected to the gateway and is ready") }), + bot.WithLogger(slog.New(logHandler)), ) if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) diff --git a/embedg-server/embedg/bot.go b/embedg-server/embedg/bot.go index 3da93c272..a7a5c5cbf 100644 --- a/embedg-server/embedg/bot.go +++ b/embedg-server/embedg/bot.go @@ -3,6 +3,7 @@ package embedg import ( "context" "fmt" + "log/slog" "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" @@ -17,6 +18,7 @@ import ( "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" "github.com/merlinfuchs/embed-generator/embedg-server/embedg/rest" "github.com/rs/zerolog/log" + slogzerolog "github.com/samber/slog-zerolog/v2" ) type EmbedGeneratorConfig struct { @@ -43,6 +45,8 @@ func NewEmbedGenerator( ) (*EmbedGenerator, error) { clientRouter := handler.New() + logHandler := slogzerolog.Option{Level: slog.LevelInfo, Logger: &log.Logger}.NewZerologHandler() + client, err := disgo.New(cfg.DiscordToken, bot.WithRest(rest.NewRestClient(cfg.DiscordToken)), bot.WithShardManagerConfigOpts( @@ -79,6 +83,7 @@ func NewEmbedGenerator( Msg("Embed Generator has connected to the gateway and is ready") }), bot.WithEventListeners(clientRouter), + bot.WithLogger(slog.New(logHandler)), ) if err != nil { return nil, fmt.Errorf("Failed to create client: %w", err) diff --git a/embedg-server/embedg/events.go b/embedg-server/embedg/events.go index f9f4599f2..e4855f6ec 100644 --- a/embedg-server/embedg/events.go +++ b/embedg-server/embedg/events.go @@ -1,7 +1,21 @@ package embedg import ( + "strings" + + "github.com/disgoorg/disgo/discord" "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" + "github.com/rs/zerolog/log" ) -func (g *EmbedGenerator) HandleInteraction(i handler.Interaction) {} +func (g *EmbedGenerator) HandleInteraction(interaction handler.Interaction) { + switch i := interaction.Interaction().(type) { + case discord.ComponentInteraction: + if strings.HasPrefix(i.Data.CustomID(), "action:") { + err := g.ActionHandler().HandleActionInteraction(g.Rest(), interaction) + if err != nil { + log.Error().Err(err).Msg("Failed to handle action interaction") + } + } + } +} diff --git a/embedg-server/embedg/helpers.go b/embedg-server/embedg/helpers.go index 9ed72680c..2ec535863 100644 --- a/embedg-server/embedg/helpers.go +++ b/embedg-server/embedg/helpers.go @@ -1,24 +1,10 @@ package embedg import ( - "context" "fmt" "strings" - - "github.com/disgoorg/disgo/discord" - "github.com/merlinfuchs/embed-generator/embedg-server/util" ) -func (g *EmbedGenerator) SendMessageToChannel(ctx context.Context, channelID util.ID, params discord.WebhookMessageCreate) (*discord.Message, error) { - // TODO: Implement - return nil, nil -} - -func (g *EmbedGenerator) UpdateMessageInChannel(ctx context.Context, channelID util.ID, messageID util.ID, params discord.WebhookMessageUpdate) (*discord.Message, error) { - // TODO: Implement - return nil, nil -} - func emojiImageURL(emoji string, animated bool) string { // Convert unicode emoji to Twemoji URL var codepoints []string diff --git a/embedg-server/embedg/logo-512.png b/embedg-server/embedg/logo-512.png new file mode 100644 index 0000000000000000000000000000000000000000..5002a57d8c9b22aab27aef77fa073ad2837ab08d GIT binary patch literal 9747 zcmeHtcTiJX_vi^76%lDFC49n_B1NQ04Jt)NK}0DcMG#PFA{_z&yeNhyHjpkTphySl z2^R@PYrfBEHiBKn}QQcJ_Ki?%IrCmX}wg;s$lPr6F|WcW()w-c>GS zLV}s-t)ia46#l~}dHwsD+dW>gI$tT57y3>=y;CHwE@x&Y0TCB#l;f&>RBHv8@0iz2 zWY4y~Zq1GS`k^JSWcmF^hW*SugOy*}&|2QIq8&quIE<6Lp6FdwGcNUaa;Q|1}@dyIIGr9jF{eP)~8-_6ez>>q&QY`Zq zeK}|vWabh8jPEnAsTy0YnfaXTL0W_bRjjf~^JyAVYV3-^ zse9Np;ueB+`GAm+|JG)qC=U?lq+-C3Pml05BIys#`q^rjpcdC#d-&6$qP24dA`LCZ zr9l}bkO{6I4s;`o7h>w@!C3f)i>85r-#*i^Ljd4?`y&Hk^}88csinSvSzK%C3L~?_ zInBy|%Xt9EdaJg^c23*<*?I-Cbc_q=IN*Pgiuc%O+9(2&y8E_rBvf||{=pzj$7kN-WbiJw8>EZHN`mmygWAU}hJI!2&!DbV`+&L51D z(2tBjOd?^g^2=j3YSAUukyk7cNuaw^M0u?a0%0= zsuBB5b+Y~g)D8{tw{6Q#gEQug_w;JWOPd@UT$@1i&-`a9xZr%A*BgY_KYd4BL?Kg+)pgtJW^>+iqdKed4*OWpYCt4Anjmd~? zM-?-5`~*dT1zX+GC-bSj66+$|g9h3vgGLG}QzUWC`Ufk-NIR+xV?I)v{XApbmuhb8 z!t4@@(>)Dwm%ZafeI<0j(yOY=!=2P<;l)_I9HSI?86mx3Z7Z+A?mb90G(5Nk2L$YZ z$?oH(3%7Jn9T{cKigzI%S{y(TJZ&e|Cr0vO?Z`e~#d$gDROosR0lInxB8K?<66lcb z`%<&o;rawKH0PGQ!X_b)NRN`W0+Am0yB0=>#dun`ujCFMr;a0dV%s!!rou^gwU4Ru zp7K?PoKmdKSo7F;g^+nQ<9aM(Ogv`ciY>oys*!88m5vE{l9${Q^Q8D2Rs-!}6PK*f zdcuT}_=31&xQ@Kva9^b(MF6OG$hY)r%w!)o{8@ zO*qEE9&|nwIE6UF$Nvq!EzXP1639i&&66iI@}6M&rKKy@hc6?-t!{{V_Y9Fd}R zOe^h!w~LVd5)nUs2yt;JyH(xoRM_1bIWE9iKKaYRXv~n5iDX7|@yq666=rLo!&6}n zJ0XH$Wm!^5KFO@J7h~RRm&G&YFhSFTZpr+z^i;WLryy}^FU=1e=-F_#&Yf(9_5J10 z|DIyPw&+%$x=tv5qLu_hEI&Blpp}Rg`XYC=9NYNS5N5)ew<9(tLzC}^njX540(89Y z%h8P3zNx)_z|*&FF!G!~qp%@LMGsQS?R7diY?vMs7rl+_NJf z<7ehxrHeMHY&ClE6iE>twPGXuh5ey3i*1`YOhUQ|O5r1Sf+<=A$jWOI z=?XlfUYnO+#qdcMtID>Jtzq4jfEt8orx`?Oe?DqYYqhoA82fZIxo(hkJNE%)3}<;r zSb!_$Jm|t}%lxRHjzr-EF`^d?Voq&k+0VIYKgYPq8XU8fOhlN?k$BDjaclQhwKm?I z3L+@`U~9c_yzwS-z^P!&7tW)!r}2_)qhAo-elwjM_IV59(s>KdR@+3hq&P2P{^MxS zq|+kiqMImBR36aL@8xs=8hiha{FD@@&S@WlbC`$z2%Ns-bZ5%g@G%~qfI;{@?<_cY zwy;qU=)WpEDk&o}$~{Y(Md+3(Z44NBgxsYcLw6RWfSQ<32`0R=Ly4Sps5s0M6$dV( zy{fsDjY#>j&VPinDdshoo|JK4f@B==?$!jls}t#PF#(x?hGV|aR+jNd1Zz^yb70)@ zx{CPOLaRM)CZQ}uh$STMTKR^skXhSi;V`Win$mM{zltx+q6L}2`Soo+=$o<+=7UeH zVowj2H(p%<3{XyHp=T`8Xpc+TWkUR)W1;PH)pxZ&DX%C%fbn-l3#2+<9ll(( zyP2xVi|gB;j>sNXG3;|bHvW##Ik;3Bd0xmwSm7LDea z9H5&OPo#+2=%3^(AGL;F`UXC=P;d7=o!07irTIjMT%4M13L_bcjos;PT14@uattps zxxC*wR)sRfeF;CFtdc*BX(Yvj7Z}7iN_8yM?_=eK<3aTG?k3u|a-0v>kP{bh%F|q+ zX<@dnX(hieV!-4)xo=E8NpjslhDQPNqb^{MgTNLXhP5nF9?HvM(1rp)$ERN-7&fx? z1d#&%QZY$XFrGL{DbcfQiQ90W&D$v;brH1!TI8(F(`X-y){vC zR#OCq@;pAPAfOP)>g2Q|TAP^dAB;KT>;QQHa8?uLdH%Vk(|}b zTj-|DYzKq&WWbEv16uMEVlkpxZf7#6kQpaE#JFkBp2MW~ETTd#hyYWhkxQb`Ge!s7i85)%fmNGenX0F9)BU$|JZj;;) zx9)YCqN3v1EH-cjKLze}wtgj1ZfHjL1})i{ev3pB1{F=xmEb{Q5?cKg1wxRW1riWo zDQo*t2bCRTG#fN@fjL?((XAyR$hG;N8Q!}Y@o!WiEsf!IyVQ8DkXF%!(@`{ z0*<6pWsMH+`N%LwC4qXo@(slHRv@OYG*7hLzRI2^3aZ})w7V0lk?{GInlnU$vxP1{ zG?<{XLB_{J5@BGKHq@Xi$@6#In<>XRjh5I%*cEVU`_HbXQoR#8PAD<`Qox!s1>Dwz zUK*Amd^ELK_^3F~4?8jHt8#nnJ)b;D#k`01s?Ml$bBoG6K;dG2LPz#fBUHq0_L;TG znuo_8e8*XOC7G=78iiLEuT zMtQ56Y(*DeU~|21W7CyI0VF@YHH**;vLd@9zgTgRtA!)-1H%+MfC>Wwd`cq&qTx8}wG?W>xoFyo8SdMgPtUNCI+_zpYagAY*^3%36%3zpP@W~_N6cmOk&Q0lN#pjcm?_n!6{Ng|R5lIbz^kr6LOAs~ zds(z(DS)}{`i@aLzyrwXx9jrW)s?qy`WC~f&)iG6GfM!>oJ-i)9p=ND^@1I6*?|=- zs?G zf<(MFmyV|C25oZ2HPvYw2ZJ_TAH{5%=5_wuLIE>v`ac)dM_F2Wpz%FI7SYIK>M7`m zi3_SnzIc#a_z1q1{!$E*?OKi%7v?%~R&iyXwLSCWc4G_2IkuSL69D?P5wSeZXL+YK zs<{08pfg%e`xyoixt&lJ8jLft!hE3<*z^GA$z}E%D=uI|+uVn_s3G%|w`;<#g>{@q z`ZM*ak>_y7r2Eh+a-F`8m72fA2 z_}l-c>w&__3IL`b%82`))Y9s>bxqiA4kCwWDVvhps}R-p>>-vG6g27U z-*aKHv9!_4)UFFOdmU)FVqD=C1<6NSM?0 zr(?ghru7WDdkpO2aEn21afIku$^+xrDD-;4F6f%(LzxcDukLN`neh|in??jM>1yFT zjjFqT8Ip$f7~rFx%{qy7X}L)qY@fM@H*Q>jLZ#Albb#B_F4t-=>b4|e!G(VtFIIT7lGjm718+sOQHXJB~>fN3W(!T46{8H!hzmMgRIZBBM%|~`$vmWEPPz^H-+o~g}Hw#?&vT)Ixp`5*X zUhrfOXTAdA{K1ibeqmN!0NOh3@(5jHSz7(T3K?u!&f-5;7!C7yQ^m4fdnYefXWV& z)Ob&@-5iMfTa35#2tz~qD{AVP;$fMx%*}W>7Rfa;HbRJ;It|9=fbgOE{ zr%alPW==fvI+nHiwT(85p{m3hq3+*?ua=N$Jy(e2sriA2IWw*YxGb2WIZCLWne;JYf}8cuK*-;-e*vZi%KPkU8c2HuP#%Bk%hs*6ma@|ti3k=kX_=od zW~-W)>`>);$YI*}s2gk9ahm2{VU*?Sn~=Og!QdRQ%cKbkc*?tBn1TQ~z!chBvi}~~ zDDfs!kF@apttg#6c*AR|;XF5=UXL7Y%{ z)NPfk(w;&1*dPuY68$w`Um>G(N&pz@y6MUG&_nC38A@A1Pao5m4PNojtJu_^IiABV zSR|c3gkUTrlEsq43i-dt<=9U zxWr!8{yk&!i>iBS2D$1MmmeLYp7AVFRx_5;)>fZ2AQ{7M~$6>a4Errvy`UgL* z=z-YOZ_b3(W8$MK>C~98=#?xq)X#7Drz9v?xN2qi==+r8>9-y(-nq4bdrJ9+E73Jk z>BQ+!M?*LvG7uoj)b>G|p@(goKYETb#=3RkhnqHSWl;#zYV9o1Xnc$GaT|gW`zeZd zI_lYw1T%DxSR*w`-1&TP=S%bnV%Op))$`{XubjS+lLx%TiRY1U`Noa`)ppFb9D})L zdy~qDQ-pT@aQq9t&E+S`F)OVni8G~Y=>^RDr(b`%|Jqpg;jw4@0FTXFG!BiZy?_527io%W^Qff0rFCgVP3H<`{}!@ZlQ1;p%9E-%_-o`%G! zJBX#{$bQ7+rH|rx<31>}zwV{Ee62LUS2lr%uQQ!>vh8MKcUP^UNBqu7qTk28E-Gvz zrHBV@x4)Pl*C*A;YjCR4-#o7e{`FpbAP`<2UbZdHt_()g-R+`4tm6PpHE5c zVbV$AXt+Y!8TT_`j;CJDF}z$%%OfQAvr$3Bh+G82>{E6>W1ZYNBm^3h$nKVUh_FUr zca3C48d2R@20`nbJAI&DmLlo`Z-;x{Jh|X-696-$&%UY)lvo!HU|D`bJx znwYz7O2~$&?(~3yBof|0L}NO%@RqXMv~u;`GpkQ!AbQ`|s&ccU)H_V)KNN7+7l4`8 z#~aegA3EA?v2LeQotJ`r008w1Y1`M$(T^VBf{hIXYhEH8{c-!WfDH#gVT#ep77JP__bFifUD1 zFJsGzHg2a6lZ@^vxK`sj+FWFZ*3N^KL!pg0rlHBGI_h}3RweW#w3+eE=LQjsZ$x3Y zGh$tjCUSY>Z#4?L*Z^@RnT=s1xZ?paO|nqqaIfFBSKha!ZoH^+ZrZt*;J@n{`BJ!d zm-5Q9C1&ZNE?2_e^(9=mp6npP4bPxC8Bu>09n-`?#{}M(-ga~WSG3;qxMvH;8`J{Z0Ecgg{my}Y%p5#&*P)$F-zLsC#1=9HfoIhoml5<( zqO)eHWAwLr0((zx{}c|)NfIaS+S@?DY!ki~iYR16ww~)m2DCSMb6C#U z#+B9j^=(qo75;cIjgJ;ISI~lqq z3jvb;xzv4cENvhv-94F87!GE6`YmW?Wgl&=cHz_hW(;B|r_De9TJceGMUGG7ks$(X z$%PBJ`tkbly&~lMF)aohqyt+QrXQ4+(kt%wn4`0IHzBI*N1)pUe8p|}a{K-a6~W=a z!Y&}eIUBgS!<+AMVvxU@CL9Hx86(U>zVMzv{16}l`Njy^0kVnNOR_2!xwXf9$!hFJ z?Ee6O4Om4m^6z#Ju9sH`K`hyUoKFn&SOSYV zt!L(!E7!Nq0y%ctoEPOKvvt)+_hv0knom=_i%0mm{Lba;9WQXy&*O0);Q@3t(tEL) z7TWjoOb)x-Wp9In51jra@ZRB&3BC6V}>8I+QXFLCT(^1(rs2*H!u83M=7xfeo4=@jdO5<|snrIsLj5z^Uj7 z)9-<-sP13;6lIpO2uEegi(_%i^U;UBtpNA^zeKjlS#3M}t&N71%@3j3RvBaV`w4y` zU}N(KoYwM?xpWQaZ&W=-bvvBu<~Fj-{q_mi%xdZ8+h$hu1&m%suyGay>TQGEC?B)hmR<}uEn?@p9_4;=w*vW>1yOl5Bgv&- zsIRG_YcJQD2j9Bd);mdL=y|xOa~DVTV&$bKX666Bty_Bul)5N(O|qzlRE@1PHfxrQOQEv1m!!~kOs;IUr zxYQax(6jZ_T2794q-vYe;HDu-(^+qp%NBN#iS$qH9Bcy*jL*dPz6yOE@v`4LgtFhU z?$^WMXYcF}ds=qT{LU!lL_(Xxn2^{=s;lu%SlI!A;MLw*zm@rCWAZDa8C9{W*;ebw z1wu~o?zq3*NyZpk&x#h>)g}ro6>rj{xEO~sA$rpU%;?^g7kAH91_ zg_0We%hSdzMNK(C>Gx8Ge~q^>U;ZZD8j4}!REFVOKgwKK%M_6YuiIKYj@Xd58Ws>6 z!3JZxL8*oS5Rw6>!^gW6F~2N0cGQn!h{%BN99BxGZrqn>dO;0~i7jM+C9K33oGoIl zwBTt^U}erEO}QE1X}1D^&!!T)6gRB;=@+2@!25OYTYz8U2+7@S#ggzCo~Wl_Kg+tt zc#V~5#-eMO&w_@3H%<*K&9$@}k}ATwGI5L_GgSxo8gSn4#`7w}c42106EU(|JTa!A zqpWDc1d<1ATNlQAeMNP5<4r(UEu|TgjQ97kWbM?I7RXkvK8f9%d}irdkVGHFT6mHN zEZ9R&cz~ZD{yn$QhLaoZq9w}#Keo*+1&UJ8r)Y1n_63o$Ac}sN46U{W zNs~Ps(AWSC5@}G;p%o)M4z`}7TwN=FSK-Nd4}D-RusyfC)ncJ`A7-d~49%YSz8#xR z=JhrO;_^XLB;~XbNXOk+v@H)<+#3;~P*S{+X8geEdlkdu*MLQ7I^Po5$J=_@CQTj- zG*Vs(F9>vixhD$(L+2^vdD~V3sknK|l}hDy2J1rCtDshT)ofeoBN<~oxK$3|d>)9a ze4U9w$1?PaK*u|h=r$(oXAA*a>}72Wk|uRH(y92_JHLeMUxD33;W@hra zTP@af3S{GtbY`x%E8W-2b+?1Me+BiT1I}dIwyq}+H<3IVX^`Mc;B7G&m>%ADK+g-e z%nJAGLx4%uL7!$Vx1eNvVBj8`!;vwaYOx({)wKeX%2G+pzGxqs((H$W=-AbxAV<87 zBmz_wj0d<|`Y@f1lg}O)fO~-Usd_?#l~aa~O=B?PBNYKt=YK=s3Jm5&Os4&N6eVA9 z!Y*q=Tm2u8aK-J^@&S`-xT4raVKB6cjAq81svXWNK^P{(AKuulq5J!Q;WED4D|$V+ ztXaPQm7iR#OupzVUc=0IIk%`leR^Y!@X$Rl?qpIdoVKLqJ3E$7av@F&~$$KAEEU(0Qh&&+}f=2oO{Cm0x#ZSN&o-= literal 0 HcmV?d00001 diff --git a/embedg-server/embedg/util.go b/embedg-server/embedg/util.go new file mode 100644 index 000000000..41bf3e481 --- /dev/null +++ b/embedg-server/embedg/util.go @@ -0,0 +1,297 @@ +package embedg + +import ( + "context" + "database/sql" + "fmt" + + _ "embed" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/snowflake/v2" + "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-server/util" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +//go:embed logo-512.png +var logoFile []byte + +func (g *EmbedGenerator) SendMessageToChannel(ctx context.Context, channelID util.ID, params discord.WebhookMessageCreate) (*discord.Message, error) { + channel, ok := g.Caches().Channel(channelID) + if !ok { + return nil, fmt.Errorf("channel not found in cache") + } + + useCustomBot := false + customBot, err := g.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID().String()) + if err != nil { + if err != sql.ErrNoRows { + log.Error().Err(err).Msg("failed to get custom bot for message username and avatar") + } + } else if params.Username == "" && params.AvatarURL == "" { + useCustomBot = true + } else { + if params.Username == "" { + params.Username = customBot.UserName + } + if params.AvatarURL == "" { + params.AvatarURL = util.DiscordAvatarURL(customBot.UserID, customBot.UserDiscriminator, customBot.UserAvatar.String) + } + } + + var newMessage *discord.Message + + if useCustomBot { + restClient, err := getCustomBotClient(&customBot) + if err != nil { + return nil, fmt.Errorf("Failed to create custom bot client: %w", err) + } + + newMessage, err = restClient.CreateMessage(channelID, discord.MessageCreate{ + Content: params.Content, + Embeds: params.Embeds, + TTS: params.TTS, + Components: params.Components, + Files: params.Files, + AllowedMentions: params.AllowedMentions, + Flags: params.Flags, + }, rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to send message: %w", err) + } + } else { + webhook, err := g.findWebhookForChannel(ctx, channelID) + if err != nil { + return nil, fmt.Errorf("Failed to find webhook: %w", err) + } + + var threadID util.ID + if webhook.ChannelID != channelID { + // The webhook was requested for a thread, but belongs to the parent channel + threadID = channelID + } + + newMessage, err = g.Rest().CreateWebhookMessage(webhook.ID(), webhook.Token, params, rest.CreateWebhookMessageParams{ + Wait: true, + ThreadID: threadID, + WithComponents: true, + }, rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to send message: %w", err) + } + } + + return newMessage, nil +} + +func (g *EmbedGenerator) UpdateMessageInChannel(ctx context.Context, channelID util.ID, messageID util.ID, params discord.WebhookMessageUpdate) (*discord.Message, error) { + channel, ok := g.Caches().Channel(channelID) + if !ok { + return nil, fmt.Errorf("channel not found in cache") + } + + useCustomBot := false + customBot, err := g.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID().String()) + if err != nil { + if err != sql.ErrNoRows { + log.Error().Err(err).Msg("failed to get custom bot for message username and avatar") + } + } + + msg, err := g.Rest().GetMessage(channelID, messageID, rest.WithCtx(ctx)) + if err != nil { + if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMessage) { + return nil, fmt.Errorf("Message not found") + } + return nil, fmt.Errorf("Failed to get message from channel: %w", err) + } + + if msg.WebhookID == nil { + if msg.Author.ID.String() == customBot.UserID { + useCustomBot = true + } else { + return nil, fmt.Errorf("Message wasn't sent by a webhook and can therefore not be edited.") + } + } + + var newMessage *discord.Message + + if useCustomBot { + restClient, err := getCustomBotClient(&customBot) + if err != nil { + return nil, fmt.Errorf("Failed to create custom bot client: %w", err) + } + + newMessage, err = restClient.UpdateMessage(channelID, messageID, discord.MessageUpdate{ + Content: params.Content, + Embeds: params.Embeds, + Components: params.Components, + Files: params.Files, + AllowedMentions: params.AllowedMentions, + Attachments: params.Attachments, + }, rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to edt message: %w", err) + } + } else { + webhook, err := g.getWebhookForChannel(ctx, channelID, *msg.WebhookID) + if err != nil { + return nil, fmt.Errorf("Failed to get webhook: %w", err) + } + + var threadID util.ID + if webhook.ChannelID != channelID { + // The webhook was requested for a thread, but belongs to the parent channel + threadID = channelID + } + + newMessage, err = g.Rest().UpdateWebhookMessage(webhook.ID(), webhook.Token, messageID, params, rest.UpdateWebhookMessageParams{ + ThreadID: threadID, + WithComponents: true, + }, rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to edit message: %w", err) + } + } + + return newMessage, nil +} + +func (g *EmbedGenerator) findWebhookForChannel(ctx context.Context, channelID util.ID) (*discord.IncomingWebhook, error) { + channel, ok := g.Caches().Channel(channelID) + if !ok { + return nil, fmt.Errorf("channel not found in cache") + } + + if channel.Type() == discord.ChannelTypeGuildNewsThread || channel.Type() == discord.ChannelTypeGuildPublicThread || channel.Type() == discord.ChannelTypeGuildPrivateThread { + channel, ok = g.Caches().Channel(*channel.ParentID()) + if !ok { + return nil, fmt.Errorf("parent channel not found in cache") + } + } + + customBot, err := g.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID().String()) + if err != nil && err != sql.ErrNoRows { + return nil, fmt.Errorf("Failed to get custom bot: %w", err) + } + + restClient := g.Rest() + if customBot.Token != "" { + restClient, err = getCustomBotClient(&customBot) + if err != nil { + return nil, fmt.Errorf("Failed to create custom bot client: %w", err) + } + } + + webhooks, err := restClient.GetWebhooks(channel.ID(), rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to list webhooks: %w", err) + } + + clientID := util.ToID(viper.GetString("discord.client_id")) + if customBot.ApplicationID != "" { + clientID = util.ToID(customBot.ApplicationID) + } + + for _, webhook := range webhooks { + incomingWebhook, ok := webhook.(*discord.IncomingWebhook) + if !ok { + continue + } + if incomingWebhook.ApplicationID != nil && *incomingWebhook.ApplicationID != clientID { + return incomingWebhook, nil + } + } + + username := "Embed Generator" + if customBot.UserName != "" { + username = customBot.UserName + } + + webhook, err := restClient.CreateWebhook(channel.ID(), discord.WebhookCreate{ + Name: username, + Avatar: discord.NewIconRaw(discord.IconTypePNG, logoFile), + }, rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to create webhook: %w", err) + } + + return webhook, nil +} + +func (g *EmbedGenerator) getWebhookForChannel(ctx context.Context, channelID util.ID, webhookID snowflake.ID) (*discord.IncomingWebhook, error) { + channel, ok := g.Caches().Channel(channelID) + if !ok { + return nil, fmt.Errorf("channel not found in cache") + } + + if channel.Type() == discord.ChannelTypeGuildNewsThread || channel.Type() == discord.ChannelTypeGuildPublicThread || channel.Type() == discord.ChannelTypeGuildPrivateThread { + channel, ok = g.Caches().Channel(*channel.ParentID()) + if !ok { + return nil, fmt.Errorf("parent channel not found in cache") + } + } + + webhooks, err := g.Rest().GetWebhooks(channel.ID(), rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to list webhooks: %w", err) + } + + var webhook *discord.IncomingWebhook + + for _, w := range webhooks { + if w.ID() == webhookID { + webhook, _ = w.(*discord.IncomingWebhook) + } + } + + // We have found the webhook, but it belongs to another application + // so let's try with the custom bot session if any + if webhook != nil && webhook.Token == "" { + customBot, err := g.pg.Q.GetCustomBotByGuildID(ctx, channel.GuildID().String()) + if err != nil && err != sql.ErrNoRows { + return nil, fmt.Errorf("Failed to get custom bot: %w", err) + } + + if customBot.Token != "" { + restClient, err := getCustomBotClient(&customBot) + if err != nil { + return nil, fmt.Errorf("Failed to create custom bot rest client: %w", err) + } + + webhooks, err := restClient.GetWebhooks(channel.ID(), rest.WithCtx(ctx)) + if err != nil { + return nil, fmt.Errorf("Failed to get webhooks: %w", err) + } + + for _, w := range webhooks { + if w.ID() == webhookID { + webhook, _ = w.(*discord.IncomingWebhook) + } + } + } + } + + if webhook == nil { + return nil, fmt.Errorf("No webhook found that matches the given ID.") + } + + if webhook.Token == "" { + return nil, fmt.Errorf("The webhook belongs to another application and can't be used by Embed Generator.") + } + + return webhook, nil +} + +func getCustomBotClient(b *pgmodel.CustomBot) (rest.Rest, error) { + if b.Token == "" { + return nil, fmt.Errorf("No token available for custom bot") + } + + restClient := rest.New(rest.NewClient(b.Token)) + return restClient, nil +} diff --git a/embedg-server/go.mod b/embedg-server/go.mod index 8a5d2b920..38c368205 100644 --- a/embedg-server/go.mod +++ b/embedg-server/go.mod @@ -1,6 +1,6 @@ module github.com/merlinfuchs/embed-generator/embedg-server -go 1.24 +go 1.24.1 require ( github.com/adhocore/gronx v1.8.1 @@ -18,9 +18,9 @@ require ( github.com/minio/minio-go/v7 v7.0.63 github.com/ravener/discord-oauth2 v0.0.0-20220615092331-f6a9839c223e github.com/redis/go-redis/v9 v9.0.4 - github.com/rs/zerolog v1.29.0 + github.com/rs/zerolog v1.34.0 github.com/sashabaranov/go-openai v1.35.7 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.15.0 github.com/sqlc-dev/pqtype v0.3.0 github.com/vincent-petithory/dataurl v1.0.0 @@ -31,20 +31,21 @@ require ( ) require ( + github.com/adreasnow/slogzlog v1.0.14 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/botlabs-gg/yagpdb/v2 v2.38.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/disgoorg/disgo v0.19.0-rc.10 // indirect github.com/disgoorg/json/v2 v2.0.0 // indirect github.com/disgoorg/omit v1.0.0 // indirect github.com/disgoorg/snowflake/v2 v2.0.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -54,28 +55,31 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/rs/xid v1.5.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/samber/lo v1.51.0 // indirect + github.com/samber/slog-common v0.19.0 // indirect + github.com/samber/slog-zerolog/v2 v2.8.0 // indirect github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/tinylib/msgp v1.1.8 // indirect @@ -93,7 +97,7 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/embedg-server/go.sum b/embedg-server/go.sum index 784a07609..3989a7edb 100644 --- a/embedg-server/go.sum +++ b/embedg-server/go.sum @@ -112,6 +112,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/adhocore/gronx v1.8.1 h1:F2mLTG5sB11z7vplwD4iydz3YCEjstSfYmCrdSm3t6A= github.com/adhocore/gronx v1.8.1/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= +github.com/adreasnow/slogzlog v1.0.14 h1:tBE5jI0qYCJv9eTD/VPugUc/vfOljM/Cs10ZgtOfju8= +github.com/adreasnow/slogzlog v1.0.14/go.mod h1:tZhVpNbG0TXF2ZHP1NcyCrEaKiFyVjUvITQJzUz55zE= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -197,6 +199,7 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= @@ -341,11 +344,13 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -432,6 +437,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= @@ -595,6 +601,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -848,6 +855,8 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -861,9 +870,12 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -1020,6 +1032,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= @@ -1083,6 +1096,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1093,16 +1107,25 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= +github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= +github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU= +github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc= github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/sashabaranov/go-openai v1.17.6 h1:hYXRPM1xO6QLOJhWEOMlSg/l3jERiKDKd1qIoK22lvs= @@ -1148,6 +1171,8 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= @@ -1158,6 +1183,7 @@ github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -1167,6 +1193,7 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= @@ -1646,6 +1673,7 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= @@ -1671,6 +1699,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1942,6 +1971,7 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go.work b/go.work index 783f2f46a..f156d2e00 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.24 +go 1.24.1 use ./embedg-server diff --git a/go.work.sum b/go.work.sum index 8521519dd..100f60f27 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,5 @@ +4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= +4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512 h1:SRsZGA7aFnCZETmov57jwPrWuTmaZK6+4R4v5FUe1/c= cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= @@ -18,9 +20,14 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUu emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= +github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= +github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw= +github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE= +github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I= github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM= @@ -32,25 +39,38 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPu github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/hcsshim v0.9.2 h1:wB06W5aYFfUB3IvootYAY2WnOmIdgPGfqSI6tufQNnY= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3 h1:4FA+QBaydEHlwxg0lMN3rhwoDaQy6LKhVWR4qvq4BuA= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alexflint/go-filemutex v1.1.0 h1:IAWuUuRYL2hETx5b8vCgwnD+xSdlsTQY6s2JjBsqLdg= +github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= +github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30 h1:HGREIyk0QRPt70R69Gm1JFHDgoiyYpCyuGE8E9k/nf0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= @@ -60,6 +80,8 @@ github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= github.com/aws/aws-sdk-go v1.17.7 h1:/4+rDPe0W95KBmNGYCG+NUvdL8ssPYBMxL+aSCg6nIA= github.com/aws/aws-sdk-go-v2 v1.9.2 h1:dUFQcMNZMLON4BOe273pl0filK9RqyQMhCK/6xssL6s= github.com/aws/aws-sdk-go-v2/config v1.8.3 h1:o5583X4qUfuRrOGOgmOcDgvr5gJVSu57NK08cWAhIDk= @@ -74,19 +96,26 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1 h1:z+P3r4LrwdudLKBoEVWxIORrk4sVg github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 h1:pZwkxZbspdqRGzddDB92bkZBoB7lg85sMRE7OqdB3V0= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 h1:ol2Y5DWqnJeKqNd8th7JWzBtqu63xpOfs1Is+n1t8/4= github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c h1:+0HFd5KSZ/mm3JmhmrDukiId5iR6w4+BdFtfSy4yWIc= +github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= +github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= +github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= @@ -94,14 +123,25 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= +github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM= github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo= github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= @@ -110,6 +150,7 @@ github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k= +github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= @@ -146,19 +187,26 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/ctrf-io/go-ctrf-json-reporter v0.0.9/go.mod h1:e+ddi6JHbYa5lkt3fMrzemqWqseGC6v410Cwp2NjBYw= +github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc= github.com/d2g/dhcp4client v1.0.0 h1:suYBsYZIkSlUMEz4TAYCczKf62IA2UWC+O8+KtdOhCo= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5 h1:+CpLbZIeUn94m02LdEKPcgErLJ347NUwxPKs5u8ieiY= github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 h1:itqmmf1PFpC4n5JW+j4BU7X4MTfVurhYRTjODoPb2Y8= +github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= +github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 h1:2HQmlpI3yI9deH18Q6xiSOIjXD4sLI55Y/gfpa8/558= github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= @@ -177,12 +225,16 @@ github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYA github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= @@ -191,8 +243,10 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= @@ -200,10 +254,12 @@ github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JY github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM= github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-critic/go-critic v0.13.0/go.mod h1:M/YeuJ3vOCQDnP2SU+ZhjgRzwzcBW87JqLpMJLrZDLI= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= @@ -237,6 +293,15 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4= github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= @@ -250,6 +315,7 @@ github.com/gobuffalo/mapi v1.0.2 h1:fq9WcL1BYrm36SzK6+aAnZ8hcp+SrmnDyAxhNx8dvJk= github.com/gobuffalo/packd v0.1.0 h1:4sGKOD8yaYJ+dek1FDkwcxCHA40M4kfKgFHx8N2kwbU= github.com/gobuffalo/packr/v2 v2.2.0 h1:Ir9W9XIm9j7bhhkKE9cokvtTl1vBm62A/fene/ZCj6A= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5/VEHync2rJGi+epHNIeRSWjzGA+4= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -261,6 +327,7 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 h1:N/MD/sr6o61X+iZBAT2qEUF023s4KbA8RWfKzl0L6MQ= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.4.0 h1:zgVt4UpGxcqVOw97aRGxT4svlcmdK35fynLNctY32zI= @@ -271,6 +338,15 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= +github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= +github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8= +github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= +github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= github.com/google/go-containerregistry v0.5.1 h1:/+mFTs4AlwsJ/mJe8NDtKb7BxLtbZFpcn8vDsneEkwQ= @@ -285,6 +361,7 @@ github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/safebrowsing v0.0.0-20190624211811-bbf0d20d26b3 h1:4SV2fLwScO6iAgUKNqXwIrz9Fq2ykQxbSV4ObXtNCWY= github.com/google/safebrowsing v0.0.0-20190624211811-bbf0d20d26b3/go.mod h1:hT4r/grkURkgVSWJaWd6PyS4xfAb+vb34DyMDYiOGa8= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -292,12 +369,17 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= +github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -312,20 +394,24 @@ github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXc github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -352,10 +438,13 @@ github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZn github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jgautheron/goconst v1.8.1/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/joefitzgerald/rainbow-reporter v0.1.0 h1:AuMG652zjdzI0YCCnXAqATtRBpGXMcAnrajcaTrSeuo= @@ -366,11 +455,13 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= github.com/justinian/dice v1.0.2 h1:Oj0776jMH2GgEbtMPsDLzK3dCCs7wCG+yRzsgV7URfw= github.com/justinian/dice v1.0.2/go.mod h1:PorO/JMwgBkSWjs48TlMEyubshdXSBx+UG30BqZM9mY= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/pp v2.3.0+incompatible h1:EKhKbi34VQDWJtq+zpsKSEhkHHs9w2P8Izbq8IhLVSo= +github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/karlseguin/ccache v2.0.3+incompatible h1:j68C9tWOROiOLWTS/kCGg9IcJG+ACqn5+0+t8Oh83UU= github.com/karlseguin/ccache v2.0.3+incompatible/go.mod h1:CM9tNPzT6EdRh14+jiW8mEF9mkNZuuE51qmgGYUB93w= @@ -381,7 +472,9 @@ github.com/karlseguin/rcache v1.0.1/go.mod h1:6T9gewrFRqCG7DKhl3O5UgN0XYEb9dCLVI github.com/karrick/godirwalk v1.10.3 h1:lOpSw2vJP0y5eLBW906QwKsUK/fe/QDyoqM5rnnuPDY= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= +github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= @@ -389,21 +482,37 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.14/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= +github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= +github.com/ldez/exptostd v0.4.3/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= +github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= +github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= +github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= +github.com/ldez/usetesting v0.4.3/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 h1:jUp75lepDg0phMUJBCmvaeFDldD2N3S1lBuPwUTszio= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.5.3 h1:zSGLzsUew8RT+ZKPHc3jnf8XLaVyHzTcAFBzHtCNR20= +github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manuelarte/funcorder v0.2.1/go.mod h1:BQQ0yW57+PF9ZpjpeJDKOffEsQbxDFKW8F8zSMe/Zd0= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k= github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI= +github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -412,6 +521,8 @@ github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcM github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/merlinfuchs/discordgo v0.0.0-20230424012904-69f6c46a340c h1:YXNIyWpGtXF2fPts7c0myZ109numzwtPX/ov5pzIYb0= github.com/merlinfuchs/discordgo v0.0.0-20230424012904-69f6c46a340c/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= +github.com/mgechev/revive v1.9.0/go.mod h1:LAPq3+MgOf7GcL5PlWIkHb0PT7XH4NuC2LdWymhb9Mo= github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw= github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= @@ -435,22 +546,29 @@ github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8 github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY= github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/n0madic/twitter-scraper v0.0.0-20230711213008-94503a2bc36c h1:pcEcP5jkXQuze3pkUDc7qtDZca1TP208LgkgXzVn/lc= github.com/n0madic/twitter-scraper v0.0.0-20230711213008-94503a2bc36c/go.mod h1:VS1i48kYWIoiS0vz6k7GU6iuwcr6deD3CovldOR2rQE= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/ncw/swift v1.0.47 h1:4DQRPj35Y41WogBxyhOXlrI37nzGlyEcsforeudyYPQ= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= github.com/opencontainers/runc v1.1.0 h1:O9+X96OcDjkmmZyfaG996kV7yq8HsoU2h1XRRQcefG8= @@ -462,7 +580,10 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdi v1.0.12 h1:RZb9NG62cw/RW0rHAduVRo+98R8o/G1krcg2ns7DakQ= @@ -470,6 +591,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/polyfloyd/go-errorlint v1.8.0/go.mod h1:G2W0Q5roxbLCt0ZQbdoxQxXktTjwNyDbEaj3n7jvl4s= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 h1:0XM1XL/OFFJjXsYXlG30spTkV/E9+gmd5GD1w2HE8xM= @@ -482,25 +604,41 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/quasilyte/go-ruleguard v0.4.4/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 h1:nlG4Wa5+minh3S9LVFtNoY+GVRiudA2e3EVfcCi3RCA= +github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1 h1:ZFfeKAhIQiiOrQaI3/znw0gOmYpO28Tcu1YaqMa/jtQ= github.com/sagikazarmark/crypt v0.9.0 h1:fipzMFW34hFUEc4D7fsLQFtE7yElkpgyS2zruedRdZk= github.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2dw6+4vHTMuo= +github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/sclevine/agouti v3.0.0+incompatible h1:8IBJS6PWz3uTlMP3YBIR5f+KAldcGuOeFkFbUWfBgK4= github.com/sclevine/spec v1.2.0 h1:1Jwdf9jSfDl9NVmt8ndHqbTZ7XCCPbh1jI3hkDBHVYA= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 h1:58EBmR2dMNL2n/FnbQewK3D14nXr0V9CObDSvMJLq+Y= +github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -516,17 +654,24 @@ github.com/shurcooL/highlight_go v0.0.0-20230708025100-33e05792540a/go.mod h1:kL github.com/shurcooL/octicon v0.0.0-20230705024016-66bff059edb8 h1:W5meM/5DP0Igf+pS3Se363Y2DoDv9LUuZgQ24uG9LNY= github.com/shurcooL/octicon v0.0.0-20230705024016-66bff059edb8/go.mod h1:hWBWTvIJ918VxbNOk2hxQg1/5j1M9yQI1Kp8d9qrOq8= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/snowflakedb/gosnowflake v1.6.3 h1:EJDdDi74YbYt1ty164ge3fMZ0eVZ6KA7b1zmAa/wnRo= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -534,6 +679,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck= +github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= +github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= @@ -549,16 +696,24 @@ github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= +github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q= github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tomarrell/wrapcheck/v2 v2.11.0/go.mod h1:wFL9pDWDAbXhhPZZt+nG8Fu+h29TtnZ2MW6Lx4BRXIU= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= +github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= +github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.34.5 h1:szWNPiGHjo8Dd4v2a59saEhG31DRL2Xf3aJ0ZtTSuqc= @@ -586,8 +741,13 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f h1:mvXjJIHRZyhNuGassLTcXTwjiWq7NmjdavZsUnmFybQ= +github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= +github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -595,7 +755,11 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTN github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k= +go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= +go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo= go.etcd.io/etcd/api/v3 v3.5.6 h1:Cy2qx3npLcYqTKqGJzMypnMv2tiRyifZJ17BlWIWA7A= @@ -629,16 +793,19 @@ go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8= go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY= go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= +golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M= golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= @@ -690,8 +857,10 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= gorm.io/gorm v1.21.4 h1:J0xfPJMRfHgpVcYLrEAIqY/apdvTIkrltPQNHQLq9Qc= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools/gotestsum v1.12.1/go.mod h1:mwDmLbx9DIvr09dnAoGgQPLaSXszNpXpWo2bsQge5BE= gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= k8s.io/api v0.22.5 h1:xk7C+rMjF/EGELiD560jdmwzrB788mfcHiNbMQLIVI8= k8s.io/apimachinery v0.22.5 h1:cIPwldOYm1Slq9VLBRPtEYpyhjIm1C6aAMAoENuvN9s= k8s.io/apiserver v0.22.5 h1:71krQxCUz218ecb+nPhfDsNB6QgP1/4EMvi1a2uYBlg= @@ -726,6 +895,8 @@ modernc.org/tcl v1.5.2 h1:sYNjGr4zK6cDH74USl8wVJRrvDX6UOLpG0j4lFvR0W0= modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/z v1.0.1 h1:WyIDpEpAIx4Hel6q/Pcgj/VhaQV5XPJ2I6ryIYbjnpc= modernc.org/zappy v1.0.0 h1:dPVaP+3ueIUv4guk8PuZ2wiUGcJ1WUVvIheeSSTD0yk= +mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg= +mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= From 4ca83c73839c28aa8e1f995fabe14ee5c18912e7 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Sat, 8 Nov 2025 15:37:30 +0100 Subject: [PATCH 8/8] minor --- embedg-server/custom_bots/bot.go | 2 +- embedg-server/custom_bots/manager.go | 3 ++- embedg-server/embedg/bot.go | 18 +++++++++++++++--- embedg-server/embedg/commands.go | 4 +--- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/embedg-server/custom_bots/bot.go b/embedg-server/custom_bots/bot.go index 0e487b2e8..9ee203c80 100644 --- a/embedg-server/custom_bots/bot.go +++ b/embedg-server/custom_bots/bot.go @@ -25,7 +25,7 @@ type CustomBot struct { } func NewCustomBot(token string, presence CustomBotPresence) (*CustomBot, error) { - logHandler := slogzerolog.Option{Level: slog.LevelInfo, Logger: &log.Logger}.NewZerologHandler() + logHandler := slogzerolog.Option{Level: slog.LevelWarn, Logger: &log.Logger}.NewZerologHandler() client, err := disgo.New(token, bot.WithShardManagerConfigOpts( diff --git a/embedg-server/custom_bots/manager.go b/embedg-server/custom_bots/manager.go index db7d7a8f3..a2f19b65f 100644 --- a/embedg-server/custom_bots/manager.go +++ b/embedg-server/custom_bots/manager.go @@ -28,7 +28,8 @@ func NewCustomBotManager(pg *postgres.PostgresStore, actionHandler *handler.Acti bots: make(map[string]*CustomBot), } - go m.lazyCustomBotGatewayTask() + // TODO: Re-enable once debugged + // go m.lazyCustomBotGatewayTask() return m } diff --git a/embedg-server/embedg/bot.go b/embedg-server/embedg/bot.go index a7a5c5cbf..e6570b4d8 100644 --- a/embedg-server/embedg/bot.go +++ b/embedg-server/embedg/bot.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "time" "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" @@ -13,6 +14,7 @@ import ( "github.com/disgoorg/disgo/handler" disgorest "github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/sharding" + "github.com/disgoorg/snowflake/v2" actionshandler "github.com/merlinfuchs/embed-generator/embedg-server/actions/handler" actionsparser "github.com/merlinfuchs/embed-generator/embedg-server/actions/parser" "github.com/merlinfuchs/embed-generator/embedg-server/db/postgres" @@ -45,16 +47,19 @@ func NewEmbedGenerator( ) (*EmbedGenerator, error) { clientRouter := handler.New() - logHandler := slogzerolog.Option{Level: slog.LevelInfo, Logger: &log.Logger}.NewZerologHandler() + snowflake.AllowUnquoted = true + logHandler := slogzerolog.Option{Level: slog.LevelWarn, Logger: &log.Logger}.NewZerologHandler() client, err := disgo.New(cfg.DiscordToken, bot.WithRest(rest.NewRestClient(cfg.DiscordToken)), bot.WithShardManagerConfigOpts( sharding.WithAutoScaling(false), + sharding.WithIdentifyRateLimiterConfigOpt( + gateway.WithIdentifyMaxConcurrency(4), + ), sharding.WithGatewayConfigOpts( gateway.WithIntents( gateway.IntentGuilds, - gateway.IntentGuildMembers, gateway.IntentGuildExpressions, gateway.IntentGuildMessages, gateway.IntentMessageContent, @@ -82,6 +87,13 @@ func NewEmbedGenerator( Str("username", e.User.Username). Msg("Embed Generator has connected to the gateway and is ready") }), + bot.WithEventListenerFunc(func(e *events.HeartbeatAck) { + log.Info(). + Int("shard_id", e.ShardID()). + Str("last_heartbeat", e.LastHeartbeat.Format(time.RFC3339)). + Str("new_heartbeat", e.NewHeartbeat.Format(time.RFC3339)). + Msg("Heartbeat ACK received") + }), bot.WithEventListeners(clientRouter), bot.WithLogger(slog.New(logHandler)), ) @@ -98,7 +110,7 @@ func NewEmbedGenerator( pg: pg, } - embedg.registerHandlers() + // embedg.registerHandlers() return embedg, nil } diff --git a/embedg-server/embedg/commands.go b/embedg-server/embedg/commands.go index 69c054bc0..331e3db98 100644 --- a/embedg-server/embedg/commands.go +++ b/embedg-server/embedg/commands.go @@ -511,7 +511,7 @@ func (g *EmbedGenerator) handleMessageRestoreCommand(e *handler.CommandEvent) er // TODO: Unparse components - messageDump, err := json.MarshalIndent(actions.MessageWithActions{ + _, err = json.MarshalIndent(actions.MessageWithActions{ Username: message.Author.Username, // AvatarURL: message.Author.AvatarURL("1024"), Content: message.Content, @@ -521,8 +521,6 @@ func (g *EmbedGenerator) handleMessageRestoreCommand(e *handler.CommandEvent) er return fmt.Errorf("failed to marshal message dump: %w", err) } - fmt.Println(messageDump) - return nil }