refactor!: restructure all project packages to simplify it

This commit is contained in:
Guz
2024-11-04 09:02:57 -03:00
parent 984f39caf2
commit daa63f5d64
26 changed files with 612 additions and 374 deletions

View File

@@ -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"
)

View File

@@ -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",

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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)

View File

@@ -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,
}}
}

View File

@@ -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"
)

View File

@@ -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"
)

View File

@@ -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)

497
bot/events/threads.go Normal file
View File

@@ -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
}

View File

@@ -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"
)

2
go.mod
View File

@@ -1,4 +1,4 @@
module dislate
module forge.capytal.company/capytal/dislate
go 1.22.5

View File

@@ -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.
//

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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"
)

View File

@@ -1,4 +1,4 @@
package lang
package translator
type Language string

22
translator/translator.go Normal file
View File

@@ -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
}