diff --git a/botv1/bot.go b/botv1/bot.go deleted file mode 100644 index 9f80dc5..0000000 --- a/botv1/bot.go +++ /dev/null @@ -1,61 +0,0 @@ -package bot - -import ( - "log/slog" - - "forge.capytal.company/capytal/dislate/translator" - - "forge.capytal.company/capytal/dislate/bot/gconf" - - dgo "github.com/bwmarrin/discordgo" -) - -type Bot struct { - token string - db gconf.DB - translator translator.Translator - session *dgo.Session - logger *slog.Logger -} - -func NewBot( - token string, - db gconf.DB, - translator translator.Translator, - logger *slog.Logger, -) (*Bot, error) { - discord, err := dgo.New("Bot " + token) - if err != nil { - return &Bot{}, err - } - - return &Bot{ - token: token, - db: db, - translator: translator, - session: discord, - logger: logger, - }, nil -} - -func (b *Bot) Start() error { - b.registerEventHandlers() - - b.session.Identify.Intents = dgo.MakeIntent(dgo.IntentsAllWithoutPrivileged) - - if err := b.session.Open(); err != nil { - return err - } - - if err := b.registerCommands(); err != nil { - return err - } - return nil -} - -func (b *Bot) Stop() error { - if err := b.removeCommands(); err != nil { - return err - } - return b.session.Close() -} diff --git a/botv1/commands.go b/botv1/commands.go deleted file mode 100644 index 0c2ac3f..0000000 --- a/botv1/commands.go +++ /dev/null @@ -1,180 +0,0 @@ -package bot - -import ( - "encoding/json" - "errors" - "fmt" - "log/slog" - "slices" - - "forge.capytal.company/capytal/dislate/botv1/commands" - - dgo "github.com/bwmarrin/discordgo" -) - -func (b *Bot) registerCommands() error { - cs := []commands.Command{ - commands.NewMagageConfig(b.db), - commands.NewManageChannel(b.db), - } - - handlers := make(map[string]func(*dgo.Session, *dgo.InteractionCreate), len(cs)) - componentsHandlers := make(map[string]func(*dgo.Session, *dgo.InteractionCreate)) - - for _, v := range cs { - var cmd *dgo.ApplicationCommand - var err error - subCmds := make(map[string]commands.Command) - - sb := v.Subcommands() - - if len(sb) == 0 { - cmd, err = b.session.ApplicationCommandCreate(b.session.State.User.ID, "", v.Info()) - if err != nil { - return err - } - } else { - subCmdsOpts := make([]*dgo.ApplicationCommandOption, len(sb)) - for i, sb := range sb { - subCmds[sb.Info().Name] = sb - subCmdsOpts[i] = &dgo.ApplicationCommandOption{ - Type: dgo.ApplicationCommandOptionSubCommand, - Name: sb.Info().Name, - Description: sb.Info().Description, - Options: sb.Info().Options, - } - } - info := v.Info() - info.Options = subCmdsOpts - - cmd, err = b.session.ApplicationCommandCreate(b.session.State.User.ID, "", info) - if err != nil { - return err - } - } - - for _, c := range v.Components() { - cj, err := c.Info().MarshalJSON() - if err != nil { - return errors.Join(fmt.Errorf("Failed to marshal command"), err) - } - - var v struct { - CustomID string `json:"custom_id"` - } - if err := json.Unmarshal(cj, &v); err != nil { - return errors.Join(fmt.Errorf("Failed to unmarshal command"), err) - } - - componentsHandlers[v.CustomID] = func(s *dgo.Session, ic *dgo.InteractionCreate) { - b.logger.Debug("Handling message component", - slog.String("id", ic.Interaction.ID), - slog.String("custom_id", ic.Interaction.MessageComponentData().CustomID), - ) - err := c.Handle(s, ic) - if err != nil { - b.logger.Error("Failed to handle message component", - slog.String("custom_id", ic.Interaction.MessageComponentData().CustomID), - slog.String("err", err.Error()), - ) - } - } - } - - handlers[cmd.Name] = func(s *dgo.Session, ic *dgo.InteractionCreate) { - b.logger.Debug("Handling command", - slog.String("id", ic.Interaction.ID), - slog.String("name", ic.Interaction.ApplicationCommandData().Name), - ) - - opts := ic.Interaction.ApplicationCommandData().Options - isSub := slices.IndexFunc( - opts, - func(o *dgo.ApplicationCommandInteractionDataOption) bool { - return o.Type == dgo.ApplicationCommandOptionSubCommand - }, - ) - if isSub != -1 { - sc := opts[isSub] - - err := subCmds[sc.Name].Handle(s, ic) - if err != nil { - _ = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseDeferredChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Content: fmt.Sprintf( - "Error while trying to handle sub command: %s", - err.Error(), - ), - Flags: dgo.MessageFlagsEphemeral, - }, - }) - b.logger.Error("Failed to handle sub command", - slog.String("name", sc.Name), - slog.String("err", err.Error()), - ) - } - - return - } - - err := v.Handle(s, ic) - if err != nil { - _ = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseDeferredChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Content: fmt.Sprintf( - "Error while trying to handle command: %s", - err.Error(), - ), - Flags: dgo.MessageFlagsEphemeral, - }, - }) - b.logger.Error("Failed to handle command", - slog.String("name", cmd.Name), - slog.String("id", cmd.ID), - slog.String("err", err.Error()), - ) - } - } - - b.logger.Info("Registered command", - slog.String("name", cmd.Name), - slog.String("id", cmd.ID), - ) - } - - b.session.AddHandler(func(s *dgo.Session, i *dgo.InteractionCreate) { - switch i.Interaction.Type { - case dgo.InteractionApplicationCommand: - if h, ok := handlers[i.ApplicationCommandData().Name]; ok { - h(s, i) - } - case dgo.InteractionMessageComponent: - if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok { - h(s, i) - } - } - }) - - return nil -} - -func (b *Bot) removeCommands() error { - cmds, err := b.session.ApplicationCommands(b.session.State.Application.ID, "") - if err != nil { - return err - } - - for _, v := range cmds { - err := b.session.ApplicationCommandDelete(b.session.State.User.ID, "", v.ID) - if err != nil { - return err - } - b.logger.Info("Removed command", - slog.String("name", v.Name), - slog.String("id", v.ID), - ) - } - return nil -} diff --git a/botv1/commands/channels.go b/botv1/commands/channels.go deleted file mode 100644 index ed542c8..0000000 --- a/botv1/commands/channels.go +++ /dev/null @@ -1,378 +0,0 @@ -package commands - -import ( - "errors" - "fmt" - "strings" - - "forge.capytal.company/capytal/dislate/bot/gconf" - "forge.capytal.company/capytal/dislate/guilddb" - "forge.capytal.company/capytal/dislate/translator" - - gdb "forge.capytal.company/capytal/dislate/guilddb" - - dgo "github.com/bwmarrin/discordgo" -) - -type ManageChannel struct { - db gconf.DB -} - -func NewManageChannel(db gconf.DB) ManageChannel { - return ManageChannel{db} -} - -func (c ManageChannel) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionManageChannels - - return &dgo.ApplicationCommand{ - Name: "channel", - Description: "Manages a channel options", - DefaultMemberPermissions: &permissions, - } -} - -func (c ManageChannel) Subcommands() []Command { - return []Command{ - channelsInfo(c), - channelsLink(c), - channelsSetLang(c), - } -} - -func (c ManageChannel) Handle(s *dgo.Session, i *dgo.InteractionCreate) error { - return nil -} - -func (c ManageChannel) Components() []Component { - return []Component{} -} - -type channelsInfo struct { - db gconf.DB -} - -func (c channelsInfo) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionManageChannels - - return &dgo.ApplicationCommand{ - Name: "info", - Description: "Get information about a channel", - DefaultMemberPermissions: &permissions, - Options: []*dgo.ApplicationCommandOption{{ - Type: dgo.ApplicationCommandOptionChannel, - Name: "channel", - Description: "The channel to manage", - ChannelTypes: []dgo.ChannelType{ - dgo.ChannelTypeGuildText, - dgo.ChannelTypeGuildForum, - dgo.ChannelTypeGuildPublicThread, - dgo.ChannelTypeGuildPrivateThread, - }, - }}, - } -} - -func (c channelsInfo) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error { - opts := getOptions(ic.ApplicationCommandData().Options) - - var err error - - var dch *dgo.Channel - if c, ok := opts["channel"]; ok { - dch = c.ChannelValue(s) - } else { - dch, err = s.Channel(ic.ChannelID) - if err != nil { - return err - } - } - - ch, err := getChannel(c.db, dch.GuildID, dch.ID) - if err != nil { - return err - } - - info, err := getChannelInfo(c.db, ch) - if err != nil { - return err - } - - err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Embeds: []*dgo.MessageEmbed{info}, - Flags: dgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - return err - } - - return nil -} - -func (c channelsInfo) Components() []Component { - return []Component{} -} - -func (c channelsInfo) Subcommands() []Command { - return []Command{} -} - -type channelsLink struct { - db gconf.DB -} - -func (c channelsLink) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionManageChannels - - return &dgo.ApplicationCommand{ - Name: "link", - Description: "Link two channels together", - DefaultMemberPermissions: &permissions, - Options: []*dgo.ApplicationCommandOption{{ - Type: dgo.ApplicationCommandOptionChannel, - Name: "channel_one", - Description: "The channel to link", - Required: true, - ChannelTypes: []dgo.ChannelType{ - dgo.ChannelTypeGuildText, - dgo.ChannelTypeGuildForum, - dgo.ChannelTypeGuildPublicThread, - dgo.ChannelTypeGuildPrivateThread, - }, - }, { - Type: dgo.ApplicationCommandOptionChannel, - Name: "channel_two", - Description: "The channel to link", - ChannelTypes: []dgo.ChannelType{ - dgo.ChannelTypeGuildText, - dgo.ChannelTypeGuildForum, - dgo.ChannelTypeGuildPublicThread, - dgo.ChannelTypeGuildPrivateThread, - }, - }}, - } -} - -func (c channelsLink) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error { - opts := getOptions(ic.ApplicationCommandData().Options) - - var err error - var dch1, dch2 *dgo.Channel - if c, ok := opts["channel_one"]; ok { - dch1 = c.ChannelValue(s) - } else { - return errors.New("channel_one is required") - } - - if c, ok := opts["channel_two"]; ok { - dch2 = c.ChannelValue(s) - } else { - dch2, err = s.Channel(ic.ChannelID) - if err != nil { - return err - } - } - - if dch1.ID == dch2.ID { - return errors.New("channel_one and channel_two must be different values") - } else if dch1.Type != dch2.Type { - return errors.New("channel_one and channel_two must be the same channel types") - } - - ch1, err := getChannel(c.db, dch1.GuildID, dch1.ID) - if err != nil { - return err - } - ch2, err := getChannel(c.db, dch2.GuildID, dch2.ID) - if err != nil { - return err - } - - var cb1, cb2 guilddb.ChannelGroup - - cb1, err = c.db.ChannelGroup(ch1.GuildID, ch1.ID) - if err != nil && !errors.Is(err, guilddb.ErrNotFound) { - return err - } - cb2, err = c.db.ChannelGroup(ch2.GuildID, ch2.ID) - if err != nil && !errors.Is(err, guilddb.ErrNotFound) { - return err - } - - if len(cb1) > 0 && len(cb2) > 0 { - return errors.New("both channels are already in a group") - } else if len(cb1) > 0 { - cb1 = append(cb1, ch2) - err = c.db.ChannelGroupUpdate(cb1) - } else if len(cb2) > 0 { - cb2 = append(cb2, ch1) - err = c.db.ChannelGroupUpdate(cb2) - } else { - err = c.db.ChannelGroupInsert(guilddb.ChannelGroup{ch1, ch2}) - } - if err != nil { - return err - } - err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Content: fmt.Sprintf( - "Linked channel %s (%s) and %s (%s)", - dch1.Name, dch1.ID, dch2.Name, dch2.ID, - ), - Flags: dgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - return err - } - - return nil -} - -func (c channelsLink) Components() []Component { - return []Component{} -} - -func (c channelsLink) Subcommands() []Command { - return []Command{} -} - -type channelsSetLang struct { - db gconf.DB -} - -func (c channelsSetLang) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionManageChannels - - return &dgo.ApplicationCommand{ - Name: "set-lang", - Description: "Link two channels together", - DefaultMemberPermissions: &permissions, - Options: []*dgo.ApplicationCommandOption{{ - Type: dgo.ApplicationCommandOptionString, - Required: true, - Name: "language", - Description: "The new language", - Choices: []*dgo.ApplicationCommandOptionChoice{ - {Name: "English (EN)", Value: translator.EN}, - {Name: "Portuguese (PT)", Value: translator.PT}, - }, - }, { - Type: dgo.ApplicationCommandOptionChannel, - Name: "channel", - Description: "The channel to change the language", - ChannelTypes: []dgo.ChannelType{ - dgo.ChannelTypeGuildText, - dgo.ChannelTypeGuildForum, - dgo.ChannelTypeGuildPublicThread, - dgo.ChannelTypeGuildPrivateThread, - }, - }}, - } -} - -func (c channelsSetLang) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error { - opts := getOptions(ic.ApplicationCommandData().Options) - - var err error - var dch *dgo.Channel - var l translator.Language - - if c, ok := opts["language"]; ok { - switch c.StringValue() { - case string(translator.PT): - l = translator.PT - default: - l = translator.EN - } - } else { - return errors.New("language is a required option") - } - - if c, ok := opts["channel"]; ok { - dch = c.ChannelValue(s) - } else { - dch, err = s.Channel(ic.ChannelID) - if err != nil { - return err - } - } - - ch, err := getChannel(c.db, dch.GuildID, dch.ID) - if err != nil { - return err - } - - ch.Language = l - - err = c.db.ChannelUpdate(ch) - if err != nil { - return err - } - - err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Content: fmt.Sprintf( - "Changed language of channel %s (%s) to %s", - dch.Name, dch.ID, l, - ), - Flags: dgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - return err - } - - return nil -} - -func (c channelsSetLang) Components() []Component { - return []Component{} -} - -func (c channelsSetLang) Subcommands() []Command { - return []Command{} -} - -func getChannel(db gconf.DB, guildID, channelID string) (gdb.Channel, error) { - ch, err := db.Channel(guildID, channelID) - if errors.Is(err, gdb.ErrNotFound) { - if err := db.ChannelInsert(gdb.NewChannel(guildID, channelID, translator.EN)); err != nil { - return gdb.Channel{}, err - } - ch, err = db.Channel(guildID, channelID) - if err != nil { - return gdb.Channel{}, err - } - } else if err != nil { - return gdb.Channel{}, err - } - - return ch, nil -} - -func getChannelInfo(db gconf.DB, ch gdb.Channel) (*dgo.MessageEmbed, error) { - group, err := db.ChannelGroup(ch.GuildID, ch.ID) - if err != nil && !errors.Is(err, gdb.ErrNotFound) { - return nil, err - } - - g := make([]string, len(group)) - for i, gi := range group { - g[i] = "<#" + gi.ID + ">" - } - - return &dgo.MessageEmbed{ - Title: "Channel Information", - Fields: []*dgo.MessageEmbedField{ - {Name: "ID", Value: ch.ID, Inline: true}, - {Name: "Language", Value: string(ch.Language), Inline: true}, - {Name: "Linked Channels", Value: strings.Join(g, ", "), Inline: true}, - }, - }, nil -} diff --git a/botv1/commands/commands.go b/botv1/commands/commands.go deleted file mode 100644 index 362a252..0000000 --- a/botv1/commands/commands.go +++ /dev/null @@ -1,33 +0,0 @@ -package commands - -import ( - dgo "github.com/bwmarrin/discordgo" -) - -type Command interface { - Info() *dgo.ApplicationCommand - Handle(s *dgo.Session, i *dgo.InteractionCreate) error - Subcommands() []Command - Components() []Component -} - -type Component interface { - Info() dgo.MessageComponent - Handle(s *dgo.Session, i *dgo.InteractionCreate) error -} - -func getOptions( - opts []*dgo.ApplicationCommandInteractionDataOption, -) map[string]*dgo.ApplicationCommandInteractionDataOption { - m := make(map[string]*dgo.ApplicationCommandInteractionDataOption, len(opts)) - - for _, opt := range opts { - if opt.Type == dgo.ApplicationCommandOptionSubCommand { - return getOptions(opt.Options) - } else { - m[opt.Name] = opt - } - } - - return m -} diff --git a/botv1/commands/config.go b/botv1/commands/config.go deleted file mode 100644 index 83616ce..0000000 --- a/botv1/commands/config.go +++ /dev/null @@ -1,187 +0,0 @@ -package commands - -import ( - e "errors" - "fmt" - "log/slog" - - "forge.capytal.company/capytal/dislate/bot/gconf" - - dgo "github.com/bwmarrin/discordgo" -) - -type ManageConfig struct { - db gconf.DB -} - -func NewMagageConfig(db gconf.DB) ManageConfig { - return ManageConfig{db} -} - -func (c ManageConfig) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionAdministrator - - return &dgo.ApplicationCommand{ - Name: "config", - Description: "Manages the guild's configuration", - DefaultMemberPermissions: &permissions, - } -} - -func (c ManageConfig) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error { - return nil -} - -func (c ManageConfig) Components() []Component { - return []Component{} -} - -func (c ManageConfig) Subcommands() []Command { - return []Command{ - loggerConfigChannel(c), - loggerConfigLevel(c), - } -} - -type loggerConfigChannel struct { - db gconf.DB -} - -func (c loggerConfigChannel) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionAdministrator - return &dgo.ApplicationCommand{ - Name: "log-channel", - Description: "Change logging channel", - DefaultMemberPermissions: &permissions, - Options: []*dgo.ApplicationCommandOption{{ - Type: dgo.ApplicationCommandOptionChannel, - Required: true, - Name: "log-channel", - Description: "The channel to send log messages and errors to", - ChannelTypes: []dgo.ChannelType{ - dgo.ChannelTypeGuildText, - }, - }}, - } -} - -func (c loggerConfigChannel) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error { - opts := getOptions(ic.ApplicationCommandData().Options) - - var err error - var dch *dgo.Channel - if c, ok := opts["log-channel"]; ok { - dch = c.ChannelValue(s) - } else { - dch, err = s.Channel(ic.ChannelID) - if err != nil { - return err - } - } - - guild, err := c.db.Guild(ic.GuildID) - if err != nil { - return err - } - - conf := guild.Config - conf.LoggingChannel = &dch.ID - guild.Config = conf - - err = c.db.GuildUpdate(guild) - if err != nil { - return err - } - - err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Content: fmt.Sprintf("Logging channel changed to %s", *guild.Config.LoggingChannel), - Flags: dgo.MessageFlagsEphemeral, - }, - }) - - return err -} - -func (c loggerConfigChannel) Components() []Component { - return []Component{} -} - -func (c loggerConfigChannel) Subcommands() []Command { - return []Command{} -} - -type loggerConfigLevel struct { - db gconf.DB -} - -func (c loggerConfigLevel) Info() *dgo.ApplicationCommand { - var permissions int64 = dgo.PermissionAdministrator - return &dgo.ApplicationCommand{ - Name: "log-level", - Description: "Change logging channel", - DefaultMemberPermissions: &permissions, - Options: []*dgo.ApplicationCommandOption{{ - Type: dgo.ApplicationCommandOptionString, - Required: true, - Name: "log-level", - Description: "The logging level of messages and errors", - Choices: []*dgo.ApplicationCommandOptionChoice{ - {Name: "Debug", Value: slog.LevelDebug.String()}, - {Name: "Info", Value: slog.LevelInfo.String()}, - {Name: "Warn", Value: slog.LevelWarn.String()}, - {Name: "Error", Value: slog.LevelError.String()}, - }, - }}, - } -} - -func (c loggerConfigLevel) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error { - opts := getOptions(ic.ApplicationCommandData().Options) - - var err error - - opt, ok := opts["log-level"] - if !ok { - return e.New("Parameter log-level is required") - } - - var l slog.Level - err = l.UnmarshalText([]byte(opt.StringValue())) - if err != nil { - return e.Join(e.New("Parameter log-level is not a valid value"), err) - } - - guild, err := c.db.Guild(ic.GuildID) - if err != nil { - return err - } - - conf := guild.Config - conf.LoggingLevel = &l - guild.Config = conf - - err = c.db.GuildUpdate(guild) - if err != nil { - return err - } - - err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{ - Type: dgo.InteractionResponseChannelMessageWithSource, - Data: &dgo.InteractionResponseData{ - Content: fmt.Sprintf("Logging level changed to %s", l), - Flags: dgo.MessageFlagsEphemeral, - }, - }) - - return err -} - -func (c loggerConfigLevel) Components() []Component { - return []Component{} -} - -func (c loggerConfigLevel) Subcommands() []Command { - return []Command{} -} diff --git a/botv1/events.go b/botv1/events.go deleted file mode 100644 index bb193de..0000000 --- a/botv1/events.go +++ /dev/null @@ -1,32 +0,0 @@ -package bot - -import ( - "forge.capytal.company/capytal/dislate/bot/events" - - dgo "github.com/bwmarrin/discordgo" -) - -func w[E any](h events.EventHandler[E]) interface{} { - return func(s *dgo.Session, ev E) { - err := h.Serve(s, ev) - if err != nil { - err.Log() - err.Send() - err.Reply() - } - } -} - -func (b *Bot) registerEventHandlers() { - ehs := []any{ - w(events.NewGuildCreate(b.logger, b.db)), - w(events.NewMessageCreate(b.db, b.translator)), - w(events.NewMessageUpdate(b.db, b.translator)), - w(events.NewMessageDelete(b.db)), - w(events.NewReady(b.logger, b.db)), - w(events.NewThreadCreate(b.db, b.translator)), - } - for _, h := range ehs { - b.session.AddHandler(h) - } -} diff --git a/botv1/events/errors/default.go b/botv1/events/errors/default.go deleted file mode 100644 index 5e8fca4..0000000 --- a/botv1/events/errors/default.go +++ /dev/null @@ -1,115 +0,0 @@ -package errors - -import ( - "fmt" - "log/slog" - "reflect" - "strings" - - dgo "github.com/bwmarrin/discordgo" -) - -type defaultEventErr[E any] struct { - message string - data map[string]any - session *dgo.Session - channelID string - messageReference *dgo.MessageReference - logger *slog.Logger - errs []error -} - -func (d *defaultEventErr[E]) Join(errs ...error) EventErr { - n := 0 - for _, err := range errs { - if err != nil { - n++ - } - } - if n == 0 { - return nil - } - e := &defaultEventErr[E]{ - message: d.message, - data: d.data, - session: d.session, - channelID: d.channelID, - messageReference: d.messageReference, - logger: d.logger, - errs: make([]error, 0, n), - } - for _, err := range errs { - if err != nil { - e.errs = append(e.errs, err) - } - } - return e -} - -func (d *defaultEventErr[E]) Error() string { - var data []string - for k, v := range d.data { - data = append(data, slog.Any(k, v).String()) - } - - var e string - if d.message != "" { - e = fmt.Sprintf("%s-ERRO: %s %s", d.message, d.Event(), strings.Join(data, " ")) - } else { - e = fmt.Sprintf("%s-ERRO: %s", d.Event(), strings.Join(data, " ")) - } - - var s strings.Builder - _, berr := s.WriteString(e) - if berr != nil { - return "Failed to write error string" - } - for _, err := range d.errs { - _, berr := s.WriteString("\n" + err.Error()) - if berr != nil { - return "Failed to write error string" - } - } - return s.String() -} - -func (d *defaultEventErr[E]) Event() string { - var e E - t := reflect.TypeOf(e) - if t.Kind() == reflect.Pointer { - t = t.Elem() - } - - return strings.ToUpper(t.Name()) -} - -func (d *defaultEventErr[E]) Reply() error { - if d.channelID == "" || d.messageReference == nil || d.session == nil { - return nil - } - - _, err := d.session.ChannelMessageSendReply(d.channelID, d.Error(), d.messageReference) - return err -} - -func (d *defaultEventErr[E]) Send() error { - if d.channelID == "" || d.session == nil { - return nil - } - - _, err := d.session.ChannelMessageSend(d.channelID, d.Error()) - return err -} - -func (d *defaultEventErr[E]) Log() { - var args []any - for k, v := range d.data { - args = append(args, slog.Any(k, v)) - } - - d.logger.Error(d.Error(), args...) -} - -func (d *defaultEventErr[E]) AddData(key string, v any) { - d.data[key] = v -} diff --git a/botv1/events/errors/errors.go b/botv1/events/errors/errors.go deleted file mode 100644 index 5a19eea..0000000 --- a/botv1/events/errors/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package errors - -type EventErr interface { - Error() string - Event() string - Reply() error - Send() error - Log() - Join(...error) EventErr -} diff --git a/botv1/events/errors/guild.go b/botv1/events/errors/guild.go deleted file mode 100644 index 43d6543..0000000 --- a/botv1/events/errors/guild.go +++ /dev/null @@ -1,42 +0,0 @@ -package errors - -import ( - "log/slog" - - dgo "github.com/bwmarrin/discordgo" -) - -type GuildErr[E any] struct { - *defaultEventErr[E] -} - -func NewGuildErr[E any]( - g *dgo.Guild, - log *slog.Logger, -) GuildErr[E] { - return GuildErr[E]{&defaultEventErr[E]{ - data: map[string]any{ - "GuildID": g.ID, - }, - logger: log, - }} -} - -type ReadyErr struct { - *defaultEventErr[*dgo.Ready] -} - -func NewReadyErr( - ev *dgo.Ready, - log *slog.Logger, -) ReadyErr { - return ReadyErr{&defaultEventErr[*dgo.Ready]{ - data: map[string]any{ - "SessionID": ev.SessionID, - "BotUserID": ev.User.ID, - "BotUserName": ev.User.Username, - "Guilds": ev.Guilds, - }, - logger: log, - }} -} diff --git a/botv1/events/errors/messages.go b/botv1/events/errors/messages.go deleted file mode 100644 index 7cc16b6..0000000 --- a/botv1/events/errors/messages.go +++ /dev/null @@ -1,35 +0,0 @@ -package errors - -import ( - "log/slog" - - dgo "github.com/bwmarrin/discordgo" -) - -type MessageErr[E any] struct { - *defaultEventErr[E] -} - -func NewMessageErr[E any]( - s *dgo.Session, - msg *dgo.Message, - log *slog.Logger, -) MessageErr[E] { - var authorID string - if msg.Author != nil { - authorID = msg.Author.ID - } - - return MessageErr[E]{&defaultEventErr[E]{ - data: map[string]any{ - "MessageID": msg.ID, - "ChannelID": msg.ChannelID, - "GuildID": msg.GuildID, - "AuthorID": authorID, - }, - session: s, - channelID: msg.ChannelID, - messageReference: msg.Reference(), - logger: log, - }} -} diff --git a/botv1/events/errors/threads.go b/botv1/events/errors/threads.go deleted file mode 100644 index 150a769..0000000 --- a/botv1/events/errors/threads.go +++ /dev/null @@ -1,24 +0,0 @@ -package errors - -import ( - "log/slog" - - dgo "github.com/bwmarrin/discordgo" -) - -type ThreadCreateErr struct { - *defaultEventErr[*dgo.ThreadCreate] -} - -func NewThreadCreateErr(s *dgo.Session, ev *dgo.ThreadCreate, log *slog.Logger) ThreadCreateErr { - return ThreadCreateErr{&defaultEventErr[*dgo.ThreadCreate]{ - data: map[string]any{ - "ThreadID": ev.ID, - "ParentID": ev.ParentID, - "GuildID": ev.GuildID, - }, - session: s, - channelID: ev.ID, - logger: log, - }} -} diff --git a/botv1/events/events.go b/botv1/events/events.go deleted file mode 100644 index 79bdb76..0000000 --- a/botv1/events/events.go +++ /dev/null @@ -1,11 +0,0 @@ -package events - -import ( - "forge.capytal.company/capytal/dislate/bot/events/errors" - - dgo "github.com/bwmarrin/discordgo" -) - -type EventHandler[E any] interface { - Serve(*dgo.Session, E) errors.EventErr -} diff --git a/botv1/events/guild.go b/botv1/events/guild.go deleted file mode 100644 index 4557fa4..0000000 --- a/botv1/events/guild.go +++ /dev/null @@ -1,65 +0,0 @@ -package events - -import ( - e "errors" - "log/slog" - - "forge.capytal.company/capytal/dislate/bot/events/errors" - "forge.capytal.company/capytal/dislate/bot/gconf" - - gdb "forge.capytal.company/capytal/dislate/guilddb" - - dgo "github.com/bwmarrin/discordgo" -) - -type GuildCreate struct { - log *slog.Logger - db gconf.DB -} - -func NewGuildCreate(log *slog.Logger, db gconf.DB) GuildCreate { - return GuildCreate{log, db} -} - -func (h GuildCreate) Serve(s *dgo.Session, ev *dgo.GuildCreate) errors.EventErr { - err := h.db.GuildInsert(gdb.Guild[gconf.ConfigString]{ID: ev.Guild.ID}) - - everr := errors.NewGuildErr[*dgo.GuildCreate](ev.Guild, h.log) - - if err != nil && !e.Is(err, gdb.ErrNoAffect) { - return everr.Join(e.New("Failed to add guild to database"), err) - } else if err != nil { - h.log.Info("Guild already in database", slog.String("id", ev.Guild.ID)) - } else { - h.log.Info("Added guild", slog.String("id", ev.Guild.ID)) - } - - return nil -} - -type Ready struct { - log *slog.Logger - db gconf.DB -} - -func NewReady(log *slog.Logger, db gconf.DB) EventHandler[*dgo.Ready] { - return Ready{log, db} -} - -func (h Ready) Serve(s *dgo.Session, ev *dgo.Ready) errors.EventErr { - everr := errors.NewReadyErr(ev, h.log) - - for _, g := range ev.Guilds { - err := h.db.GuildInsert(gdb.Guild[gconf.ConfigString]{ID: g.ID}) - - if err != nil && !e.Is(err, gdb.ErrNoAffect) { - return everr.Join(err) - } else if err != nil { - h.log.Info("Guild already in database", slog.String("id", g.ID)) - } else { - h.log.Info("Added guild", slog.String("id", g.ID)) - } - } - - return nil -} diff --git a/botv1/events/messages.go b/botv1/events/messages.go deleted file mode 100644 index 2602cee..0000000 --- a/botv1/events/messages.go +++ /dev/null @@ -1,440 +0,0 @@ -package events - -import ( - e "errors" - "log/slog" - "slices" - "sync" - - "forge.capytal.company/capytal/dislate/bot/events/errors" - "forge.capytal.company/capytal/dislate/bot/gconf" - "forge.capytal.company/capytal/dislate/guilddb" - "forge.capytal.company/capytal/dislate/translator" - - dgo "github.com/bwmarrin/discordgo" -) - -type MessageCreate struct { - db gconf.DB - translator translator.Translator -} - -func NewMessageCreate(db gconf.DB, t translator.Translator) MessageCreate { - return MessageCreate{db, t} -} - -func (h MessageCreate) Serve( - s *dgo.Session, - ev *dgo.MessageCreate, -) errors.EventErr { - if ev.Message.Author.Bot || ev.Type != dgo.MessageTypeDefault { - return nil - } - - log := gconf.GetLogger(ev.Message.GuildID, s, h.db) - return h.sendMessage(log, s, ev.Message) -} - -func (h MessageCreate) sendMessage( - log *slog.Logger, - s *dgo.Session, - msg *dgo.Message, -) errors.EventErr { - everr := errors.NewMessageErr[*dgo.MessageCreate](s, msg, log) - - ch, err := h.db.Channel(msg.GuildID, msg.ChannelID) - if e.Is(err, guilddb.ErrNotFound) { - log.Debug("Channel is not in database, ignoring.", - slog.String("guild", msg.GuildID), - slog.String("channel", msg.ChannelID), - slog.String("message", msg.ID), - ) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get channel from database"), err) - } - - gc, err := h.db.ChannelGroup(ch.GuildID, ch.ID) - if e.Is(err, guilddb.ErrNotFound) { - log.Debug("Channel is not in a group, ignoring.", - slog.String("guild", msg.GuildID), - slog.String("channel", msg.ChannelID), - slog.String("message", msg.ID), - ) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get channel group from database"), err) - } - - _, err = getMessage(h.db, msg, ch.Language) - if err != nil { - return everr.Join(e.New("Failed to get/add message to database"), err) - } - - var wg sync.WaitGroup - errs := make(chan errors.EventErr) - - for _, c := range gc { - if c.ID == ch.ID && c.GuildID == ch.GuildID { - continue - } - wg.Add(1) - go func(c guilddb.Channel, errs chan<- errors.EventErr) { - defer wg.Done() - - everr := errors.NewMessageErr[*dgo.MessageCreate](s, msg, log) - everr.AddData("TranslatedChannelID", c.ID) - - dch, err := s.Channel(c.ID) - - var channelID string - if err != nil { - errs <- everr.Join(e.New("Failed to get information about translated channel"), err) - return - } else if dch.IsThread() { - channelID = dch.ParentID - } else { - channelID = dch.ID - } - - uw, err := getUserWebhook(s, channelID, msg.Author) - if err != nil { - errs <- everr.Join(e.New("Failed to get/set user webhook for translated channel"), err) - return - } - - t, err := h.translator.Translate(ch.Language, c.Language, msg.Content) - if err != nil { - errs <- everr.Join(e.New("Error while trying to translate message"), err) - return - } - - var tdm *dgo.Message - if dch.IsThread() { - tdm, err = s.WebhookThreadExecute(uw.ID, uw.Token, true, dch.ID, &dgo.WebhookParams{ - AvatarURL: msg.Author.AvatarURL(""), - Username: msg.Author.GlobalName, - Content: t, - }) - } else { - tdm, err = s.WebhookExecute(uw.ID, uw.Token, true, &dgo.WebhookParams{ - AvatarURL: msg.Author.AvatarURL(""), - Username: msg.Author.GlobalName, - Content: t, - }) - } - if err != nil { - everr.AddData("WebhookID", uw.ID) - errs <- everr.Join(e.New("Error while trying to execute user webhook"), err) - return - } - - if tdm.GuildID == "" { - tdm.GuildID = msg.GuildID - } - - _, err = getTranslatedMessage(h.db, tdm, msg, c.Language) - if err != nil { - everr.AddData("WebhookID", uw.ID) - everr.AddData("TranslatedMessageID", uw.ID) - errs <- everr.Join(e.New("Error while trying to add translated message to dabase"), err) - return - } - }(c, errs) - - } - - wg.Wait() - for err := range errs { - everr.Join(err) - } - if len(errs) > 0 { - return everr - } - - return nil -} - -type MessageUpdate struct { - db gconf.DB - translator translator.Translator -} - -func NewMessageUpdate(db gconf.DB, t translator.Translator) MessageUpdate { - return MessageUpdate{db, t} -} - -func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) errors.EventErr { - if ev.Message.Author.Bot || ev.Type != dgo.MessageTypeDefault { - return nil - } - - log := gconf.GetLogger(ev.Message.GuildID, s, h.db) - everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log) - - msg, err := h.db.Message(ev.Message.GuildID, ev.Message.ChannelID, ev.Message.ID) - if e.Is(err, guilddb.ErrNotFound) { - log.Debug("Message is not in database, ignoring.", - slog.String("guild", ev.Message.GuildID), - slog.String("channel", ev.Message.ChannelID), - ) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get message from database"), err) - } - - tmsgs, err := h.db.MessagesWithOrigin(msg.GuildID, msg.ChannelID, msg.ID) - if e.Is(err, guilddb.ErrNotFound) { - log.Debug("No translated message found, ignoring.", - slog.String("guild", ev.GuildID), - slog.String("channel", ev.ChannelID), - ) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get translated messages from database"), err) - } - - var wg sync.WaitGroup - errs := make(chan errors.EventErr) - - for _, m := range tmsgs { - if m.ID == msg.ID && m.GuildID == msg.GuildID { - continue - } - wg.Add(1) - go func(m guilddb.Message, errs chan<- errors.EventErr) { - defer wg.Done() - - everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log) - everr.AddData("TranslatedMessageID", m.ID) - everr.AddData("TranslatedChannelID", m.ChannelID) - - var channelID string - if dch, err := s.Channel(m.ChannelID); err != nil { - errs <- everr.Join(e.New("Failed to get information about translated channel"), err) - return - } else if dch.IsThread() { - channelID = dch.ParentID - } else { - channelID = dch.ID - } - - uw, err := getUserWebhook(s, channelID, ev.Message.Author) - if err != nil { - errs <- everr.Join(e.New("Failed to get/set user webhook for translated channel"), err) - return - } - - t, err := h.translator.Translate(msg.Language, m.Language, ev.Message.Content) - if err != nil { - errs <- everr.Join(e.New("Error while trying to translate message"), err) - return - } - - _, err = s.WebhookMessageEdit(uw.ID, uw.Token, m.ID, &dgo.WebhookEdit{ - Content: &t, - }) - if err != nil { - everr.AddData("WebhookID", uw.ID) - errs <- everr.Join(e.New("Error while trying to execute user webhook"), err) - return - } - }(m, errs) - - } - - wg.Wait() - for err := range errs { - everr.Join(err) - } - if len(errs) > 0 { - return everr - } - - return nil -} - -type MessageDelete struct { - db gconf.DB -} - -func NewMessageDelete(db gconf.DB) MessageDelete { - return MessageDelete{db} -} - -func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) errors.EventErr { - if ev.Type != dgo.MessageTypeDefault { - return nil - } - - log := gconf.GetLogger(ev.Message.GuildID, s, h.db) - everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log) - - msg, err := h.db.Message(ev.Message.GuildID, ev.Message.ChannelID, ev.Message.ID) - if e.Is(err, guilddb.ErrNotFound) { - log.Debug("Message is not in database, ignoring.", - slog.String("guild", ev.Message.GuildID), - slog.String("channel", ev.Message.ChannelID), - ) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get message from database"), err) - } - - var originChannelID, originID string - if msg.OriginID != nil && msg.OriginChannelID != nil { - oMsg, err := h.db.Message(ev.Message.GuildID, *msg.OriginChannelID, *msg.OriginID) - if err != nil { - originChannelID = *msg.OriginChannelID - originID = *msg.OriginID - } else { - msg = oMsg - originChannelID = oMsg.ChannelID - originID = oMsg.ID - } - } else { - originChannelID = msg.ChannelID - originID = msg.ID - } - - tmsgs, err := h.db.MessagesWithOrigin(msg.GuildID, originChannelID, originID) - if e.Is(err, guilddb.ErrNotFound) { - log.Debug("No translated message found, ignoring.", - slog.String("guild", ev.GuildID), - slog.String("channel", ev.ChannelID), - ) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get translated messages from database"), err) - } - - for _, m := range tmsgs { - if m.ID == msg.ID && m.ChannelID == msg.ChannelID && m.GuildID == msg.GuildID { - continue - } - go func(m guilddb.Message) { - if err := s.ChannelMessageDelete(m.ChannelID, m.ID); err != nil { - log.Warn("Failed to delete message", - slog.String("channel", m.ChannelID), - slog.String("message", m.ID), - slog.String("err", err.Error()), - ) - } - }(m) - } - - if err := s.ChannelMessageDelete(msg.ChannelID, msg.ID); err != nil { - log.Warn("Failed to delete message", - slog.String("channel", msg.ChannelID), - slog.String("message", msg.ID), - slog.String("err", err.Error()), - ) - } - - var wg sync.WaitGroup - errs := make(chan errors.EventErr) - - for _, m := range append(tmsgs, msg) { - go func(m guilddb.Message, errs chan<- errors.EventErr) { - everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log) - everr.AddData("TranslatedMessageID", m.ID) - everr.AddData("TranslatedChannelID", m.ChannelID) - - err := h.db.MessageDeleteFromChannel(guilddb.NewChannel(m.GuildID, m.ID, translator.EN)) - if err != nil && !e.Is(err, guilddb.ErrNoAffect) { - errs <- everr.Join(e.New("Failed to delete message from channel"), err) - return - } - - err = h.db.ChannelDelete(guilddb.NewChannel(m.GuildID, m.ID, translator.EN)) - if err != nil && !e.Is(err, guilddb.ErrNoAffect) { - errs <- everr.Join(e.New("Failed to delete message thread from channel"), err) - return - } - }(m, errs) - } - - wg.Wait() - - everrs := make([]error, 0, len(errs)) - for err := range errs { - everrs = append(everrs, err) - } - if len(errs) > 0 { - return everr.Join(everrs...) - } - - if err := h.db.MessageDelete(guilddb.NewMessage(msg.GuildID, msg.ChannelID, msg.ID, translator.EN)); err != nil { - return everr.Join(e.New("Failed to delete message from database"), err) - } - - return nil -} - -func getUserWebhook(s *dgo.Session, channelID string, user *dgo.User) (*dgo.Webhook, error) { - whName := "DISLATE_USER_WEBHOOK_" + user.ID - - ws, err := s.ChannelWebhooks(channelID) - if err != nil { - return &dgo.Webhook{}, err - } - wi := slices.IndexFunc(ws, func(w *dgo.Webhook) bool { - return w.Name == whName - }) - - if wi > -1 { - return ws[wi], nil - } - - w, err := s.WebhookCreate(channelID, whName, user.AvatarURL("")) - if err != nil { - return &dgo.Webhook{}, err - } - - return w, nil -} - -func getMessage(db gconf.DB, m *dgo.Message, lang translator.Language) (guilddb.Message, error) { - msg, err := db.Message(m.GuildID, m.ChannelID, m.ID) - - if e.Is(err, guilddb.ErrNotFound) { - if err := db.MessageInsert(guilddb.NewMessage(m.GuildID, m.ChannelID, m.ID, lang)); err != nil { - return guilddb.Message{}, err - } - msg, err = db.Message(m.GuildID, m.ChannelID, m.ID) - if err != nil { - return guilddb.Message{}, err - } - } - - return msg, nil -} - -func getTranslatedMessage( - db gconf.DB, - m, original *dgo.Message, - lang translator.Language, -) (guilddb.Message, error) { - msg, err := db.Message(m.GuildID, m.ChannelID, m.ID) - - if e.Is(err, guilddb.ErrNotFound) { - if err := db.MessageInsert(guilddb.NewTranslatedMessage( - m.GuildID, - m.ChannelID, - m.ID, - lang, - original.ChannelID, - original.ID, - )); err != nil { - return guilddb.Message{}, err - } - msg, err = db.Message(m.GuildID, m.ChannelID, m.ID) - if err != nil { - return guilddb.Message{}, err - } - } else if err != nil { - return guilddb.Message{}, err - } - - return msg, nil -} diff --git a/botv1/events/threads.go b/botv1/events/threads.go deleted file mode 100644 index 448ef53..0000000 --- a/botv1/events/threads.go +++ /dev/null @@ -1,497 +0,0 @@ -package events - -import ( - e "errors" - "log/slog" - "slices" - "sync" - - "forge.capytal.company/capytal/dislate/bot/events/errors" - "forge.capytal.company/capytal/dislate/bot/gconf" - "forge.capytal.company/capytal/dislate/translator" - - gdb "forge.capytal.company/capytal/dislate/guilddb" - - dgo "github.com/bwmarrin/discordgo" -) - -type EThreadCreate struct { - db gconf.DB - translator translator.Translator -} - -func NewEThreadCreate(db gconf.DB, t translator.Translator) EThreadCreate { - return EThreadCreate{db, t} -} - -func (h EThreadCreate) Serve(s *dgo.Session, ev *dgo.ThreadCreate) errors.EventErr { - log := gconf.GetLogger(ev.GuildID, s, h.db) - everr := errors.NewThreadCreateErr(s, ev, log) - - parentCh, err := h.db.Channel(ev.GuildID, ev.ParentID) - if e.Is(err, gdb.ErrNotFound) { - log.Debug("Parent channel of thread not in database, ignoring", - slog.String("thread", ev.ID), - slog.String("parent", ev.ParentID), - ) - return nil - } - - // INFO: Threads have the same ID as the origin message of them - threadMsg, err := h.db.Message(ev.GuildID, ev.ParentID, ev.ID) - - var startMsg *dgo.Message - - // If no thread message is found in database, it is probably a thread started without - // a source message or a forum post. - if e.Is(err, gdb.ErrNotFound) { - ms, err := s.ChannelMessages(ev.ID, 10, "", "", "") - if err != nil { - return everr.Join(e.New("Failed to get messages of thread"), err) - } else if len(ms) == 0 { - log.Debug("Failed to get messages of thread, empty slice returned, probably created by bot, ignoring", - slog.String("thread", ev.ID), - slog.String("parent", ev.ParentID), - ) - return nil - } - - threadMsg = gdb.NewMessage(ev.GuildID, ev.ParentID, ev.ID, parentCh.Language) - startMsg = ms[0] - - } else if err != nil { - return everr.Join(e.New("Failed to get thread starter message from database"), err) - } - - var originMsg gdb.Message - if threadMsg.OriginID != nil && threadMsg.OriginChannelID != nil { - oMsg, err := h.db.Message(ev.GuildID, *threadMsg.OriginChannelID, *threadMsg.OriginID) - if err != nil { - originMsg = threadMsg - } else { - originMsg = oMsg - } - } else { - originMsg = threadMsg - } - - dth, err := s.Channel(ev.ID) - if err != nil { - return everr.Join(e.New("Failed to get discord thread"), err) - } else if !dth.IsThread() { - return everr.Join(e.New("Channel is not a thread")) - } - - th := gdb.NewChannel(dth.GuildID, dth.ID, threadMsg.Language) - if err := h.db.ChannelInsert(th); e.Is(err, gdb.ErrNoAffect) { - if err = h.db.MessageInsert(threadMsg); err != nil && !e.Is(err, gdb.ErrNoAffect) { - return everr.Join(e.New("Failed to add thread started message to database"), err) - } - return nil - } else if err != nil { - return everr.Join(e.New("Failed to add thread channel to database"), err) - } - - parentChannelGroup, err := h.db.ChannelGroup(parentCh.GuildID, parentCh.ID) - if e.Is(err, gdb.ErrNotFound) { - parentChannelGroup = gdb.ChannelGroup{parentCh} - } else if err != nil { - return everr.Join(e.New("Failed to get parent channel group")) - } - - var wg sync.WaitGroup - - tg := make(chan gdb.Channel, len(parentChannelGroup)) - errs := make(chan errors.EventErr) - - for _, pc := range parentChannelGroup { - if pc.ID == dth.ParentID { - continue - } - - m, err := h.db.MessageWithOriginByLang(pc.GuildID, pc.ID, originMsg.ID, pc.Language) - if e.Is(err, gdb.ErrNotFound) && startMsg != nil { - - wg.Add(1) - go func(pc gdb.Channel, tg chan<- gdb.Channel, errs chan<- errors.EventErr) { - defer wg.Done() - - everr := errors.NewThreadCreateErr(s, ev, log) - everr.AddData("TranslatedParentID", pc.ID) - - parentDCh, err := s.Channel(pc.ID) - if err != nil { - errs <- everr.Join(e.New("Failed to get translated parent channel object"), err) - return - } - - content, err := h.translator.Translate( - parentCh.Language, - pc.Language, - startMsg.Content, - ) - if err != nil { - errs <- everr.Join(e.New("Failed to translate forum post of thread"), err) - return - } - - var dtth *dgo.Channel - var msg *dgo.Message - - if parentDCh.Type == dgo.ChannelTypeGuildForum && startMsg != nil { - tags := slices.DeleteFunc(dth.AppliedTags, func(t string) bool { - return !slices.ContainsFunc( - parentDCh.AvailableTags, - func(pt dgo.ForumTag) bool { - return pt.Name == t - }, - ) - }) - - dtth, err = s.ForumThreadStartComplex(pc.ID, &dgo.ThreadStart{ - Name: dth.Name, - AutoArchiveDuration: dth.ThreadMetadata.AutoArchiveDuration, - Type: dth.Type, - Invitable: dth.ThreadMetadata.Invitable, - RateLimitPerUser: dth.RateLimitPerUser, - AppliedTags: tags, - }, &dgo.MessageSend{ - Content: content, - Embeds: startMsg.Embeds, - TTS: startMsg.TTS, - Components: startMsg.Components, - }) - if err != nil { - errs <- everr.Join(e.New("Failed to translate forum post of thread"), err) - return - } - - msg, err = s.ChannelMessage(dtth.ID, dtth.ID) - if err != nil { - errs <- everr.Join(e.New("Failed to get translated thread starter message"), err) - return - } - - } else { - dtth, err = s.ThreadStartComplex(pc.ID, &dgo.ThreadStart{ - Name: dth.Name, - AutoArchiveDuration: dth.ThreadMetadata.AutoArchiveDuration, - Type: dth.Type, - Invitable: dth.ThreadMetadata.Invitable, - RateLimitPerUser: dth.RateLimitPerUser, - }) - if err != nil { - errs <- everr.Join(e.New("Failed to create thread"), err) - return - } - - uw, err := getUserWebhook(s, pc.ID, startMsg.Author) - if err != nil { - errs <- everr.Join(e.New("Failed to get/set user webhook for parent channel of translated thread"), err) - return - } - - msg, err = s.WebhookThreadExecute(uw.ID, uw.Token, true, dtth.ID, &dgo.WebhookParams{ - AvatarURL: startMsg.Author.AvatarURL(""), - Username: startMsg.Author.GlobalName, - Content: content, - }) - if err != nil { - errs <- everr.Join(e.New("Error while trying to execute user webhook"), err) - return - } - } - - if err := h.db.ChannelInsert(gdb.NewChannel(dtth.GuildID, dtth.ID, pc.Language)); err != nil && - !e.Is(err, gdb.ErrNoAffect) { - everr.AddData("TranslatedThreadID", dtth.ID) - errs <- everr.Join(e.New("Failed to add translated thread to database"), err) - return - } - - err = h.db.MessageInsert( - gdb.NewTranslatedMessage( - dtth.GuildID, - dtth.ID, - msg.ID, - pc.Language, - startMsg.ChannelID, - startMsg.ID, - ), - ) - if err != nil { - errs <- everr.Join(e.New("Failed to add translated thread starter message to database"), err) - return - } - - tg <- gdb.NewChannel(dtth.GuildID, dtth.ID, pc.Language) - }( - pc, - tg, - errs, - ) - - } else if err != nil { - return everr.Join(e.New("Failed to get thread translated start message"), err) - } else { - - wg.Add(1) - go func(m gdb.Message, tg chan<- gdb.Channel, errs chan<- errors.EventErr) { - defer wg.Done() - everr := errors.NewThreadCreateErr(s, ev, log) - everr.AddData("TranslatedParentID", m.ChannelID) - - dtth, err := s.MessageThreadStartComplex( - m.ChannelID, - m.ID, - &dgo.ThreadStart{ - Name: dth.Name, - AutoArchiveDuration: dth.ThreadMetadata.AutoArchiveDuration, - Type: dth.Type, - Invitable: dth.ThreadMetadata.Invitable, - RateLimitPerUser: dth.RateLimitPerUser, - AppliedTags: dth.AppliedTags, - }, - ) - if err != nil { - errs <- everr.Join(e.New("Failed to create translated thread"), err) - return - } - everr.AddData("TranslatedThreadID", pc.ID) - - if err := h.db.ChannelInsert(gdb.NewChannel(dtth.GuildID, dtth.ID, m.Language)); err != nil && - !e.Is(err, gdb.ErrNoAffect) { - errs <- everr.Join(e.New("Failed to add translated thread to database"), err) - return - } - - tg <- gdb.NewChannel(dtth.GuildID, dtth.ID, m.Language) - }(m, tg, errs) - - } - } - - wg.Wait() - - everrs := make([]error, 0, len(errs)) - for err := range errs { - everrs = append(everrs, err) - } - if len(errs) > 0 { - return everr.Join(everrs...) - } - - var threadGroup gdb.ChannelGroup - for t := range tg { - threadGroup = append(threadGroup, t) - } - - if err := h.db.ChannelGroupInsert(threadGroup); err != nil { - return everr.Join(e.New("Failed to add group of threads to database"), err) - } - - thMsgs, err := s.ChannelMessages(th.ID, 10, "", "", "") - if err != nil { - return everr.Join(e.New("Failed to get thread messages"), err) - } - - for _, m := range thMsgs { - if startMsg != nil && m.ID == startMsg.ID { - continue - } - if m.Content != "" { - m.GuildID = th.GuildID - NewMessageCreate(h.db, h.translator).sendMessage(log, s, m) - } - } - - return nil -} - -type ThreadCreate struct { - db gconf.DB - translator translator.Translator - session *dgo.Session - thread *dgo.Channel - originLang translator.Language -} - -func NewThreadCreate(db gconf.DB, t translator.Translator) ThreadCreate { - return ThreadCreate{db, t, nil, nil, translator.EN} -} - -func (h ThreadCreate) Serve(s *dgo.Session, ev *dgo.ThreadCreate) errors.EventErr { - log := gconf.GetLogger(ev.GuildID, s, h.db) - everr := errors.NewThreadCreateErr(s, ev, log) - - parentCh, err := h.db.Channel(ev.GuildID, ev.ParentID) - if e.Is(err, gdb.ErrNotFound) { - log.Debug("Parent channel of thread not in database, ignoring", - slog.String("ThreadID", ev.ID), - slog.String("ParentID", ev.ParentID)) - return nil - } - - ms, err := s.ChannelMessages(ev.ID, 10, "", "", "") - if err != nil { - return everr.Join(e.New("Failed to get messages of thread"), err) - } else if len(ms) == 0 || (len(ms) == 1 && ms[0].Type == dgo.MessageTypeThreadStarterMessage) { - log.Debug("No messages found in thread, probably created by bot, ignoring", - slog.String("ThreadID", ev.ID), - slog.String("ParentID", ev.ParentID)) - return nil - } - - // INFO: Threads have the same ID as their starter messages - starterMsg, err := h.db.Message(parentCh.GuildID, parentCh.ID, ev.ID) - if e.Is(err, gdb.ErrNotFound) { - starterMsg = gdb.NewMessage(parentCh.GuildID, ev.ID, ev.ID, parentCh.Language) - err = h.db.MessageInsert(starterMsg) - if err != nil { - return everr.Join(e.New("Failed to add starter message to database"), err) - } - } - - thread, err := s.Channel(starterMsg.ID) - if err != nil { - return everr.Join(e.New("Failed to get thread from discord"), err) - } else if !thread.IsThread() { - return everr.Join(e.New("Failed to get thread from discord, thread is not a thread somehow"), err) - } - - parentChannelGroup, err := h.db.ChannelGroup(parentCh.GuildID, parentCh.ID) - if e.Is(err, gdb.ErrNotFound) { - log.Debug("Parent channel not in a group, ignoring", - slog.String("ThreadID", ev.ID), - slog.String("ParentID", ev.ParentID)) - return nil - } else if err != nil { - return everr.Join(e.New("Failed to get parent channel group")) - } - - var wg sync.WaitGroup - tg := make(chan gdb.Channel) - errs := make(chan error) - - h.session = s - h.originLang = parentCh.Language - h.thread = thread - - for _, pc := range parentChannelGroup { - if pc.ID == ev.ParentID { - continue - } - - wg.Add(1) - go func(tg chan<- gdb.Channel, errs chan<- error) { - defer wg.Done() - t, err := h.startTranslatedThread(pc, starterMsg) - tg <- t - if err != nil { - errs <- err - } - log.Debug("FINISHED") - }(tg, errs) - } - - wg.Wait() - - everrs := make([]error, 0, len(errs)) - for err := range errs { - log.Debug("ERR") - everrs = append(everrs, err) - } - if len(errs) > 0 { - log.Debug("ERR RETURN") - return everr.Join(everrs...) - } - - log.Debug("FUNCTION 1") - - if err := h.db.ChannelInsert(gdb.NewChannel(thread.GuildID, thread.ID, parentCh.Language)); err != nil { - return everr.Join(e.New("Failed to add thread channel to database"), err) - } - - threadGroup := make(gdb.ChannelGroup, 0, len(tg)) - for t := range tg { - threadGroup = append(threadGroup, t) - } - - log.Debug("FUNCTION 2") - - if err := h.db.ChannelGroupInsert(threadGroup); err != nil { - return everr.Join(e.New("Failed to add group of thread to database"), err) - } - - thMsgs, err := s.ChannelMessages(thread.ID, 10, "", "", "") - if err != nil { - return everr.Join(e.New("Failed to get thread messages"), err) - } - - log.Debug("FUNCTION 3") - - for _, m := range thMsgs { - m.GuildID = thread.GuildID - err := NewMessageCreate(h.db, h.translator).sendMessage(log, s, m) - if err != nil { - return everr.Join(e.New("Failed to translate thread messages"), err) - } - } - - return nil -} - -func (h ThreadCreate) startTranslatedThread( - pc gdb.Channel, - sm gdb.Message, -) (gdb.Channel, error) { - if sm.OriginChannelID != nil && *sm.OriginChannelID == pc.ID { - m, err := h.db.Message(sm.GuildID, *sm.OriginChannelID, *sm.OriginID) - if err != nil { - return gdb.Channel{}, e.Join( - e.New("Failed to get origin message of starter message"), - err, - ) - } - return h.startTranslatedMessageThread(m) - } - - m, err := h.db.MessageWithOriginByLang(sm.GuildID, sm.ChannelID, sm.ID, pc.Language) - if e.Is(err, gdb.ErrNotFound) { - } else if err != nil { - return gdb.Channel{}, e.Join( - e.New("Failed to get translated message of starter message"), - err, - ) - } - - return h.startTranslatedMessageThread(m) -} - -func (h ThreadCreate) startTranslatedMessageThread( - m gdb.Message, -) (gdb.Channel, error) { - name, err := h.translator.Translate(h.originLang, m.Language, h.thread.Name) - if err != nil { - return gdb.Channel{}, e.Join(e.New("Failed to translate thread name"), err) - } - - th, err := h.session.MessageThreadStartComplex(m.ChannelID, m.ID, &dgo.ThreadStart{ - Name: name, - AutoArchiveDuration: h.thread.ThreadMetadata.AutoArchiveDuration, - Type: h.thread.Type, - Invitable: h.thread.ThreadMetadata.Invitable, - RateLimitPerUser: h.thread.RateLimitPerUser, - AppliedTags: h.thread.AppliedTags, - }) - if err != nil { - return gdb.Channel{}, e.Join(e.New("Failed to create thread"), err) - } - - c := gdb.NewChannel(th.GuildID, th.ID, m.Language) - if err := h.db.ChannelInsert(c); err != nil { - return c, e.Join(e.New("Failed to insert thread on database"), err) - } - - return c, nil -} diff --git a/botv1/gconf/config.go b/botv1/gconf/config.go deleted file mode 100644 index a1c45e5..0000000 --- a/botv1/gconf/config.go +++ /dev/null @@ -1,63 +0,0 @@ -package gconf - -import ( - "log/slog" - - gdb "forge.capytal.company/capytal/dislate/guilddb" - - dgo "github.com/bwmarrin/discordgo" -) - -type Config struct { - Logger *slog.Logger -} - -type ConfigString struct { - LoggingChannel *string `json:"logging_channel"` - LoggingLevel *slog.Level `json:"logging_level"` -} - -type ( - Guild gdb.Guild[ConfigString] - DB gdb.GuildDB[ConfigString] -) - -func (g Guild) GetConfig(s *dgo.Session) (*Config, error) { - var l *slog.Logger - var err error - - if g.Config.LoggingChannel != nil { - c, err := s.Channel(*g.Config.LoggingChannel) - if err != nil { - return nil, err - } - - var lv slog.Level - if g.Config.LoggingLevel != nil { - lv = *g.Config.LoggingLevel - } else { - lv = slog.LevelInfo - } - l = slog.New(NewGuildHandler(s, c, &slog.HandlerOptions{ - Level: lv, - })) - } else { - l = slog.New(disabledHandler{}) - } - - return &Config{l}, err -} - -func GetLogger(guildID string, s *dgo.Session, db DB) *slog.Logger { - g, err := db.Guild(guildID) - if err != nil { - return slog.New(disabledHandler{}) - } - - c, err := Guild(g).GetConfig(s) - if err != nil { - return slog.New(disabledHandler{}) - } - - return c.Logger -} diff --git a/botv1/gconf/logger.go b/botv1/gconf/logger.go deleted file mode 100644 index 9f99196..0000000 --- a/botv1/gconf/logger.go +++ /dev/null @@ -1,46 +0,0 @@ -package gconf - -import ( - "context" - "log/slog" - - dgo "github.com/bwmarrin/discordgo" -) - -type guildHandler struct { - *slog.TextHandler -} - -func NewGuildHandler(s *dgo.Session, c *dgo.Channel, opts *slog.HandlerOptions) guildHandler { - w := NewChannelWriter(s, c) - h := slog.NewTextHandler(w, opts) - return guildHandler{h} -} - -type disabledHandler struct { - *slog.TextHandler -} - -func (_ disabledHandler) Enabled(_ context.Context, _ slog.Level) bool { - return false -} - -type channelWriter struct { - session *dgo.Session - channel *dgo.Channel -} - -func NewChannelWriter(s *dgo.Session, c *dgo.Channel) channelWriter { - w := channelWriter{s, c} - - return w -} - -func (w channelWriter) Write(p []byte) (int, error) { - m, err := w.session.ChannelMessageSend(w.channel.ID, string(p)) - if err != nil { - return 0, err - } - - return len(m.Content), nil -} diff --git a/guilddb/guilddb.go b/guilddb/guilddb.go deleted file mode 100644 index e630d3e..0000000 --- a/guilddb/guilddb.go +++ /dev/null @@ -1,163 +0,0 @@ -package guilddb - -import ( - "errors" - - "forge.capytal.company/capytal/dislate/translator" -) - -type Guild[C any] struct { - ID string - Config C -} - -func NewGuild[C any](ID string, config C) Guild[C] { - return Guild[C]{ID, config} -} - -type Channel struct { - GuildID string - ID string - Language translator.Language -} - -func NewChannel(GuildID, ID string, lang translator.Language) Channel { - return Channel{GuildID, ID, lang} -} - -type ChannelGroup []Channel - -type Message struct { - GuildID string - ChannelID string - ID string - Language translator.Language - OriginChannelID *string - OriginID *string -} - -func NewMessage(GuildID, ChannelID, ID string, lang translator.Language) Message { - return Message{GuildID, ChannelID, ID, lang, nil, nil} -} - -func NewTranslatedMessage( - GuildID, ChannelID, ID string, - lang translator.Language, - OriginChannelID, OriginID string, -) Message { - return Message{GuildID, ChannelID, ID, lang, &OriginChannelID, &OriginID} -} - -type GuildDB[C any] interface { - // Selects and returns a Message from the database, based on the - // key pair of Channel's ID and Message's ID. - // - // Will return ErrNotFound if no message is found or ErrInternal. - Message(guildID, channelID, ID string) (Message, error) - // Returns a slice of Messages with the provided Message.OriginChannelID and Message.OriginID. - // - // Will return ErrNotFound if no message is found (slice's length == 0) or ErrInternal. - MessagesWithOrigin(guildID, originChannelID, originID string) ([]Message, error) - // Returns a Messages with the provided Message.OriginChannelID, Message.OriginID - // and Message.Language. - // - // Will return ErrNotFound if no message is found or ErrInternal. - MessageWithOriginByLang( - guildID, originChannelId, originId string, - language translator.Language, - ) (Message, error) - // Inserts a new Message object in the database. - // - // Message.ChannelID and Message.ID must be a unique pair and not already - // in the database. Implementations of this function may require Message.ChannelID - // to be an already stored Channel object, in the case that it isn't stored, - // ErrPreconditionFailed may be returned. - // - // Message.OriginID and Message.OriginChannelID should not be nil if the message - // is a translated one. - // - // Will return ErrNoAffect if the object already exists or ErrInternal. - MessageInsert(m Message) error - // Updates the Message object in the database. Message.ID and Message.ChannelID - // are used to find the correct message. - // - // Will return ErrNoAffect if no object was updated or ErrInternal. - MessageUpdate(m Message) error - // Deletes the Message object in the database. Message.ID and Message.ChannelID - // are used to find the correct message. - // - // Will return ErrNoAffect if no object was deleted or ErrInternal. - MessageDelete(m Message) error - // Deletes all messages in a Channel in the database. Channel.ID is used to find - // the correct messages. - // - // Will return ErrNoAffect if no object was deleted or ErrInternal. - MessageDeleteFromChannel(c Channel) error - // Selects and returns a Channel from the database, based on the - // ID provided. - // - // Will return ErrNotFound if no channel is found or ErrInternal. - Channel(guildID, ID string) (Channel, error) - // Inserts a new Channel object in the database. - // - // Channel.ID must be unique and not already in the database. - // - // Will return ErrNoAffect if the object already exists or ErrInternal. - ChannelInsert(c Channel) error - // Updates the Channel object in the database. Channel.ID is used to find the - // correct Channel. - // - // Will return ErrNoAffect if no object was updated or ErrInternal. - ChannelUpdate(c Channel) error - // Deletes the Channel object in the database. Channel.ID is used to find the - // correct Channel. - // - // Will return ErrNoAffect if no object was deleted or ErrInternal. - ChannelDelete(c Channel) error - // Selects and returns a ChannelGroup from the database. Finds a ChannelGroup - // that has a Channel if the provided ID. - // - // Channels cannot be in two ChannelGroup at the same time. - // - // Will return ErrNotFound if no channel is found or ErrInternal. - ChannelGroup(guildID, ID string) (ChannelGroup, error) - // Inserts a new ChannelGroup object in the database. ChannelGroup must be unique - // and not have Channels that are already in other groups. - // - // Will return ErrNoAffect if the object already exists or ErrInternal. - ChannelGroupInsert(g ChannelGroup) error - // Updates the ChannelGroup object in the database. - // - // Will return ErrNoAffect if no object was updated or ErrInternal. - ChannelGroupUpdate(g ChannelGroup) error - // Deletes the ChannelGroup object in the database. - // - // Will return ErrNoAffect if no object was deleted or ErrInternal. - ChannelGroupDelete(g ChannelGroup) error - // Selects and returns a Guild from the database. - // - // Will return ErrNotFound if no Guild is found or ErrInternal. - Guild(ID string) (Guild[C], error) - // Inserts a new Guild object in the database. Guild.ID must be unique and - // not already in the database. - // - // Will return ErrNoAffect if the object already exists or ErrInternal. - GuildInsert(g Guild[C]) error - // Delete a Guild from the database. Guild.ID is used to find the object. - // - // Will return ErrNoAffect if no object was deleted or ErrInternal. - GuildDelete(g Guild[C]) error - // Updates the Guild object in the database. - // - // Will return ErrNoAffect if no object was updated or ErrInternal. - GuildUpdate(g Guild[C]) error -} - -var ( - ErrNoAffect = errors.New("Not able to affect anything in the database") - ErrNotFound = errors.New("Object not found in the database") - ErrPreconditionFailed = errors.New("Precondition failed") - ErrInvalidObject = errors.New("Invalid object") - ErrInternal = errors.New("Internal error while trying to use database") - ErrConfigParsing = errors.New("Error while parsing Guild's config") -) diff --git a/guilddb/sqlite.go b/guilddb/sqlite.go deleted file mode 100644 index b7ec556..0000000 --- a/guilddb/sqlite.go +++ /dev/null @@ -1,564 +0,0 @@ -package guilddb - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "slices" - "strings" - - "forge.capytal.company/capytal/dislate/translator" - - _ "github.com/tursodatabase/go-libsql" -) - -type SQLiteDB[C any] struct { - sql *sql.DB -} - -func NewSQLiteDB[C any](file string) (*SQLiteDB[C], error) { - db, err := sql.Open("libsql", file) - if err != nil { - return &SQLiteDB[C]{}, err - } - db.SetMaxOpenConns(1) - return &SQLiteDB[C]{db}, nil -} - -func (db *SQLiteDB[C]) Close() error { - return db.sql.Close() -} - -func (db *SQLiteDB[C]) Prepare() error { - if _, err := db.sql.Exec(` - CREATE TABLE IF NOT EXISTS guilds ( - ID text NOT NULL, - Config text NOT NULL, - PRIMARY KEY(ID) - ); - `); err != nil { - return errors.Join(ErrInternal, err) - } - - if _, err := db.sql.Exec(` - CREATE TABLE IF NOT EXISTS channels ( - GuildID text NOT NULL, - ID text NOT NULL, - Language text NOT NULL, - PRIMARY KEY(ID, GuildID), - FOREIGN KEY(GuildID) REFERENCES guilds(ID) - ); - `); err != nil { - return errors.Join(ErrInternal, err) - } - - if _, err := db.sql.Exec(` - CREATE TABLE IF NOT EXISTS channelGroups ( - GuildID text NOT NULL, - Channels text NOT NULL, - PRIMARY KEY(Channels, GuildID), - FOREIGN KEY(GuildID) REFERENCES guilds(ID) - ); - `); err != nil { - return errors.Join(ErrInternal, err) - } - - if _, err := db.sql.Exec(` - CREATE TABLE IF NOT EXISTS messages ( - GuildID text NOT NULL, - ChannelID text NOT NULL, - ID text NOT NULL, - Language text NOT NULL, - OriginChannelID text, - OriginID text, - PRIMARY KEY(ID, ChannelID, GuildID), - FOREIGN KEY(GuildID, ChannelID) REFERENCES channels(GuildID, ID), - FOREIGN KEY(GuildID, OriginChannelID, OriginID) REFERENCES messages(GuildID, ChannelID, ID) - ); - `); err != nil { - return errors.Join(ErrInternal, err) - } - - return nil -} - -func (db *SQLiteDB[C]) Message(guildID, channelID, messageID string) (Message, error) { - return db.selectMessage(` - WHERE "GuildID" = $1 AND "ChannelID" = $2 AND "ID" = $3 - `, guildID, channelID, messageID) -} - -func (db *SQLiteDB[C]) MessagesWithOrigin( - guildID, originChannelID, originID string, -) ([]Message, error) { - return db.selectMessages(` - WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3 - `, guildID, originChannelID, originID) -} - -func (db *SQLiteDB[C]) MessageWithOriginByLang( - guildID, originChannelID, originID string, - language translator.Language, -) (Message, error) { - return db.selectMessage(` - WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3 AND "Language" = $4 - `, guildID, originChannelID, originID, language) -} - -func (db *SQLiteDB[C]) MessageInsert(m Message) error { - _, err := db.Channel(m.GuildID, m.ChannelID) - if errors.Is(err, ErrNotFound) { - return errors.Join( - ErrPreconditionFailed, - fmt.Errorf("Channel %s doesn't exists in the database", m.ChannelID), - ) - } else if err != nil { - return errors.Join( - ErrInternal, - errors.New("Failed to check if Channel exists in the database"), - err, - ) - } - - r, err := db.sql.Exec(` - INSERT OR IGNORE INTO messages (GuildID, ChannelID, ID, Language, OriginChannelID, OriginID) - VALUES ($1, $2, $3, $4, $5, $6) - `, m.GuildID, m.ChannelID, m.ID, m.Language, m.OriginChannelID, m.OriginID) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) MessageUpdate(m Message) error { - r, err := db.sql.Exec(` - UPDATE messages - SET Language = $1, OriginChannelID = $2, OriginID = $3 - WHERE "GuildID" = $4 AND "ChannelID" = $5 AND "ID" = $6 - `, m.Language, - m.OriginChannelID, - m.OriginID, - m.GuildID, - m.ChannelID, - m.ID, - ) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) MessageDelete(m Message) error { - _, err := db.sql.Exec(` - DELETE FROM messages - WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3 - `, m.GuildID, m.ChannelID, m.ID) - if err != nil && !errors.Is(err, ErrNoAffect) { - return errors.Join(ErrInternal, err) - } - - r, err := db.sql.Exec(` - DELETE FROM messages - WHERE "GuildID" = $1 AND "ChannelID" = $2 AND "ID" = $3 - `, m.GuildID, m.ChannelID, m.ID) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) MessageDeleteFromChannel(c Channel) error { - r, err := db.sql.Exec(` - DELETE FROM messages - WHERE "GuildID" = $1 AND "ChannelID" = $2 - `, c.GuildID, c.ID) - if err != nil && !errors.Is(err, ErrNoAffect) { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) selectMessage(query string, args ...any) (Message, error) { - var m Message - err := db.sql.QueryRow(fmt.Sprintf(` - SELECT GuildID, ChannelID, ID, Language, OriginChannelID, OriginID FROM messages - %s - `, query), args...). - Scan(&m.GuildID, &m.ChannelID, &m.ID, &m.Language, &m.OriginChannelID, &m.OriginID) - - if errors.Is(err, sql.ErrNoRows) { - return m, errors.Join(ErrNotFound, err) - } else if err != nil { - return m, errors.Join(ErrInternal, err) - } - - return m, nil -} - -func (db *SQLiteDB[C]) selectMessages(query string, args ...any) ([]Message, error) { - r, err := db.sql.Query(fmt.Sprintf(` - SELECT GuildID, ChannelID, ID, Language, OriginChannelID, OriginID FROM messages - %s - `, query), args...) - defer r.Close() - - if err != nil { - return []Message{}, errors.Join(ErrInternal, err) - } - - var ms []Message - for r.Next() { - var m Message - - err = r.Scan(&m.GuildID, &m.ChannelID, &m.ID, &m.Language, &m.OriginChannelID, &m.OriginID) - if err != nil { - return ms, errors.Join( - ErrInternal, - fmt.Errorf("Query: %s\nArguments: %v", query, args), - err, - ) - } - - ms = append(ms, m) - } - - if len(ms) == 0 { - return ms, errors.Join( - ErrNotFound, - fmt.Errorf("Query: %s\nArguments: %v", query, args), - ) - } - return ms, err -} - -func (db *SQLiteDB[C]) Channel(guildID, ID string) (Channel, error) { - return db.selectChannel(` - WHERE "GuildID" = $1 AND "ID" = $2 - `, guildID, ID) -} - -func (db *SQLiteDB[C]) ChannelInsert(c Channel) error { - r, err := db.sql.Exec(` - INSERT OR IGNORE INTO channels (GuildID, ID, Language) - VALUES ($1, $2, $3) - `, c.GuildID, c.ID, c.Language) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) ChannelUpdate(c Channel) error { - r, err := db.sql.Exec(` - UPDATE channels - SET Language = $1 - WHERE "GuildID" = $2 AND "ID" = $3 - `, c.Language, c.GuildID, c.ID) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) ChannelDelete(c Channel) error { - r, err := db.sql.Exec(` - DELETE FROM channels - WHERE "GuildID" = $1 AND "ID" = $2 - `, c.ID, c.ID) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) ChannelGroup(guildID, channelID string) (ChannelGroup, error) { - var j string - err := db.sql.QueryRow(fmt.Sprintf(` - SELECT Channels FROM channelGroups, json_each(Channels) - WHERE "GuildID" = $1 AND json_each.value='%s'; - `, channelID), guildID).Scan(&j) - - if errors.Is(err, sql.ErrNoRows) { - return ChannelGroup{}, errors.Join(ErrNotFound, err) - } else if err != nil { - return ChannelGroup{}, errors.Join(ErrInternal, err) - } - - var ids []string - err = json.Unmarshal([]byte(j), &ids) - if err != nil { - return ChannelGroup{}, errors.Join(ErrInternal, err) - } - for i, v := range ids { - ids[i] = fmt.Sprintf("\"ID\" = %s", v) - } - - cs, err := db.selectChannels(fmt.Sprintf(` - WHERE %s AND "GuildID" = $1 - `, strings.Join(ids, " OR ")), guildID) - - if errors.Is(err, ErrNotFound) || len(cs) != len(ids) { - return ChannelGroup{}, errors.Join( - ErrPreconditionFailed, - fmt.Errorf( - "ChannelGroup has Channels that doesn't exist in the database, group: %s", - ids, - ), - err, - ) - } else if err != nil { - return ChannelGroup{}, errors.Join(ErrInternal, err) - } - - return cs, nil -} - -func (db *SQLiteDB[C]) ChannelGroupInsert(g ChannelGroup) error { - if len(g) == 0 { - return ErrNoAffect - } - - var ids []string - for _, c := range g { - ids = append(ids, c.ID) - } - slices.Sort(ids) - - j, err := json.Marshal(ids) - if err != nil { - return errors.Join(ErrInternal, err) - } - - r, err := db.sql.Exec(fmt.Sprintf(` - INSERT OR IGNORE INTO channelGroups (GuildID, Channels) - VALUES ($1, json('%s')) - `, string(j)), g[0].GuildID) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) ChannelGroupUpdate(g ChannelGroup) error { - if len(g) != 0 { - return nil - } - - var ids, idsq []string - for _, c := range g { - ids = append(ids, c.ID) - idsq = append(idsq, "json_each.value='"+c.ID+"'") - } - slices.Sort(ids) - - r, err := db.sql.Exec( - fmt.Sprintf(` - UPDATE channelGroups, json_each(Channels) - SET Channels = $1 - WHERE %s AND "GuildID" = $2 - `, strings.Join(idsq, " OR ")), - strings.Join(ids, ","), - g[0].GuildID, - ) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) ChannelGroupDelete(g ChannelGroup) error { - if len(g) != 0 { - return nil - } - - var ids, idsq []string - for _, c := range g { - ids = append(ids, c.ID) - idsq = append(idsq, "json_each.value='"+c.ID+"'") - } - slices.Sort(ids) - - r, err := db.sql.Exec( - fmt.Sprintf(` - DELETE FROM channelGroups, json_each(Channels) - WHERE %s AND "GuildID" = $2 - `, strings.Join(idsq, " OR ")), - g[0].GuildID, - ) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) selectChannel(query string, args ...any) (Channel, error) { - var c Channel - err := db.sql.QueryRow(fmt.Sprintf(` - SELECT GuildID, ID, Language FROM channels - %s - `, query), args...).Scan(&c.GuildID, &c.ID, &c.Language) - - if errors.Is(err, sql.ErrNoRows) { - return c, errors.Join(ErrNotFound, err) - } else if err != nil { - return c, errors.Join(ErrInternal, err) - } - - return c, nil -} - -func (db *SQLiteDB[C]) selectChannels(query string, args ...any) ([]Channel, error) { - r, err := db.sql.Query(fmt.Sprintf(` - SELECT GuildID, ID, Language FROM channels - %s - `, query), args...) - defer r.Close() - - if err != nil { - return []Channel{}, errors.Join(ErrInternal, err) - } - - var cs []Channel - for r.Next() { - var c Channel - - err = r.Scan(&c.GuildID, &c.ID, &c.Language) - if err != nil { - return cs, errors.Join( - ErrInternal, - fmt.Errorf("Query: %s\nArguments: %v", query, args), - err, - ) - } - - cs = append(cs, c) - } - - if len(cs) == 0 { - return cs, errors.Join( - ErrNotFound, - fmt.Errorf("Query: %s\nArguments: %v", query, args), - ) - } - return cs, err -} - -func (db *SQLiteDB[C]) Guild(ID string) (Guild[C], error) { - var g struct { - ID string - Config string - } - - if err := db.sql.QueryRow(` - SELECT "ID", "Config" FROM guilds - WHERE "ID" = $1 - `, ID).Scan(&g.ID, &g.Config); errors.Is(err, sql.ErrNoRows) { - return Guild[C]{}, errors.Join(ErrNotFound, err) - } else if err != nil { - return Guild[C]{}, errors.Join(ErrInternal, err) - } - - var c C - err := json.Unmarshal([]byte(g.Config), &c) - if err != nil { - return Guild[C]{}, errors.Join(ErrConfigParsing, err) - } - - return Guild[C]{g.ID, c}, nil -} - -func (db *SQLiteDB[C]) GuildInsert(g Guild[C]) error { - j, err := json.Marshal(g.Config) - if err != nil { - return errors.Join(ErrConfigParsing, err) - } - - r, err := db.sql.Exec(` - INSERT OR IGNORE INTO guilds (ID, Config) - VALUES ($1, $2) - `, g.ID, string(j)) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) GuildUpdate(g Guild[C]) error { - j, err := json.Marshal(g.Config) - if err != nil { - return errors.Join(ErrConfigParsing, err) - } - - r, err := db.sql.Exec(fmt.Sprintf(` - UPDATE guilds - SET "Config" = '%s' - WHERE "ID" = '%s' - `, string(j), g.ID)) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -} - -func (db *SQLiteDB[C]) GuildDelete(g Guild[C]) error { - r, err := db.sql.Exec(` - DELETE FROM guilds - WHERE "ID" = $1 - `, g.ID) - - if err != nil { - return errors.Join(ErrInternal, err) - } else if rows, _ := r.RowsAffected(); rows == 0 { - return ErrNoAffect - } - - return nil -}