refactor: remove old abstraction from codebase

This commit is contained in:
Guz
2024-11-25 14:41:00 -03:00
parent 4a616f824b
commit 5c8f8e7fd8
5 changed files with 0 additions and 734 deletions

View File

@@ -1,67 +0,0 @@
package bot
import (
"database/sql"
"errors"
"log/slog"
"forge.capytal.company/capytal/dislate/commands"
"forge.capytal.company/capytal/dislate/db"
"forge.capytal.company/capytal/dislate/translator"
"github.com/bwmarrin/discordgo"
)
type Bot struct {
translator translator.Translator
db *db.Queries
logger *slog.Logger
session *discordgo.Session
}
func NewBot(
token string,
database *sql.DB,
translator translator.Translator,
log *slog.Logger,
) (*Bot, error) {
s, err := discordgo.New("Bot " + token)
if err != nil {
return nil, err
}
s.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
db, err := db.Prepare(database)
if err != nil {
return nil, err
}
return &Bot{
session: s,
db: db,
translator: translator,
logger: log,
}, nil
}
func (b *Bot) Start() error {
if err := b.session.Open(); err != nil {
return err
}
ch := commands.NewCommandsHandler(b.logger, b.session)
COMMANDS := []commands.Command{
&mockCommand{},
}
if err := ch.UpdateCommands(COMMANDS); err != nil {
return errors.Join(errors.New("Failed to update commands"), err)
}
return nil
}
func (b *Bot) Stop() error {
return b.session.Close()
}

View File

@@ -1,89 +0,0 @@
package bot
import (
"errors"
"fmt"
"forge.capytal.company/capytal/dislate/commands"
"github.com/bwmarrin/discordgo"
)
type mockCommand struct{}
func (c *mockCommand) Info() *discordgo.ApplicationCommand {
return &discordgo.ApplicationCommand{
Name: "mock",
Type: discordgo.ChatApplicationCommand,
Description: "this is a mock command used for testing",
Options: []*discordgo.ApplicationCommandOption{{
Name: "message",
Description: "The message to respond",
Type: discordgo.ApplicationCommandOptionString,
}},
}
}
func (c *mockCommand) Handle(
s *discordgo.Session,
ic *discordgo.InteractionCreate,
data discordgo.ApplicationCommandInteractionData,
) error {
if len(data.Options) == 0 {
return errors.New("Option \"message\" is not defined")
}
text, ok := data.Options[0].Value.(string)
if !ok {
return errors.New("Failed to convert \"message\" into string")
}
if _, err := s.ChannelMessageSendComplex(ic.ChannelID, &discordgo.MessageSend{
Content: "test component",
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
c.Components()[0].Info(),
},
},
},
}); err != nil {
return err
}
return s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Hello user! Your text is %q", text),
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
func (c *mockCommand) Components() []commands.Component {
return []commands.Component{
&mockButton{},
}
}
type mockButton struct{}
func (c *mockButton) Info() discordgo.MessageComponent {
return &discordgo.Button{
Label: "test button",
CustomID: "mock-command-test-button",
}
}
func (c *mockButton) Handle(
s *discordgo.Session,
ic *discordgo.InteractionCreate,
data discordgo.MessageComponentInteractionData,
) error {
return s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Button clicked!"),
Flags: discordgo.MessageFlagsEphemeral,
},
})
}

View File

@@ -1,228 +0,0 @@
package commands
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"github.com/bwmarrin/discordgo"
)
type Command interface {
Info() *discordgo.ApplicationCommand
Handle(
s *discordgo.Session,
ic *discordgo.InteractionCreate,
data discordgo.ApplicationCommandInteractionData,
) error
}
type CommandWithComponents interface {
Command
Components() []Component
}
type Component interface {
Info() discordgo.MessageComponent
Handle(
s *discordgo.Session,
ic *discordgo.InteractionCreate,
data discordgo.MessageComponentInteractionData,
) error
}
type (
commandName = string
commandId = string
commandHandlerFunc = func(s *discordgo.Session, ic *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error
componentCustomId = string
componentHandlerFunc = func(s *discordgo.Session, ic *discordgo.InteractionCreate, data discordgo.MessageComponentInteractionData) error
)
type CommandsHandler struct {
logger *slog.Logger
session *discordgo.Session
}
func NewCommandsHandler(logger *slog.Logger, session *discordgo.Session) *CommandsHandler {
return &CommandsHandler{logger, session}
}
func (h *CommandsHandler) UpdateCommands(
commands []Command,
guildID ...string,
) error {
var GUILD_ID string
if len(guildID) > 0 {
GUILD_ID = guildID[0]
}
commandsMap, err := h.mapCommands(commands)
if err != nil {
return err
}
APP_ID := h.session.State.User.ID
if APP_ID == "" {
return errors.New("User ID is not set in session state")
}
registeredCommands, err := h.mapRegisteredCommmands(GUILD_ID)
if err != nil {
return err
}
if err := h.removeUnhandledCommands(commandsMap, registeredCommands, GUILD_ID); err != nil {
return err
}
commandInteractionHandlers := make(map[commandName]commandHandlerFunc, len(commandsMap))
componentInteractionHandlers := make(map[componentCustomId]componentHandlerFunc)
for _, cmd := range commandsMap {
var err error
appCmd, isRegistered := registeredCommands[cmd.Info().Name]
if !isRegistered {
h.logger.Debug("Bot command is not registered in application, registering.",
slog.String("command_name", cmd.Info().Name),
slog.String("guild_id", GUILD_ID))
appCmd, err = h.session.ApplicationCommandCreate(APP_ID, GUILD_ID, cmd.Info())
if err != nil {
return err
}
} else if y, err := equalToRegistered(cmd.Info(), appCmd); !y {
h.logger.Debug("Bot command and registered command are different, deleting registered command for updating.",
slog.String("command_name", cmd.Info().Name),
slog.String("registered_command_id", appCmd.ID),
slog.String("registered_command_name", appCmd.Name),
slog.String("guild_id", GUILD_ID),
slog.String("difference", err.Error()))
err = h.session.ApplicationCommandDelete(APP_ID, GUILD_ID, appCmd.ID)
if err != nil {
return err
}
appCmd, err = h.session.ApplicationCommandCreate(APP_ID, GUILD_ID, cmd.Info())
if err != nil {
return err
}
}
if withCompsCmd, ok := cmd.(CommandWithComponents); ok {
for _, comp := range withCompsCmd.Components() {
var id string
if comp.Info().Type() == discordgo.ActionsRowComponent {
// TODO
} else if comp.Info().Type() == discordgo.ButtonComponent {
button, ok := comp.Info().(*discordgo.Button)
if !ok {
return fmt.Errorf("Failed to convert ButtonComponent to Button struct on command %q", appCmd.Name)
}
switch {
case button.CustomID == "" && button.URL == "":
return fmt.Errorf("Button component on command %q does not have a valid CustomID or URL", appCmd.Name)
case button.CustomID != "" && button.URL != "":
return fmt.Errorf("Button component on command %q has mutually exclusive CustomID and URL", appCmd.Name)
case button.CustomID != "":
id = button.CustomID
case button.URL != "":
id = button.URL
}
} else {
j, err := comp.Info().MarshalJSON()
if err != nil {
return err
}
var v struct{ CustomID string }
if err := json.Unmarshal(j, &v); err != nil {
return err
}
id = v.CustomID
}
if _, ok := componentInteractionHandlers[id]; ok {
return fmt.Errorf(
"Component of ID %q used in command %q already exists!",
id,
appCmd.Name,
)
}
componentInteractionHandlers[id] = comp.Handle
}
}
commandInteractionHandlers[appCmd.Name] = cmd.Handle
}
h.session.AddHandler(func(s *discordgo.Session, ic *discordgo.InteractionCreate) {
h.handleInteraction(commandInteractionHandlers, componentInteractionHandlers, s, ic)
})
return nil
}
func (h *CommandsHandler) mapCommands(commands []Command) (map[commandName]Command, error) {
m := make(map[commandName]Command, len(commands))
for _, c := range commands {
if n := c.Info().Name; n != "" {
m[c.Info().Name] = c
} else {
return m, fmt.Errorf("Command doesn't have a valid name!")
}
}
return m, nil
}
func (h *CommandsHandler) mapRegisteredCommmands(
guildID string,
) (map[commandName]*discordgo.ApplicationCommand, error) {
cmdMap := map[commandName]*discordgo.ApplicationCommand{}
registeredCommands, err := h.session.ApplicationCommands(h.session.State.User.ID, guildID)
if err != nil {
return cmdMap, err
}
for _, rc := range registeredCommands {
cmdMap[rc.Name] = rc
}
return cmdMap, nil
}
func (h *CommandsHandler) removeUnhandledCommands(
handledCommands map[commandName]Command,
registeredCommands map[commandName]*discordgo.ApplicationCommand,
guildID string,
) error {
for _, cmd := range registeredCommands {
if _, isHandled := handledCommands[cmd.Name]; !isHandled {
h.logger.Debug("Registered command no longer is being handled, deleting.",
slog.String("registered_command_name", cmd.Name),
slog.String("registered_command_id", cmd.ID),
slog.String("guild_id", guildID))
err := h.session.ApplicationCommandDelete(h.session.State.User.ID, guildID, cmd.ID)
if err != nil {
return err
}
delete(registeredCommands, cmd.Name)
}
}
return nil
}

View File

@@ -1,211 +0,0 @@
package commands
import (
"errors"
"fmt"
"reflect"
"github.com/bwmarrin/discordgo"
)
// Helper function used inside equalToRegistered and equalToRegistered option,
// THIS ISN'T SUPPOSED TO BE USED outside of said functions.
func equal[T any](l, r T) bool {
lv := reflect.ValueOf(l)
if lv.Kind() == reflect.Pointer {
// If the local value is a nil-pointer, we assume it as the
// zero-value of the underling value for comparison
var v any
if lv.IsNil() {
v = reflect.Zero(lv.Type().Elem()).Interface()
} else {
v = lv.Elem().Interface()
}
rv := reflect.ValueOf(r)
var rvv any
if rv.IsNil() {
rvv = reflect.Zero(rv.Type().Elem()).Interface()
} else {
rvv = rv.Elem().Interface()
}
return equal(v, rvv)
} else {
return reflect.DeepEqual(l, r)
}
}
// Helper function used inside equalToRegistered and equalToRegistered option,
// THIS ISN'T SUPPOSED TO BE USED outside of said functions.
func val[T any](v *T) T {
if v != nil {
return *v
} else {
return *new(T)
}
}
func equalToRegistered(local, registered *discordgo.ApplicationCommand) (bool, error) {
switch {
case local.Type != registered.Type:
return false, fmt.Errorf(
"Type is not equal. Local: %#v Registered: %#v",
local.Type,
registered.Type,
)
case local.Name != registered.Name:
return false, fmt.Errorf(
"Name is not equal. Local: %#v Registered: %#v",
local.Name,
registered.Name,
)
case !equal(local.NameLocalizations, registered.NameLocalizations):
return false, fmt.Errorf(
"NameLocalizations is not equal. Local: *%#v Registered: *%#v",
val(local.NameLocalizations),
val(registered.NameLocalizations),
)
// DEPRECATED FIELDS https://discord.com/developers/docs/interactions/application-commands
//
// case !equal(local.DefaultMemberPermissions, registered.DefaultMemberPermissions):
// return false, fmt.Errorf(
// "DefaultMemberPermissions is not equal. Local: *%#v Registered: *%#v",
// val(local.DefaultMemberPermissions),
// val(registered.DefaultMemberPermissions),
// )
//
// case
// !equal(local.DMPermission, registered.DMPermission):
// return false, fmt.Errorf(
// "DMPermission is not equal. Local: *%#v Registered: *%#v",
// val(local.DMPermission),
// val(registered.DMPermission),
// )
case !equal(local.NSFW, registered.NSFW):
return false, fmt.Errorf(
"VALUE is not equal. Local: *%#v Registered: *%#v",
val(local.NSFW),
val(registered.NSFW),
)
case local.Description != registered.Description:
return false, fmt.Errorf(
"Description is not equal. Local: %#v Registered: %#v",
local.Description,
registered.Description,
)
case !equal(local.DescriptionLocalizations, registered.DescriptionLocalizations):
return false, fmt.Errorf(
"DescriptionLocalizations is not equal. Local: *%#v Registered: *%#v",
val(local.DescriptionLocalizations),
val(registered.DescriptionLocalizations),
)
case len(local.Options) != len(registered.Options):
return false, fmt.Errorf(
"Options is not equal. Local: %#v Registered: %#v",
local.Options,
registered.Options,
)
case len(local.Options) > 0 && len(registered.Options) > 0:
for i, o := range local.Options {
if ok, err := equalToRegisteredOption(o, registered.Options[i]); !ok {
return ok, errors.Join(fmt.Errorf("Option element of index %v has difference", err))
}
}
}
return true, nil
}
func equalToRegisteredOption(local, registered *discordgo.ApplicationCommandOption) (bool, error) {
switch {
case local.Type != registered.Type:
return false, fmt.Errorf(
"Type is not equal. Local: %#v Registered: %#v",
local.Type,
registered.Type,
)
case local.Name != registered.Name:
return false, fmt.Errorf(
"Name is not equal. Local: %#v Registered: %#v",
local.Name,
registered.Name,
)
case local.Description != registered.Description:
return false, fmt.Errorf(
"Description is not equal. Local: %#v Registered: %#v",
local.Description,
registered.Description,
)
case !equal(local.DescriptionLocalizations, registered.DescriptionLocalizations):
return false, fmt.Errorf(
"DescriptionLocalizations is not equal. Local: %#v Registered: %#v",
local.DescriptionLocalizations,
registered.DescriptionLocalizations,
)
case !equal(local.ChannelTypes, registered.ChannelTypes):
return false, fmt.Errorf(
"ChannelTypes is not equal. Local: %#v Registered: %#v",
local.ChannelTypes,
registered.ChannelTypes,
)
case local.Required != registered.Required:
return false, fmt.Errorf(
"Required is not equal. Local: %#v Registered: %#v",
local.Required,
registered.Required,
)
case !equal(local.Choices, registered.Choices):
return false, fmt.Errorf(
"Choices is not equal. Local: %#v Registered: %#v",
local.Choices,
registered.Choices,
)
case !equal(local.MinValue, registered.MinValue):
return false, fmt.Errorf(
"MinValue is not equal. Local: *%#v Registered: *%#v",
val(local.MinValue),
val(registered.MinValue),
)
case local.MaxValue != registered.MaxValue:
return false, fmt.Errorf(
"MaxValue is not equal. Local: %#v Registered: %#v",
local.MaxValue,
registered.MaxValue,
)
case !equal(local.MinLength, registered.MinLength):
return false, fmt.Errorf(
"MinLength is not equal. Local: *%#v Registered: *%#v",
val(local.MinLength),
val(registered.MinLength),
)
case local.MaxLength != registered.MaxLength:
return false, fmt.Errorf(
"MaxLength is not equal. Local: %#v Registered: %#v",
local.MaxLength,
registered.MaxLength,
)
}
return true, nil
}

View File

@@ -1,139 +0,0 @@
package commands
import (
"fmt"
"log/slog"
"github.com/bwmarrin/discordgo"
)
func (h *CommandsHandler) handleInteraction(
cmdsHandlers map[commandName]commandHandlerFunc,
compsHandlers map[componentCustomId]componentHandlerFunc,
s *discordgo.Session,
ic *discordgo.InteractionCreate,
) {
var userID string
if ic.User != nil {
userID = ic.User.ID
} else {
userID = ic.Member.User.ID
}
switch ic.Type {
case discordgo.InteractionApplicationCommand:
h.handleCommandInteraction(cmdsHandlers, s, ic)
case discordgo.InteractionMessageComponent:
h.handleComponentInteraction(compsHandlers, s, ic)
default:
h.logger.Error("Application interaction is not a supported type!",
slog.String("interaction_id", ic.ID),
slog.String("interaction_type", ic.Type.String()),
slog.String("interaction_user_id", userID),
slog.String("interaction_guild_id", ic.GuildID),
)
}
}
func (h *CommandsHandler) handleCommandInteraction(
cmdsHandlers map[commandName]commandHandlerFunc,
s *discordgo.Session,
ic *discordgo.InteractionCreate,
) {
var userID string
if ic.User != nil {
userID = ic.User.ID
} else {
userID = ic.Member.User.ID
}
data := ic.ApplicationCommandData()
log := h.logger.With(
slog.String("command_data_id", data.ID),
slog.String("command_data_name", data.Name),
slog.String("interaction_user_id", userID),
slog.String("interaction_guild_id", ic.GuildID),
)
if hf, ok := cmdsHandlers[data.Name]; ok {
log.Debug("Handling application command.")
if err := hf(s, ic, data); err != nil {
log.Error("Failed to run command, error returned.", slog.String("error", err.Error()))
_, err = s.ChannelMessageSendComplex(ic.ChannelID, &discordgo.MessageSend{
Content: fmt.Sprintf(
"<@%s>\nFailed to run command! Error message:\n```\n%s\n```",
userID,
err.Error(),
),
})
if err != nil {
log.Error(
"Failed to send error message explaining error... somehow.",
slog.String("error", err.Error()),
)
}
} else {
log.Debug("Command ran successfully.")
}
} else {
log.Error("Application command interaction created without having a handler.")
}
}
func (h *CommandsHandler) handleComponentInteraction(
compsHandlers map[componentCustomId]componentHandlerFunc,
s *discordgo.Session,
ic *discordgo.InteractionCreate,
) {
var userID string
if ic.User != nil {
userID = ic.User.ID
} else {
userID = ic.Member.User.ID
}
data := ic.MessageComponentData()
log := h.logger.With(
slog.String("component_data_custom_id", data.CustomID),
slog.String("component_data_type", data.Type().String()),
slog.String("interaction_user_id", userID),
slog.String("interaction_guild_id", ic.GuildID),
)
if hf, ok := compsHandlers[data.CustomID]; ok {
log.Debug("Handling application component.")
if err := hf(s, ic, data); err != nil {
log.Error(
"Failed to handle component, error returned.",
slog.String("error", err.Error()),
)
_, err = s.ChannelMessageSendComplex(ic.ChannelID, &discordgo.MessageSend{
Content: fmt.Sprintf(
"<@%s>\nFailed to handle component! Error message:\n```\n%s\n```",
userID,
err.Error(),
),
})
if err != nil {
log.Error(
"Failed to send error message explaining error... somehow.",
slog.String("error", err.Error()),
)
}
} else {
log.Debug("Component handled successfully.")
}
} else {
log.Error("Application component interaction created without having a handler.")
}
}