From daa63f5d64d2e2a41bc1792778ea645d4a0257ee Mon Sep 17 00:00:00 2001 From: "Gustavo L de Mello (Guz)" Date: Mon, 4 Nov 2024 09:02:57 -0300 Subject: [PATCH] refactor!: restructure all project packages to simplify it --- {internals/discord/bot => bot}/bot.go | 5 +- {internals/discord/bot => bot}/commands.go | 25 +- .../discord/bot => bot}/commands/channels.go | 22 +- .../discord/bot => bot}/commands/commands.go | 0 .../discord/bot => bot}/commands/config.go | 2 +- {internals/discord/bot => bot}/events.go | 4 +- .../bot => bot}/events/errors/default.go | 0 .../bot => bot}/events/errors/errors.go | 0 .../bot => bot}/events/errors/guild.go | 0 .../bot => bot}/events/errors/messages.go | 0 bot/events/errors/threads.go | 24 + .../discord/bot => bot}/events/events.go | 2 +- .../discord/bot => bot}/events/guild.go | 7 +- .../discord/bot => bot}/events/messages.go | 26 +- bot/events/threads.go | 497 ++++++++++++++++++ .../discord/bot => bot}/gconf/config.go | 2 +- .../discord/bot => bot}/gconf/logger.go | 0 go.mod | 2 +- {internals/guilddb => guilddb}/guilddb.go | 14 +- {internals/guilddb => guilddb}/sqlite.go | 4 +- internals/discord/bot/errors/errors.go | 127 ----- internals/discord/bot/events/threads.go | 167 ------ internals/translator/translator.go | 24 - main.go | 8 +- .../translator/lang => translator}/lang.go | 2 +- translator/translator.go | 22 + 26 files changed, 612 insertions(+), 374 deletions(-) rename {internals/discord/bot => bot}/bot.go (90%) rename {internals/discord/bot => bot}/commands.go (88%) rename {internals/discord/bot => bot}/commands/channels.go (95%) rename {internals/discord/bot => bot}/commands/commands.go (100%) rename {internals/discord/bot => bot}/commands/config.go (98%) rename {internals/discord/bot => bot}/events.go (85%) rename {internals/discord/bot => bot}/events/errors/default.go (100%) rename {internals/discord/bot => bot}/events/errors/errors.go (100%) rename {internals/discord/bot => bot}/events/errors/guild.go (100%) rename {internals/discord/bot => bot}/events/errors/messages.go (100%) create mode 100644 bot/events/errors/threads.go rename {internals/discord/bot => bot}/events/events.go (71%) rename {internals/discord/bot => bot}/events/guild.go (89%) rename {internals/discord/bot => bot}/events/messages.go (95%) create mode 100644 bot/events/threads.go rename {internals/discord/bot => bot}/gconf/config.go (95%) rename {internals/discord/bot => bot}/gconf/logger.go (100%) rename {internals/guilddb => guilddb}/guilddb.go (94%) rename {internals/guilddb => guilddb}/sqlite.go (99%) delete mode 100644 internals/discord/bot/errors/errors.go delete mode 100644 internals/discord/bot/events/threads.go delete mode 100644 internals/translator/translator.go rename {internals/translator/lang => translator}/lang.go (79%) create mode 100644 translator/translator.go diff --git a/internals/discord/bot/bot.go b/bot/bot.go similarity index 90% rename from internals/discord/bot/bot.go rename to bot/bot.go index 42c3aa6..9f80dc5 100644 --- a/internals/discord/bot/bot.go +++ b/bot/bot.go @@ -3,8 +3,9 @@ package bot import ( "log/slog" - "dislate/internals/discord/bot/gconf" - "dislate/internals/translator" + "forge.capytal.company/capytal/dislate/translator" + + "forge.capytal.company/capytal/dislate/bot/gconf" dgo "github.com/bwmarrin/discordgo" ) diff --git a/internals/discord/bot/commands.go b/bot/commands.go similarity index 88% rename from internals/discord/bot/commands.go rename to bot/commands.go index 0e5960e..f3705f3 100644 --- a/internals/discord/bot/commands.go +++ b/bot/commands.go @@ -7,7 +7,7 @@ import ( "log/slog" "slices" - "dislate/internals/discord/bot/commands" + "forge.capytal.company/capytal/dislate/bot/commands" dgo "github.com/bwmarrin/discordgo" ) @@ -88,9 +88,12 @@ func (b *Bot) registerCommands() error { ) opts := ic.Interaction.ApplicationCommandData().Options - isSub := slices.IndexFunc(opts, func(o *dgo.ApplicationCommandInteractionDataOption) bool { - return o.Type == dgo.ApplicationCommandOptionSubCommand - }) + isSub := slices.IndexFunc( + opts, + func(o *dgo.ApplicationCommandInteractionDataOption) bool { + return o.Type == dgo.ApplicationCommandOptionSubCommand + }, + ) if isSub != -1 { sc := opts[isSub] @@ -99,8 +102,11 @@ func (b *Bot) registerCommands() error { _ = 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, + Content: fmt.Sprintf( + "Error while trying to handle sub command: %s", + err.Error(), + ), + Flags: dgo.MessageFlagsEphemeral, }, }) b.logger.Error("Failed to handle sub command", @@ -117,8 +123,11 @@ func (b *Bot) registerCommands() error { _ = 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, + Content: fmt.Sprintf( + "Error while trying to handle command: %s", + err.Error(), + ), + Flags: dgo.MessageFlagsEphemeral, }, }) b.logger.Error("Failed to handle command", diff --git a/internals/discord/bot/commands/channels.go b/bot/commands/channels.go similarity index 95% rename from internals/discord/bot/commands/channels.go rename to bot/commands/channels.go index cee2cab..ed542c8 100644 --- a/internals/discord/bot/commands/channels.go +++ b/bot/commands/channels.go @@ -5,11 +5,11 @@ import ( "fmt" "strings" - "dislate/internals/discord/bot/gconf" - "dislate/internals/guilddb" - "dislate/internals/translator/lang" + "forge.capytal.company/capytal/dislate/bot/gconf" + "forge.capytal.company/capytal/dislate/guilddb" + "forge.capytal.company/capytal/dislate/translator" - gdb "dislate/internals/guilddb" + gdb "forge.capytal.company/capytal/dislate/guilddb" dgo "github.com/bwmarrin/discordgo" ) @@ -258,8 +258,8 @@ func (c channelsSetLang) Info() *dgo.ApplicationCommand { Name: "language", Description: "The new language", Choices: []*dgo.ApplicationCommandOptionChoice{ - {Name: "English (EN)", Value: lang.EN}, - {Name: "Portuguese (PT)", Value: lang.PT}, + {Name: "English (EN)", Value: translator.EN}, + {Name: "Portuguese (PT)", Value: translator.PT}, }, }, { Type: dgo.ApplicationCommandOptionChannel, @@ -280,14 +280,14 @@ func (c channelsSetLang) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error var err error var dch *dgo.Channel - var l lang.Language + var l translator.Language if c, ok := opts["language"]; ok { switch c.StringValue() { - case string(lang.PT): - l = lang.PT + case string(translator.PT): + l = translator.PT default: - l = lang.EN + l = translator.EN } } else { return errors.New("language is a required option") @@ -342,7 +342,7 @@ func (c channelsSetLang) Subcommands() []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, lang.EN)); err != nil { + if err := db.ChannelInsert(gdb.NewChannel(guildID, channelID, translator.EN)); err != nil { return gdb.Channel{}, err } ch, err = db.Channel(guildID, channelID) diff --git a/internals/discord/bot/commands/commands.go b/bot/commands/commands.go similarity index 100% rename from internals/discord/bot/commands/commands.go rename to bot/commands/commands.go diff --git a/internals/discord/bot/commands/config.go b/bot/commands/config.go similarity index 98% rename from internals/discord/bot/commands/config.go rename to bot/commands/config.go index 6343f53..83616ce 100644 --- a/internals/discord/bot/commands/config.go +++ b/bot/commands/config.go @@ -5,7 +5,7 @@ import ( "fmt" "log/slog" - "dislate/internals/discord/bot/gconf" + "forge.capytal.company/capytal/dislate/bot/gconf" dgo "github.com/bwmarrin/discordgo" ) diff --git a/internals/discord/bot/events.go b/bot/events.go similarity index 85% rename from internals/discord/bot/events.go rename to bot/events.go index 18d7a4a..bb193de 100644 --- a/internals/discord/bot/events.go +++ b/bot/events.go @@ -1,7 +1,7 @@ package bot import ( - "dislate/internals/discord/bot/events" + "forge.capytal.company/capytal/dislate/bot/events" dgo "github.com/bwmarrin/discordgo" ) @@ -19,12 +19,12 @@ func w[E any](h events.EventHandler[E]) interface{} { func (b *Bot) registerEventHandlers() { ehs := []any{ - events.NewThreadCreate(b.db, b.translator).Serve, 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/internals/discord/bot/events/errors/default.go b/bot/events/errors/default.go similarity index 100% rename from internals/discord/bot/events/errors/default.go rename to bot/events/errors/default.go diff --git a/internals/discord/bot/events/errors/errors.go b/bot/events/errors/errors.go similarity index 100% rename from internals/discord/bot/events/errors/errors.go rename to bot/events/errors/errors.go diff --git a/internals/discord/bot/events/errors/guild.go b/bot/events/errors/guild.go similarity index 100% rename from internals/discord/bot/events/errors/guild.go rename to bot/events/errors/guild.go diff --git a/internals/discord/bot/events/errors/messages.go b/bot/events/errors/messages.go similarity index 100% rename from internals/discord/bot/events/errors/messages.go rename to bot/events/errors/messages.go diff --git a/bot/events/errors/threads.go b/bot/events/errors/threads.go new file mode 100644 index 0000000..150a769 --- /dev/null +++ b/bot/events/errors/threads.go @@ -0,0 +1,24 @@ +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/internals/discord/bot/events/events.go b/bot/events/events.go similarity index 71% rename from internals/discord/bot/events/events.go rename to bot/events/events.go index 5e8e6b8..79bdb76 100644 --- a/internals/discord/bot/events/events.go +++ b/bot/events/events.go @@ -1,7 +1,7 @@ package events import ( - "dislate/internals/discord/bot/events/errors" + "forge.capytal.company/capytal/dislate/bot/events/errors" dgo "github.com/bwmarrin/discordgo" ) diff --git a/internals/discord/bot/events/guild.go b/bot/events/guild.go similarity index 89% rename from internals/discord/bot/events/guild.go rename to bot/events/guild.go index 9c049f1..4557fa4 100644 --- a/internals/discord/bot/events/guild.go +++ b/bot/events/guild.go @@ -1,12 +1,13 @@ package events import ( - "dislate/internals/discord/bot/events/errors" - "dislate/internals/discord/bot/gconf" e "errors" "log/slog" - gdb "dislate/internals/guilddb" + "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" ) diff --git a/internals/discord/bot/events/messages.go b/bot/events/messages.go similarity index 95% rename from internals/discord/bot/events/messages.go rename to bot/events/messages.go index f53a071..2602cee 100644 --- a/internals/discord/bot/events/messages.go +++ b/bot/events/messages.go @@ -1,16 +1,16 @@ package events import ( - "dislate/internals/discord/bot/events/errors" - "dislate/internals/discord/bot/gconf" - "dislate/internals/guilddb" - "dislate/internals/translator" - "dislate/internals/translator/lang" 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" ) @@ -340,13 +340,13 @@ func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) errors.Event everr.AddData("TranslatedMessageID", m.ID) everr.AddData("TranslatedChannelID", m.ChannelID) - err := h.db.MessageDeleteFromChannel(guilddb.NewChannel(m.GuildID, m.ID, lang.EN)) + 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, lang.EN)) + 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 @@ -355,14 +355,16 @@ func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) errors.Event } wg.Wait() + + everrs := make([]error, 0, len(errs)) for err := range errs { - everr.Join(err) + everrs = append(everrs, err) } if len(errs) > 0 { - return everr + return everr.Join(everrs...) } - if err := h.db.MessageDelete(guilddb.NewMessage(msg.GuildID, msg.ChannelID, msg.ID, lang.EN)); err != nil { + 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) } @@ -392,7 +394,7 @@ func getUserWebhook(s *dgo.Session, channelID string, user *dgo.User) (*dgo.Webh return w, nil } -func getMessage(db gconf.DB, m *dgo.Message, lang lang.Language) (guilddb.Message, error) { +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) { @@ -411,7 +413,7 @@ func getMessage(db gconf.DB, m *dgo.Message, lang lang.Language) (guilddb.Messag func getTranslatedMessage( db gconf.DB, m, original *dgo.Message, - lang lang.Language, + lang translator.Language, ) (guilddb.Message, error) { msg, err := db.Message(m.GuildID, m.ChannelID, m.ID) diff --git a/bot/events/threads.go b/bot/events/threads.go new file mode 100644 index 0000000..ea4c732 --- /dev/null +++ b/bot/events/threads.go @@ -0,0 +1,497 @@ +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 "dislate/internals/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/internals/discord/bot/gconf/config.go b/bot/gconf/config.go similarity index 95% rename from internals/discord/bot/gconf/config.go rename to bot/gconf/config.go index 4bef662..a1c45e5 100644 --- a/internals/discord/bot/gconf/config.go +++ b/bot/gconf/config.go @@ -3,7 +3,7 @@ package gconf import ( "log/slog" - gdb "dislate/internals/guilddb" + gdb "forge.capytal.company/capytal/dislate/guilddb" dgo "github.com/bwmarrin/discordgo" ) diff --git a/internals/discord/bot/gconf/logger.go b/bot/gconf/logger.go similarity index 100% rename from internals/discord/bot/gconf/logger.go rename to bot/gconf/logger.go diff --git a/go.mod b/go.mod index 9604c1f..2c1b259 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module dislate +module forge.capytal.company/capytal/dislate go 1.22.5 diff --git a/internals/guilddb/guilddb.go b/guilddb/guilddb.go similarity index 94% rename from internals/guilddb/guilddb.go rename to guilddb/guilddb.go index d1dec22..e630d3e 100644 --- a/internals/guilddb/guilddb.go +++ b/guilddb/guilddb.go @@ -3,7 +3,7 @@ package guilddb import ( "errors" - "dislate/internals/translator/lang" + "forge.capytal.company/capytal/dislate/translator" ) type Guild[C any] struct { @@ -18,10 +18,10 @@ func NewGuild[C any](ID string, config C) Guild[C] { type Channel struct { GuildID string ID string - Language lang.Language + Language translator.Language } -func NewChannel(GuildID, ID string, lang lang.Language) Channel { +func NewChannel(GuildID, ID string, lang translator.Language) Channel { return Channel{GuildID, ID, lang} } @@ -31,18 +31,18 @@ type Message struct { GuildID string ChannelID string ID string - Language lang.Language + Language translator.Language OriginChannelID *string OriginID *string } -func NewMessage(GuildID, ChannelID, ID string, lang lang.Language) Message { +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 lang.Language, + lang translator.Language, OriginChannelID, OriginID string, ) Message { return Message{GuildID, ChannelID, ID, lang, &OriginChannelID, &OriginID} @@ -64,7 +64,7 @@ type GuildDB[C any] interface { // Will return ErrNotFound if no message is found or ErrInternal. MessageWithOriginByLang( guildID, originChannelId, originId string, - language lang.Language, + language translator.Language, ) (Message, error) // Inserts a new Message object in the database. // diff --git a/internals/guilddb/sqlite.go b/guilddb/sqlite.go similarity index 99% rename from internals/guilddb/sqlite.go rename to guilddb/sqlite.go index 82c04b7..b7ec556 100644 --- a/internals/guilddb/sqlite.go +++ b/guilddb/sqlite.go @@ -8,7 +8,7 @@ import ( "slices" "strings" - "dislate/internals/translator/lang" + "forge.capytal.company/capytal/dislate/translator" _ "github.com/tursodatabase/go-libsql" ) @@ -99,7 +99,7 @@ func (db *SQLiteDB[C]) MessagesWithOrigin( func (db *SQLiteDB[C]) MessageWithOriginByLang( guildID, originChannelID, originID string, - language lang.Language, + language translator.Language, ) (Message, error) { return db.selectMessage(` WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3 AND "Language" = $4 diff --git a/internals/discord/bot/errors/errors.go b/internals/discord/bot/errors/errors.go deleted file mode 100644 index dbe80dc..0000000 --- a/internals/discord/bot/errors/errors.go +++ /dev/null @@ -1,127 +0,0 @@ -package errors - -import ( - "errors" - "fmt" - "log/slog" - "reflect" - "strings" - - dgo "github.com/bwmarrin/discordgo" -) - -type BotError interface { - Error() string -} - -type BotErrorHandler interface { - Info() string - Error() string - Reply(s *dgo.Session, m *dgo.Message) BotErrorHandler - Send(s *dgo.Session, channelID string) BotErrorHandler - Log(l *slog.Logger) BotErrorHandler -} - -type defaultErrHandler struct { - BotError -} - -func (err *defaultErrHandler) Error() string { - return err.Error() -} - -func (err *defaultErrHandler) Info() string { - return err.Info() -} - -func (err *defaultErrHandler) Reply(s *dgo.Session, m *dgo.Message) BotErrorHandler { - if _, erro := s.ChannelMessageSendReply(m.ChannelID, err.Error(), m.Reference()); erro != nil { - s.ChannelMessageSend( - m.ChannelID, - fmt.Sprintf( - "Failed to reply message %s due to \"%s\" with error: %s.", - m.ID, - erro.Error(), - err.Error(), - ), - ) - } - return err -} - -func (err *defaultErrHandler) Send(s *dgo.Session, channelID string) BotErrorHandler { - if _, erro := s.ChannelMessageSend(channelID, err.Error()); erro != nil { - _, _ = s.ChannelMessageSend( - channelID, - fmt.Sprintf( - "Failed to send error message due to \"%s\" with error: %s.", - erro.Error(), - err.Error(), - ), - ) - } - return err -} - -func (err *defaultErrHandler) Log(l *slog.Logger) BotErrorHandler { - l.Error(err.Error()) - return err -} - -type EventError[E any] struct { - data map[string]any - errors []error -} - -func NewEventError[E any](data map[string]any, err ...error) *EventError[E] { - return &EventError[E]{data, err} -} - -func (h *EventError[E]) Wrap(err ...error) *EventError[E] { - h.errors = append(h.errors, errors.Join(err...)) - return h -} - -func (h *EventError[E]) Wrapf(format string, a ...any) *EventError[E] { - h.errors = append(h.errors, fmt.Errorf(format, a...)) - return h -} - -func (h *EventError[E]) AddData(key string, value any) *EventError[E] { - h.data[key] = value - return h -} - -func (h *EventError[E]) Error() string { - var ev E - var name string - if t := reflect.TypeOf(ev); t != nil { - if n := t.Name(); n != "" { - name = strings.ToUpper(n) - } else { - name = "UNAMED EVENT" - } - } else { - name = "UNAMED EVENT" - } - err := errors.Join(h.errors...) - return errors.Join(fmt.Errorf("Failed to process event %s", name), err).Error() -} - -func (h *EventError[E]) Log(l *slog.Logger) *EventError[E] { - dh := &defaultErrHandler{h} - dh.Log(l) - return h -} - -func (h *EventError[E]) Reply(s *dgo.Session, r *dgo.Message) *EventError[E] { - dh := &defaultErrHandler{h} - dh.Reply(s, r) - return h -} - -func (h *EventError[E]) Send(s *dgo.Session, channelID string) *EventError[E] { - dh := &defaultErrHandler{h} - dh.Send(s, channelID) - return h -} diff --git a/internals/discord/bot/events/threads.go b/internals/discord/bot/events/threads.go deleted file mode 100644 index 6f8636b..0000000 --- a/internals/discord/bot/events/threads.go +++ /dev/null @@ -1,167 +0,0 @@ -package events - -import ( - "dislate/internals/discord/bot/errors" - "dislate/internals/discord/bot/gconf" - "dislate/internals/translator" - e "errors" - "log/slog" - "sync" - - gdb "dislate/internals/guilddb" - - dgo "github.com/bwmarrin/discordgo" -) - -type ThreadCreate struct { - db gconf.DB - translator translator.Translator -} - -func NewThreadCreate(db gconf.DB, t translator.Translator) ThreadCreate { - return ThreadCreate{db, t} -} - -func (h ThreadCreate) Serve(s *dgo.Session, ev *dgo.ThreadCreate) { - log := gconf.GetLogger(ev.GuildID, s, h.db) - log.Debug("Thread created!", slog.String("parent", ev.ParentID), slog.String("thread", ev.ID)) - - everr := errors.NewEventError[ThreadCreate](map[string]any{ - "ThreadID": ev.ID, - "ParentID": ev.ParentID, - "GuildID": ev.GuildID, - }) - - // INFO: Threads have the same ID as the origin message of them - threadMsg, err := h.db.Message(ev.GuildID, ev.ParentID, ev.ID) - if e.Is(err, gdb.ErrNotFound) { - log.Debug("Parent message of thread not in database, ignoring", - slog.String("thread", ev.ID), - slog.String("parent", ev.ParentID), - slog.String("error", err.Error()), - ) - return - } else if err != nil { - everr.Wrap(e.New("Failed to get thread message"), err).Log(log).Send(s, ev.ID) - return - } - - 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 - } - - msgs, err := h.db.MessagesWithOrigin(ev.GuildID, originMsg.ChannelID, originMsg.ID) - if e.Is(err, gdb.ErrNotFound) { - log.Debug("No translated messages for thread parent message found, ignoring", - slog.String("thread message", ev.ID), - slog.String("parent channel", ev.ParentID), - ) - return - } else if err != nil { - everr.Wrapf("Failed to get parent's translated messagas", err). - AddData("OriginMessageID", originMsg.ID). - AddData("OriginChannelID", originMsg.ChannelID). - Log(log). - Send(s, ev.ID) - return - } - msgs = append(msgs, originMsg) - - dth, err := s.Channel(ev.ID) - if err != nil { - everr.Wrapf("Failed to get discord thread", err).Log(log).Send(s, ev.ID) - return - } else if !dth.IsThread() { - everr.Wrapf("Channel is not a thread").Log(log).Send(s, ev.ID) - return - } - - th := gdb.NewChannel(dth.GuildID, dth.ID, threadMsg.Language) - if err := h.db.ChannelInsert(th); e.Is(err, gdb.ErrNoAffect) { - log.Info("Thread already in database, probably created by bot", - slog.String("thread", dth.ID), - slog.String("parent", dth.ParentID), - ) - return - } else if err != nil { - everr.Wrapf("Failed to add thread channel to database", err).Log(log).Send(s, ev.ID) - return - } - - threadGroup := make([]gdb.Channel, len(msgs)) - - var wg sync.WaitGroup - - for i, m := range msgs { - - threadGroup[i] = gdb.NewChannel(m.GuildID, m.ID, m.Language) - - if m.ID == th.ID { - continue - } - - wg.Add(1) - - go func(m gdb.Message) { - defer wg.Done() - - 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 { - everr.Wrapf("Failed to create translated thread", err).Log(log).Send(s, ev.ID) - return - } - - if err := h.db.ChannelInsert(gdb.NewChannel(dtth.GuildID, dtth.ID, m.Language)); err != nil && - !e.Is(err, gdb.ErrNoAffect) { - everr.Wrapf("Failed to add translated thread to database", err). - AddData("TranslatedThreadID", dtth.ID). - AddData("TranslatedParentID", dtth.ParentID). - Log(log). - Send(s, ev.ID) - return - } - }(m) - } - - wg.Wait() - - if err := h.db.ChannelGroupInsert(threadGroup); err != nil { - everr.Wrapf("Failed to add group of threads to database", err). - AddData("ThreadGroup", threadGroup). - Log(log). - Send(s, ev.ID) - return - } - - thMsgs, err := s.ChannelMessages(th.ID, 10, "", "", "") - if err != nil { - everr.Wrapf("Failed to get thread messages", err).Log(log).Send(s, ev.ID) - return - } - - for _, m := range thMsgs { - if m.Content != "" { - m.GuildID = th.GuildID - NewMessageCreate(h.db, h.translator).sendMessage(log, s, m) - } - } -} diff --git a/internals/translator/translator.go b/internals/translator/translator.go deleted file mode 100644 index d37c4f2..0000000 --- a/internals/translator/translator.go +++ /dev/null @@ -1,24 +0,0 @@ -package translator - -import "dislate/internals/translator/lang" - -type Translator interface { - // Translate a text from a language to another language - Translate(from, to lang.Language, text string) (string, error) - // Detects the language of the text - Detect(text string) (lang.Language, error) -} - -type MockTranslator struct{} - -func NewMockTranslator() MockTranslator { - return MockTranslator{} -} - -func (t MockTranslator) Translate(from, to lang.Language, text string) (string, error) { - return text, nil -} - -func (t MockTranslator) Detect(text string) (lang.Language, error) { - return lang.EN, nil -} diff --git a/main.go b/main.go index 087a875..e330867 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,10 @@ import ( "syscall" "time" - "dislate/internals/discord/bot" - "dislate/internals/discord/bot/gconf" - "dislate/internals/guilddb" - "dislate/internals/translator" + "forge.capytal.company/capytal/dislate/bot" + "forge.capytal.company/capytal/dislate/bot/gconf" + "forge.capytal.company/capytal/dislate/guilddb" + "forge.capytal.company/capytal/dislate/translator" "github.com/charmbracelet/log" ) diff --git a/internals/translator/lang/lang.go b/translator/lang.go similarity index 79% rename from internals/translator/lang/lang.go rename to translator/lang.go index 9ed293c..61259fd 100644 --- a/internals/translator/lang/lang.go +++ b/translator/lang.go @@ -1,4 +1,4 @@ -package lang +package translator type Language string diff --git a/translator/translator.go b/translator/translator.go new file mode 100644 index 0000000..44a68aa --- /dev/null +++ b/translator/translator.go @@ -0,0 +1,22 @@ +package translator + +type Translator interface { + // Translate a text from a language to another language + Translate(from, to Language, text string) (string, error) + // Detects the language of the text + Detect(text string) (Language, error) +} + +type MockTranslator struct{} + +func NewMockTranslator() MockTranslator { + return MockTranslator{} +} + +func (t MockTranslator) Translate(from, to Language, text string) (string, error) { + return text, nil +} + +func (t MockTranslator) Detect(text string) (Language, error) { + return EN, nil +}