refactor(events,errors): simplify and structure more error handling of events

This commit is contained in:
Guz
2024-09-05 17:35:52 -03:00
parent 790687e351
commit 98009015c3
8 changed files with 356 additions and 122 deletions

View File

@@ -1,15 +1,30 @@
package bot
import "dislate/internals/discord/bot/events"
import (
"dislate/internals/discord/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{
events.NewGuildCreate(b.logger, b.db).Serve,
events.NewMessageCreate(b.db, b.translator).Serve,
events.NewMessageUpdate(b.db, b.translator).Serve,
events.NewMessageDelete(b.db).Serve,
events.NewReady(b.logger, b.db).Serve,
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)),
}
for _, h := range ehs {
b.session.AddHandler(h)

View File

@@ -0,0 +1,115 @@
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
}

View File

@@ -0,0 +1,10 @@
package errors
type EventErr interface {
Error() string
Event() string
Reply() error
Send() error
Log()
Join(...error) EventErr
}

View File

@@ -0,0 +1,42 @@
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,
}}
}

View File

@@ -0,0 +1,30 @@
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] {
return MessageErr[E]{&defaultEventErr[E]{
data: map[string]any{
"MessageID": msg.ID,
"ChannelID": msg.ChannelID,
"GuildID": msg.GuildID,
"AuthorID": msg.Author.ID,
},
session: s,
channelID: msg.ChannelID,
messageReference: msg.Reference(),
logger: log,
}}
}

View File

@@ -1,9 +1,11 @@
package events
import (
"dislate/internals/discord/bot/events/errors"
dgo "github.com/bwmarrin/discordgo"
)
type EventHandler[E any] interface {
Serve(*dgo.Session, E)
Serve(*dgo.Session, E) errors.EventErr
}

View File

@@ -1,12 +1,11 @@
package events
import (
"dislate/internals/discord/bot/events/errors"
"dislate/internals/discord/bot/gconf"
e "errors"
"log/slog"
"dislate/internals/discord/bot/errors"
"dislate/internals/discord/bot/gconf"
gdb "dislate/internals/guilddb"
dgo "github.com/bwmarrin/discordgo"
@@ -21,20 +20,20 @@ func NewGuildCreate(log *slog.Logger, db gconf.DB) GuildCreate {
return GuildCreate{log, db}
}
func (h GuildCreate) Serve(s *dgo.Session, ev *dgo.GuildCreate) {
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.NewEventError[GuildCreate](map[string]any{
"GuildID": ev.Guild.ID,
})
everr := errors.NewGuildErr[*dgo.GuildCreate](ev.Guild, h.log)
if err != nil && !e.Is(err, gdb.ErrNoAffect) {
everr.Wrapf("Failed to add guild to database", err).Log(h.log)
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 {
@@ -46,18 +45,20 @@ 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) {
everr := errors.NewEventError[GuildCreate](map[string]any{})
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) {
everr.Wrapf("Failed to add guild to database", err).AddData("GuildID", g.ID).Log(h.log)
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
}

View File

@@ -1,15 +1,15 @@
package events
import (
e "errors"
"log/slog"
"slices"
"dislate/internals/discord/bot/errors"
"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"
dgo "github.com/bwmarrin/discordgo"
)
@@ -23,21 +23,24 @@ func NewMessageCreate(db gconf.DB, t translator.Translator) MessageCreate {
return MessageCreate{db, t}
}
func (h MessageCreate) Serve(s *dgo.Session, ev *dgo.MessageCreate) {
func (h MessageCreate) Serve(
s *dgo.Session,
ev *dgo.MessageCreate,
) errors.EventErr {
if ev.Message.Author.Bot || ev.Type != dgo.MessageTypeDefault {
return
return nil
}
log := gconf.GetLogger(ev.Message.GuildID, s, h.db)
h.sendMessage(log, s, ev.Message)
return h.sendMessage(log, s, ev.Message)
}
func (h MessageCreate) sendMessage(log *slog.Logger, s *dgo.Session, msg *dgo.Message) {
everr := errors.NewEventError[MessageCreate](map[string]any{
"GuildID": msg.GuildID,
"ChannelID": msg.ChannelID,
"MessageID": msg.ID,
})
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) {
@@ -46,10 +49,9 @@ func (h MessageCreate) sendMessage(log *slog.Logger, s *dgo.Session, msg *dgo.Me
slog.String("channel", msg.ChannelID),
slog.String("message", msg.ID),
)
return
return nil
} else if err != nil {
everr.Wrapf("Failed to get channel from database", err).Log(log).Reply(s, msg)
return
return everr.Join(e.New("Failed to get channel from database"), err)
}
gc, err := h.db.ChannelGroup(ch.GuildID, ch.ID)
@@ -59,31 +61,35 @@ func (h MessageCreate) sendMessage(log *slog.Logger, s *dgo.Session, msg *dgo.Me
slog.String("channel", msg.ChannelID),
slog.String("message", msg.ID),
)
return
return nil
} else if err != nil {
everr.Wrapf("Failed to get channel group from database", err).Log(log).Reply(s, msg)
return
return everr.Join(e.New("Failed to get channel group from database"), err)
}
_, err = getMessage(h.db, msg, ch.Language)
if err != nil {
everr.Wrapf("Failed to get/add message to database", err).Log(log).Reply(s, msg)
return
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
}
go func(c guilddb.Channel) {
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 {
everr.Wrapf("Failed to get information about translated channel", err).
AddData("TranslatedChannel", c.ID).
Log(log).
Reply(s, msg)
errs <- everr.Join(e.New("Failed to get information about translated channel"), err)
return
} else if dch.IsThread() {
channelID = dch.ParentID
@@ -93,20 +99,13 @@ func (h MessageCreate) sendMessage(log *slog.Logger, s *dgo.Session, msg *dgo.Me
uw, err := getUserWebhook(s, channelID, msg.Author)
if err != nil {
everr.Wrapf("Failed to get/set user webhook for translated channel", err).
AddData("TranslatedChannel", c.ID).
AddData("User", msg.Author.ID).
Log(log).
Reply(s, msg)
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 {
everr.Wrapf("Error while trying to translate message", err).
AddData("content", msg.Content).
Log(log).
Reply(s, msg)
errs <- everr.Join(e.New("Error while trying to translate message"), err)
return
}
@@ -125,12 +124,8 @@ func (h MessageCreate) sendMessage(log *slog.Logger, s *dgo.Session, msg *dgo.Me
})
}
if err != nil {
everr.Wrapf("Error while trying to execute user webhook", err).
AddData("content", msg.Content).
AddData("User", msg.Author.ID).
AddData("Webhook", uw.ID).
Log(log).
Reply(s, msg)
everr.AddData("WebhookID", uw.ID)
errs <- everr.Join(e.New("Error while trying to execute user webhook"), err)
return
}
@@ -140,15 +135,24 @@ func (h MessageCreate) sendMessage(log *slog.Logger, s *dgo.Session, msg *dgo.Me
_, err = getTranslatedMessage(h.db, tdm, msg, c.Language)
if err != nil {
everr.Wrapf("Error while trying to get/set translated message", err).
AddData("TranslatedMessageID", tdm.ID).
Log(log).
Reply(s, msg)
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)
}(c, errs)
}
wg.Wait()
for err := range errs {
everr.Join(err)
}
if len(errs) > 0 {
return everr
}
return nil
}
type MessageUpdate struct {
@@ -160,18 +164,13 @@ func NewMessageUpdate(db gconf.DB, t translator.Translator) MessageUpdate {
return MessageUpdate{db, t}
}
func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) {
func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) errors.EventErr {
if ev.Message.Author.Bot || ev.Type != dgo.MessageTypeDefault {
return
return nil
}
log := gconf.GetLogger(ev.Message.GuildID, s, h.db)
everr := errors.NewEventError[MessageUpdate](map[string]any{
"GuildID": ev.Message.GuildID,
"ChannelID": ev.Message.ChannelID,
"MessageID": ev.Message.ID,
})
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) {
@@ -179,10 +178,9 @@ func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) {
slog.String("guild", ev.Message.GuildID),
slog.String("channel", ev.Message.ChannelID),
)
return
return nil
} else if err != nil {
everr.Wrapf("Failed to get message from database", err).Log(log).Reply(s, ev.Message)
return
return everr.Join(e.New("Failed to get message from database"), err)
}
tmsgs, err := h.db.MessagesWithOrigin(msg.GuildID, msg.ChannelID, msg.ID)
@@ -191,23 +189,30 @@ func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) {
slog.String("guild", ev.GuildID),
slog.String("channel", ev.ChannelID),
)
return
return nil
} else if err != nil {
everr.Wrapf("Failed to get translated messages from database", err).Log(log).Reply(s, ev.Message)
return
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
}
go func(m guilddb.Message) {
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 {
everr.Wrapf("Failed to get information about translated channel", err).
AddData("TranslatedChannel", m.ChannelID).
Log(log).
Reply(s, ev.Message)
errs <- everr.Join(e.New("Failed to get information about translated channel"), err)
return
} else if dch.IsThread() {
channelID = dch.ParentID
} else {
@@ -216,20 +221,13 @@ func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) {
uw, err := getUserWebhook(s, channelID, ev.Message.Author)
if err != nil {
everr.Wrapf("Failed to get/set user webhook for translated channel", err).
AddData("TranslatedChannel", m.ChannelID).
AddData("User", ev.Message.Author.ID).
Log(log).
Reply(s, ev.Message)
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 {
everr.Wrapf("Error while trying to translate message", err).
AddData("content", ev.Message.Content).
Log(log).
Reply(s, ev.Message)
errs <- everr.Join(e.New("Error while trying to translate message"), err)
return
}
@@ -237,17 +235,23 @@ func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) {
Content: &t,
})
if err != nil {
everr.Wrapf("Error while trying to execute user webhook", err).
AddData("content", ev.Message.Content).
AddData("User", ev.Message.Author.ID).
AddData("Webhook", uw.ID).
Log(log).
Reply(s, ev.Message)
everr.AddData("WebhookID", uw.ID)
errs <- everr.Join(e.New("Error while trying to execute user webhook"), err)
return
}
}(m)
}(m, errs)
}
wg.Wait()
for err := range errs {
everr.Join(err)
}
if len(errs) > 0 {
return everr
}
return nil
}
type MessageDelete struct {
@@ -258,17 +262,13 @@ func NewMessageDelete(db gconf.DB) MessageDelete {
return MessageDelete{db}
}
func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) {
func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) errors.EventErr {
if ev.Type != dgo.MessageTypeDefault {
return
return nil
}
log := gconf.GetLogger(ev.Message.GuildID, s, h.db)
everr := errors.NewEventError[MessageUpdate](map[string]any{
"GuildID": ev.Message.GuildID,
"ChannelID": ev.Message.ChannelID,
"MessageID": ev.Message.ID,
})
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) {
@@ -276,22 +276,25 @@ func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) {
slog.String("guild", ev.Message.GuildID),
slog.String("channel", ev.Message.ChannelID),
)
return
return nil
} else if err != nil {
everr.Wrapf("Failed to get message from database", err).Log(log).Reply(s, ev.Message)
return
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, originID = *msg.OriginChannelID, *msg.OriginID
originChannelID = *msg.OriginChannelID
originID = *msg.OriginID
} else {
msg, originChannelID, originID = oMsg, oMsg.ChannelID, oMsg.ID
msg = oMsg
originChannelID = oMsg.ChannelID
originID = oMsg.ID
}
} else {
originChannelID, originID = msg.ChannelID, msg.ID
originChannelID = msg.ChannelID
originID = msg.ID
}
tmsgs, err := h.db.MessagesWithOrigin(msg.GuildID, originChannelID, originID)
@@ -300,10 +303,9 @@ func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) {
slog.String("guild", ev.GuildID),
slog.String("channel", ev.ChannelID),
)
return
return nil
} else if err != nil {
everr.Wrapf("Failed to get translated messages from database", err).Log(log).Reply(s, ev.Message)
return
return everr.Join(e.New("Failed to get translated messages from database"), err)
}
for _, m := range tmsgs {
@@ -329,25 +331,42 @@ func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) {
)
}
var wg sync.WaitGroup
errs := make(chan errors.EventErr)
for _, m := range append(tmsgs, msg) {
go func(m guilddb.Message) {
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, lang.EN))
if err != nil && !e.Is(err, guilddb.ErrNoAffect) {
everr.AddData("ThreadID", m.GuildID).Log(log)
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))
if err != nil && !e.Is(err, guilddb.ErrNoAffect) {
everr.AddData("ThreadID", m.GuildID).Log(log)
errs <- everr.Join(e.New("Failed to delete message thread from channel"), err)
return
}
}(m)
}(m, errs)
}
wg.Wait()
for err := range errs {
everr.Join(err)
}
if len(errs) > 0 {
return everr
}
if err := h.db.MessageDelete(guilddb.NewMessage(msg.GuildID, msg.ChannelID, msg.ID, lang.EN)); err != nil {
everr.Wrapf("Failed to delete message from database", err).Log(log).Send(s, msg.ChannelID)
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) {