chore(v1): delete old code from version 1

This commit is contained in:
Guz
2024-11-18 13:34:25 -03:00
parent 2ca2193617
commit fd2593e43d
19 changed files with 0 additions and 2946 deletions

View File

@@ -1,61 +0,0 @@
package bot
import (
"log/slog"
"forge.capytal.company/capytal/dislate/translator"
"forge.capytal.company/capytal/dislate/bot/gconf"
dgo "github.com/bwmarrin/discordgo"
)
type Bot struct {
token string
db gconf.DB
translator translator.Translator
session *dgo.Session
logger *slog.Logger
}
func NewBot(
token string,
db gconf.DB,
translator translator.Translator,
logger *slog.Logger,
) (*Bot, error) {
discord, err := dgo.New("Bot " + token)
if err != nil {
return &Bot{}, err
}
return &Bot{
token: token,
db: db,
translator: translator,
session: discord,
logger: logger,
}, nil
}
func (b *Bot) Start() error {
b.registerEventHandlers()
b.session.Identify.Intents = dgo.MakeIntent(dgo.IntentsAllWithoutPrivileged)
if err := b.session.Open(); err != nil {
return err
}
if err := b.registerCommands(); err != nil {
return err
}
return nil
}
func (b *Bot) Stop() error {
if err := b.removeCommands(); err != nil {
return err
}
return b.session.Close()
}

View File

@@ -1,180 +0,0 @@
package bot
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"slices"
"forge.capytal.company/capytal/dislate/botv1/commands"
dgo "github.com/bwmarrin/discordgo"
)
func (b *Bot) registerCommands() error {
cs := []commands.Command{
commands.NewMagageConfig(b.db),
commands.NewManageChannel(b.db),
}
handlers := make(map[string]func(*dgo.Session, *dgo.InteractionCreate), len(cs))
componentsHandlers := make(map[string]func(*dgo.Session, *dgo.InteractionCreate))
for _, v := range cs {
var cmd *dgo.ApplicationCommand
var err error
subCmds := make(map[string]commands.Command)
sb := v.Subcommands()
if len(sb) == 0 {
cmd, err = b.session.ApplicationCommandCreate(b.session.State.User.ID, "", v.Info())
if err != nil {
return err
}
} else {
subCmdsOpts := make([]*dgo.ApplicationCommandOption, len(sb))
for i, sb := range sb {
subCmds[sb.Info().Name] = sb
subCmdsOpts[i] = &dgo.ApplicationCommandOption{
Type: dgo.ApplicationCommandOptionSubCommand,
Name: sb.Info().Name,
Description: sb.Info().Description,
Options: sb.Info().Options,
}
}
info := v.Info()
info.Options = subCmdsOpts
cmd, err = b.session.ApplicationCommandCreate(b.session.State.User.ID, "", info)
if err != nil {
return err
}
}
for _, c := range v.Components() {
cj, err := c.Info().MarshalJSON()
if err != nil {
return errors.Join(fmt.Errorf("Failed to marshal command"), err)
}
var v struct {
CustomID string `json:"custom_id"`
}
if err := json.Unmarshal(cj, &v); err != nil {
return errors.Join(fmt.Errorf("Failed to unmarshal command"), err)
}
componentsHandlers[v.CustomID] = func(s *dgo.Session, ic *dgo.InteractionCreate) {
b.logger.Debug("Handling message component",
slog.String("id", ic.Interaction.ID),
slog.String("custom_id", ic.Interaction.MessageComponentData().CustomID),
)
err := c.Handle(s, ic)
if err != nil {
b.logger.Error("Failed to handle message component",
slog.String("custom_id", ic.Interaction.MessageComponentData().CustomID),
slog.String("err", err.Error()),
)
}
}
}
handlers[cmd.Name] = func(s *dgo.Session, ic *dgo.InteractionCreate) {
b.logger.Debug("Handling command",
slog.String("id", ic.Interaction.ID),
slog.String("name", ic.Interaction.ApplicationCommandData().Name),
)
opts := ic.Interaction.ApplicationCommandData().Options
isSub := slices.IndexFunc(
opts,
func(o *dgo.ApplicationCommandInteractionDataOption) bool {
return o.Type == dgo.ApplicationCommandOptionSubCommand
},
)
if isSub != -1 {
sc := opts[isSub]
err := subCmds[sc.Name].Handle(s, ic)
if err != nil {
_ = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Content: fmt.Sprintf(
"Error while trying to handle sub command: %s",
err.Error(),
),
Flags: dgo.MessageFlagsEphemeral,
},
})
b.logger.Error("Failed to handle sub command",
slog.String("name", sc.Name),
slog.String("err", err.Error()),
)
}
return
}
err := v.Handle(s, ic)
if err != nil {
_ = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Content: fmt.Sprintf(
"Error while trying to handle command: %s",
err.Error(),
),
Flags: dgo.MessageFlagsEphemeral,
},
})
b.logger.Error("Failed to handle command",
slog.String("name", cmd.Name),
slog.String("id", cmd.ID),
slog.String("err", err.Error()),
)
}
}
b.logger.Info("Registered command",
slog.String("name", cmd.Name),
slog.String("id", cmd.ID),
)
}
b.session.AddHandler(func(s *dgo.Session, i *dgo.InteractionCreate) {
switch i.Interaction.Type {
case dgo.InteractionApplicationCommand:
if h, ok := handlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
case dgo.InteractionMessageComponent:
if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok {
h(s, i)
}
}
})
return nil
}
func (b *Bot) removeCommands() error {
cmds, err := b.session.ApplicationCommands(b.session.State.Application.ID, "")
if err != nil {
return err
}
for _, v := range cmds {
err := b.session.ApplicationCommandDelete(b.session.State.User.ID, "", v.ID)
if err != nil {
return err
}
b.logger.Info("Removed command",
slog.String("name", v.Name),
slog.String("id", v.ID),
)
}
return nil
}

View File

@@ -1,378 +0,0 @@
package commands
import (
"errors"
"fmt"
"strings"
"forge.capytal.company/capytal/dislate/bot/gconf"
"forge.capytal.company/capytal/dislate/guilddb"
"forge.capytal.company/capytal/dislate/translator"
gdb "forge.capytal.company/capytal/dislate/guilddb"
dgo "github.com/bwmarrin/discordgo"
)
type ManageChannel struct {
db gconf.DB
}
func NewManageChannel(db gconf.DB) ManageChannel {
return ManageChannel{db}
}
func (c ManageChannel) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionManageChannels
return &dgo.ApplicationCommand{
Name: "channel",
Description: "Manages a channel options",
DefaultMemberPermissions: &permissions,
}
}
func (c ManageChannel) Subcommands() []Command {
return []Command{
channelsInfo(c),
channelsLink(c),
channelsSetLang(c),
}
}
func (c ManageChannel) Handle(s *dgo.Session, i *dgo.InteractionCreate) error {
return nil
}
func (c ManageChannel) Components() []Component {
return []Component{}
}
type channelsInfo struct {
db gconf.DB
}
func (c channelsInfo) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionManageChannels
return &dgo.ApplicationCommand{
Name: "info",
Description: "Get information about a channel",
DefaultMemberPermissions: &permissions,
Options: []*dgo.ApplicationCommandOption{{
Type: dgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "The channel to manage",
ChannelTypes: []dgo.ChannelType{
dgo.ChannelTypeGuildText,
dgo.ChannelTypeGuildForum,
dgo.ChannelTypeGuildPublicThread,
dgo.ChannelTypeGuildPrivateThread,
},
}},
}
}
func (c channelsInfo) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error {
opts := getOptions(ic.ApplicationCommandData().Options)
var err error
var dch *dgo.Channel
if c, ok := opts["channel"]; ok {
dch = c.ChannelValue(s)
} else {
dch, err = s.Channel(ic.ChannelID)
if err != nil {
return err
}
}
ch, err := getChannel(c.db, dch.GuildID, dch.ID)
if err != nil {
return err
}
info, err := getChannelInfo(c.db, ch)
if err != nil {
return err
}
err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Embeds: []*dgo.MessageEmbed{info},
Flags: dgo.MessageFlagsEphemeral,
},
})
if err != nil {
return err
}
return nil
}
func (c channelsInfo) Components() []Component {
return []Component{}
}
func (c channelsInfo) Subcommands() []Command {
return []Command{}
}
type channelsLink struct {
db gconf.DB
}
func (c channelsLink) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionManageChannels
return &dgo.ApplicationCommand{
Name: "link",
Description: "Link two channels together",
DefaultMemberPermissions: &permissions,
Options: []*dgo.ApplicationCommandOption{{
Type: dgo.ApplicationCommandOptionChannel,
Name: "channel_one",
Description: "The channel to link",
Required: true,
ChannelTypes: []dgo.ChannelType{
dgo.ChannelTypeGuildText,
dgo.ChannelTypeGuildForum,
dgo.ChannelTypeGuildPublicThread,
dgo.ChannelTypeGuildPrivateThread,
},
}, {
Type: dgo.ApplicationCommandOptionChannel,
Name: "channel_two",
Description: "The channel to link",
ChannelTypes: []dgo.ChannelType{
dgo.ChannelTypeGuildText,
dgo.ChannelTypeGuildForum,
dgo.ChannelTypeGuildPublicThread,
dgo.ChannelTypeGuildPrivateThread,
},
}},
}
}
func (c channelsLink) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error {
opts := getOptions(ic.ApplicationCommandData().Options)
var err error
var dch1, dch2 *dgo.Channel
if c, ok := opts["channel_one"]; ok {
dch1 = c.ChannelValue(s)
} else {
return errors.New("channel_one is required")
}
if c, ok := opts["channel_two"]; ok {
dch2 = c.ChannelValue(s)
} else {
dch2, err = s.Channel(ic.ChannelID)
if err != nil {
return err
}
}
if dch1.ID == dch2.ID {
return errors.New("channel_one and channel_two must be different values")
} else if dch1.Type != dch2.Type {
return errors.New("channel_one and channel_two must be the same channel types")
}
ch1, err := getChannel(c.db, dch1.GuildID, dch1.ID)
if err != nil {
return err
}
ch2, err := getChannel(c.db, dch2.GuildID, dch2.ID)
if err != nil {
return err
}
var cb1, cb2 guilddb.ChannelGroup
cb1, err = c.db.ChannelGroup(ch1.GuildID, ch1.ID)
if err != nil && !errors.Is(err, guilddb.ErrNotFound) {
return err
}
cb2, err = c.db.ChannelGroup(ch2.GuildID, ch2.ID)
if err != nil && !errors.Is(err, guilddb.ErrNotFound) {
return err
}
if len(cb1) > 0 && len(cb2) > 0 {
return errors.New("both channels are already in a group")
} else if len(cb1) > 0 {
cb1 = append(cb1, ch2)
err = c.db.ChannelGroupUpdate(cb1)
} else if len(cb2) > 0 {
cb2 = append(cb2, ch1)
err = c.db.ChannelGroupUpdate(cb2)
} else {
err = c.db.ChannelGroupInsert(guilddb.ChannelGroup{ch1, ch2})
}
if err != nil {
return err
}
err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Content: fmt.Sprintf(
"Linked channel %s (%s) and %s (%s)",
dch1.Name, dch1.ID, dch2.Name, dch2.ID,
),
Flags: dgo.MessageFlagsEphemeral,
},
})
if err != nil {
return err
}
return nil
}
func (c channelsLink) Components() []Component {
return []Component{}
}
func (c channelsLink) Subcommands() []Command {
return []Command{}
}
type channelsSetLang struct {
db gconf.DB
}
func (c channelsSetLang) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionManageChannels
return &dgo.ApplicationCommand{
Name: "set-lang",
Description: "Link two channels together",
DefaultMemberPermissions: &permissions,
Options: []*dgo.ApplicationCommandOption{{
Type: dgo.ApplicationCommandOptionString,
Required: true,
Name: "language",
Description: "The new language",
Choices: []*dgo.ApplicationCommandOptionChoice{
{Name: "English (EN)", Value: translator.EN},
{Name: "Portuguese (PT)", Value: translator.PT},
},
}, {
Type: dgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "The channel to change the language",
ChannelTypes: []dgo.ChannelType{
dgo.ChannelTypeGuildText,
dgo.ChannelTypeGuildForum,
dgo.ChannelTypeGuildPublicThread,
dgo.ChannelTypeGuildPrivateThread,
},
}},
}
}
func (c channelsSetLang) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error {
opts := getOptions(ic.ApplicationCommandData().Options)
var err error
var dch *dgo.Channel
var l translator.Language
if c, ok := opts["language"]; ok {
switch c.StringValue() {
case string(translator.PT):
l = translator.PT
default:
l = translator.EN
}
} else {
return errors.New("language is a required option")
}
if c, ok := opts["channel"]; ok {
dch = c.ChannelValue(s)
} else {
dch, err = s.Channel(ic.ChannelID)
if err != nil {
return err
}
}
ch, err := getChannel(c.db, dch.GuildID, dch.ID)
if err != nil {
return err
}
ch.Language = l
err = c.db.ChannelUpdate(ch)
if err != nil {
return err
}
err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Content: fmt.Sprintf(
"Changed language of channel %s (%s) to %s",
dch.Name, dch.ID, l,
),
Flags: dgo.MessageFlagsEphemeral,
},
})
if err != nil {
return err
}
return nil
}
func (c channelsSetLang) Components() []Component {
return []Component{}
}
func (c channelsSetLang) Subcommands() []Command {
return []Command{}
}
func getChannel(db gconf.DB, guildID, channelID string) (gdb.Channel, error) {
ch, err := db.Channel(guildID, channelID)
if errors.Is(err, gdb.ErrNotFound) {
if err := db.ChannelInsert(gdb.NewChannel(guildID, channelID, translator.EN)); err != nil {
return gdb.Channel{}, err
}
ch, err = db.Channel(guildID, channelID)
if err != nil {
return gdb.Channel{}, err
}
} else if err != nil {
return gdb.Channel{}, err
}
return ch, nil
}
func getChannelInfo(db gconf.DB, ch gdb.Channel) (*dgo.MessageEmbed, error) {
group, err := db.ChannelGroup(ch.GuildID, ch.ID)
if err != nil && !errors.Is(err, gdb.ErrNotFound) {
return nil, err
}
g := make([]string, len(group))
for i, gi := range group {
g[i] = "<#" + gi.ID + ">"
}
return &dgo.MessageEmbed{
Title: "Channel Information",
Fields: []*dgo.MessageEmbedField{
{Name: "ID", Value: ch.ID, Inline: true},
{Name: "Language", Value: string(ch.Language), Inline: true},
{Name: "Linked Channels", Value: strings.Join(g, ", "), Inline: true},
},
}, nil
}

View File

@@ -1,33 +0,0 @@
package commands
import (
dgo "github.com/bwmarrin/discordgo"
)
type Command interface {
Info() *dgo.ApplicationCommand
Handle(s *dgo.Session, i *dgo.InteractionCreate) error
Subcommands() []Command
Components() []Component
}
type Component interface {
Info() dgo.MessageComponent
Handle(s *dgo.Session, i *dgo.InteractionCreate) error
}
func getOptions(
opts []*dgo.ApplicationCommandInteractionDataOption,
) map[string]*dgo.ApplicationCommandInteractionDataOption {
m := make(map[string]*dgo.ApplicationCommandInteractionDataOption, len(opts))
for _, opt := range opts {
if opt.Type == dgo.ApplicationCommandOptionSubCommand {
return getOptions(opt.Options)
} else {
m[opt.Name] = opt
}
}
return m
}

View File

@@ -1,187 +0,0 @@
package commands
import (
e "errors"
"fmt"
"log/slog"
"forge.capytal.company/capytal/dislate/bot/gconf"
dgo "github.com/bwmarrin/discordgo"
)
type ManageConfig struct {
db gconf.DB
}
func NewMagageConfig(db gconf.DB) ManageConfig {
return ManageConfig{db}
}
func (c ManageConfig) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionAdministrator
return &dgo.ApplicationCommand{
Name: "config",
Description: "Manages the guild's configuration",
DefaultMemberPermissions: &permissions,
}
}
func (c ManageConfig) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error {
return nil
}
func (c ManageConfig) Components() []Component {
return []Component{}
}
func (c ManageConfig) Subcommands() []Command {
return []Command{
loggerConfigChannel(c),
loggerConfigLevel(c),
}
}
type loggerConfigChannel struct {
db gconf.DB
}
func (c loggerConfigChannel) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionAdministrator
return &dgo.ApplicationCommand{
Name: "log-channel",
Description: "Change logging channel",
DefaultMemberPermissions: &permissions,
Options: []*dgo.ApplicationCommandOption{{
Type: dgo.ApplicationCommandOptionChannel,
Required: true,
Name: "log-channel",
Description: "The channel to send log messages and errors to",
ChannelTypes: []dgo.ChannelType{
dgo.ChannelTypeGuildText,
},
}},
}
}
func (c loggerConfigChannel) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error {
opts := getOptions(ic.ApplicationCommandData().Options)
var err error
var dch *dgo.Channel
if c, ok := opts["log-channel"]; ok {
dch = c.ChannelValue(s)
} else {
dch, err = s.Channel(ic.ChannelID)
if err != nil {
return err
}
}
guild, err := c.db.Guild(ic.GuildID)
if err != nil {
return err
}
conf := guild.Config
conf.LoggingChannel = &dch.ID
guild.Config = conf
err = c.db.GuildUpdate(guild)
if err != nil {
return err
}
err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Content: fmt.Sprintf("Logging channel changed to %s", *guild.Config.LoggingChannel),
Flags: dgo.MessageFlagsEphemeral,
},
})
return err
}
func (c loggerConfigChannel) Components() []Component {
return []Component{}
}
func (c loggerConfigChannel) Subcommands() []Command {
return []Command{}
}
type loggerConfigLevel struct {
db gconf.DB
}
func (c loggerConfigLevel) Info() *dgo.ApplicationCommand {
var permissions int64 = dgo.PermissionAdministrator
return &dgo.ApplicationCommand{
Name: "log-level",
Description: "Change logging channel",
DefaultMemberPermissions: &permissions,
Options: []*dgo.ApplicationCommandOption{{
Type: dgo.ApplicationCommandOptionString,
Required: true,
Name: "log-level",
Description: "The logging level of messages and errors",
Choices: []*dgo.ApplicationCommandOptionChoice{
{Name: "Debug", Value: slog.LevelDebug.String()},
{Name: "Info", Value: slog.LevelInfo.String()},
{Name: "Warn", Value: slog.LevelWarn.String()},
{Name: "Error", Value: slog.LevelError.String()},
},
}},
}
}
func (c loggerConfigLevel) Handle(s *dgo.Session, ic *dgo.InteractionCreate) error {
opts := getOptions(ic.ApplicationCommandData().Options)
var err error
opt, ok := opts["log-level"]
if !ok {
return e.New("Parameter log-level is required")
}
var l slog.Level
err = l.UnmarshalText([]byte(opt.StringValue()))
if err != nil {
return e.Join(e.New("Parameter log-level is not a valid value"), err)
}
guild, err := c.db.Guild(ic.GuildID)
if err != nil {
return err
}
conf := guild.Config
conf.LoggingLevel = &l
guild.Config = conf
err = c.db.GuildUpdate(guild)
if err != nil {
return err
}
err = s.InteractionRespond(ic.Interaction, &dgo.InteractionResponse{
Type: dgo.InteractionResponseChannelMessageWithSource,
Data: &dgo.InteractionResponseData{
Content: fmt.Sprintf("Logging level changed to %s", l),
Flags: dgo.MessageFlagsEphemeral,
},
})
return err
}
func (c loggerConfigLevel) Components() []Component {
return []Component{}
}
func (c loggerConfigLevel) Subcommands() []Command {
return []Command{}
}

View File

@@ -1,32 +0,0 @@
package bot
import (
"forge.capytal.company/capytal/dislate/bot/events"
dgo "github.com/bwmarrin/discordgo"
)
func w[E any](h events.EventHandler[E]) interface{} {
return func(s *dgo.Session, ev E) {
err := h.Serve(s, ev)
if err != nil {
err.Log()
err.Send()
err.Reply()
}
}
}
func (b *Bot) registerEventHandlers() {
ehs := []any{
w(events.NewGuildCreate(b.logger, b.db)),
w(events.NewMessageCreate(b.db, b.translator)),
w(events.NewMessageUpdate(b.db, b.translator)),
w(events.NewMessageDelete(b.db)),
w(events.NewReady(b.logger, b.db)),
w(events.NewThreadCreate(b.db, b.translator)),
}
for _, h := range ehs {
b.session.AddHandler(h)
}
}

View File

@@ -1,115 +0,0 @@
package errors
import (
"fmt"
"log/slog"
"reflect"
"strings"
dgo "github.com/bwmarrin/discordgo"
)
type defaultEventErr[E any] struct {
message string
data map[string]any
session *dgo.Session
channelID string
messageReference *dgo.MessageReference
logger *slog.Logger
errs []error
}
func (d *defaultEventErr[E]) Join(errs ...error) EventErr {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &defaultEventErr[E]{
message: d.message,
data: d.data,
session: d.session,
channelID: d.channelID,
messageReference: d.messageReference,
logger: d.logger,
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
func (d *defaultEventErr[E]) Error() string {
var data []string
for k, v := range d.data {
data = append(data, slog.Any(k, v).String())
}
var e string
if d.message != "" {
e = fmt.Sprintf("%s-ERRO: %s %s", d.message, d.Event(), strings.Join(data, " "))
} else {
e = fmt.Sprintf("%s-ERRO: %s", d.Event(), strings.Join(data, " "))
}
var s strings.Builder
_, berr := s.WriteString(e)
if berr != nil {
return "Failed to write error string"
}
for _, err := range d.errs {
_, berr := s.WriteString("\n" + err.Error())
if berr != nil {
return "Failed to write error string"
}
}
return s.String()
}
func (d *defaultEventErr[E]) Event() string {
var e E
t := reflect.TypeOf(e)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
return strings.ToUpper(t.Name())
}
func (d *defaultEventErr[E]) Reply() error {
if d.channelID == "" || d.messageReference == nil || d.session == nil {
return nil
}
_, err := d.session.ChannelMessageSendReply(d.channelID, d.Error(), d.messageReference)
return err
}
func (d *defaultEventErr[E]) Send() error {
if d.channelID == "" || d.session == nil {
return nil
}
_, err := d.session.ChannelMessageSend(d.channelID, d.Error())
return err
}
func (d *defaultEventErr[E]) Log() {
var args []any
for k, v := range d.data {
args = append(args, slog.Any(k, v))
}
d.logger.Error(d.Error(), args...)
}
func (d *defaultEventErr[E]) AddData(key string, v any) {
d.data[key] = v
}

View File

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

View File

@@ -1,42 +0,0 @@
package errors
import (
"log/slog"
dgo "github.com/bwmarrin/discordgo"
)
type GuildErr[E any] struct {
*defaultEventErr[E]
}
func NewGuildErr[E any](
g *dgo.Guild,
log *slog.Logger,
) GuildErr[E] {
return GuildErr[E]{&defaultEventErr[E]{
data: map[string]any{
"GuildID": g.ID,
},
logger: log,
}}
}
type ReadyErr struct {
*defaultEventErr[*dgo.Ready]
}
func NewReadyErr(
ev *dgo.Ready,
log *slog.Logger,
) ReadyErr {
return ReadyErr{&defaultEventErr[*dgo.Ready]{
data: map[string]any{
"SessionID": ev.SessionID,
"BotUserID": ev.User.ID,
"BotUserName": ev.User.Username,
"Guilds": ev.Guilds,
},
logger: log,
}}
}

View File

@@ -1,35 +0,0 @@
package errors
import (
"log/slog"
dgo "github.com/bwmarrin/discordgo"
)
type MessageErr[E any] struct {
*defaultEventErr[E]
}
func NewMessageErr[E any](
s *dgo.Session,
msg *dgo.Message,
log *slog.Logger,
) MessageErr[E] {
var authorID string
if msg.Author != nil {
authorID = msg.Author.ID
}
return MessageErr[E]{&defaultEventErr[E]{
data: map[string]any{
"MessageID": msg.ID,
"ChannelID": msg.ChannelID,
"GuildID": msg.GuildID,
"AuthorID": authorID,
},
session: s,
channelID: msg.ChannelID,
messageReference: msg.Reference(),
logger: log,
}}
}

View File

@@ -1,24 +0,0 @@
package errors
import (
"log/slog"
dgo "github.com/bwmarrin/discordgo"
)
type ThreadCreateErr struct {
*defaultEventErr[*dgo.ThreadCreate]
}
func NewThreadCreateErr(s *dgo.Session, ev *dgo.ThreadCreate, log *slog.Logger) ThreadCreateErr {
return ThreadCreateErr{&defaultEventErr[*dgo.ThreadCreate]{
data: map[string]any{
"ThreadID": ev.ID,
"ParentID": ev.ParentID,
"GuildID": ev.GuildID,
},
session: s,
channelID: ev.ID,
logger: log,
}}
}

View File

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

View File

@@ -1,65 +0,0 @@
package events
import (
e "errors"
"log/slog"
"forge.capytal.company/capytal/dislate/bot/events/errors"
"forge.capytal.company/capytal/dislate/bot/gconf"
gdb "forge.capytal.company/capytal/dislate/guilddb"
dgo "github.com/bwmarrin/discordgo"
)
type GuildCreate struct {
log *slog.Logger
db gconf.DB
}
func NewGuildCreate(log *slog.Logger, db gconf.DB) GuildCreate {
return GuildCreate{log, db}
}
func (h GuildCreate) Serve(s *dgo.Session, ev *dgo.GuildCreate) errors.EventErr {
err := h.db.GuildInsert(gdb.Guild[gconf.ConfigString]{ID: ev.Guild.ID})
everr := errors.NewGuildErr[*dgo.GuildCreate](ev.Guild, h.log)
if err != nil && !e.Is(err, gdb.ErrNoAffect) {
return everr.Join(e.New("Failed to add guild to database"), err)
} else if err != nil {
h.log.Info("Guild already in database", slog.String("id", ev.Guild.ID))
} else {
h.log.Info("Added guild", slog.String("id", ev.Guild.ID))
}
return nil
}
type Ready struct {
log *slog.Logger
db gconf.DB
}
func NewReady(log *slog.Logger, db gconf.DB) EventHandler[*dgo.Ready] {
return Ready{log, db}
}
func (h Ready) Serve(s *dgo.Session, ev *dgo.Ready) errors.EventErr {
everr := errors.NewReadyErr(ev, h.log)
for _, g := range ev.Guilds {
err := h.db.GuildInsert(gdb.Guild[gconf.ConfigString]{ID: g.ID})
if err != nil && !e.Is(err, gdb.ErrNoAffect) {
return everr.Join(err)
} else if err != nil {
h.log.Info("Guild already in database", slog.String("id", g.ID))
} else {
h.log.Info("Added guild", slog.String("id", g.ID))
}
}
return nil
}

View File

@@ -1,440 +0,0 @@
package events
import (
e "errors"
"log/slog"
"slices"
"sync"
"forge.capytal.company/capytal/dislate/bot/events/errors"
"forge.capytal.company/capytal/dislate/bot/gconf"
"forge.capytal.company/capytal/dislate/guilddb"
"forge.capytal.company/capytal/dislate/translator"
dgo "github.com/bwmarrin/discordgo"
)
type MessageCreate struct {
db gconf.DB
translator translator.Translator
}
func NewMessageCreate(db gconf.DB, t translator.Translator) MessageCreate {
return MessageCreate{db, t}
}
func (h MessageCreate) Serve(
s *dgo.Session,
ev *dgo.MessageCreate,
) errors.EventErr {
if ev.Message.Author.Bot || ev.Type != dgo.MessageTypeDefault {
return nil
}
log := gconf.GetLogger(ev.Message.GuildID, s, h.db)
return h.sendMessage(log, s, ev.Message)
}
func (h MessageCreate) sendMessage(
log *slog.Logger,
s *dgo.Session,
msg *dgo.Message,
) errors.EventErr {
everr := errors.NewMessageErr[*dgo.MessageCreate](s, msg, log)
ch, err := h.db.Channel(msg.GuildID, msg.ChannelID)
if e.Is(err, guilddb.ErrNotFound) {
log.Debug("Channel is not in database, ignoring.",
slog.String("guild", msg.GuildID),
slog.String("channel", msg.ChannelID),
slog.String("message", msg.ID),
)
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get channel from database"), err)
}
gc, err := h.db.ChannelGroup(ch.GuildID, ch.ID)
if e.Is(err, guilddb.ErrNotFound) {
log.Debug("Channel is not in a group, ignoring.",
slog.String("guild", msg.GuildID),
slog.String("channel", msg.ChannelID),
slog.String("message", msg.ID),
)
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get channel group from database"), err)
}
_, err = getMessage(h.db, msg, ch.Language)
if err != nil {
return everr.Join(e.New("Failed to get/add message to database"), err)
}
var wg sync.WaitGroup
errs := make(chan errors.EventErr)
for _, c := range gc {
if c.ID == ch.ID && c.GuildID == ch.GuildID {
continue
}
wg.Add(1)
go func(c guilddb.Channel, errs chan<- errors.EventErr) {
defer wg.Done()
everr := errors.NewMessageErr[*dgo.MessageCreate](s, msg, log)
everr.AddData("TranslatedChannelID", c.ID)
dch, err := s.Channel(c.ID)
var channelID string
if err != nil {
errs <- everr.Join(e.New("Failed to get information about translated channel"), err)
return
} else if dch.IsThread() {
channelID = dch.ParentID
} else {
channelID = dch.ID
}
uw, err := getUserWebhook(s, channelID, msg.Author)
if err != nil {
errs <- everr.Join(e.New("Failed to get/set user webhook for translated channel"), err)
return
}
t, err := h.translator.Translate(ch.Language, c.Language, msg.Content)
if err != nil {
errs <- everr.Join(e.New("Error while trying to translate message"), err)
return
}
var tdm *dgo.Message
if dch.IsThread() {
tdm, err = s.WebhookThreadExecute(uw.ID, uw.Token, true, dch.ID, &dgo.WebhookParams{
AvatarURL: msg.Author.AvatarURL(""),
Username: msg.Author.GlobalName,
Content: t,
})
} else {
tdm, err = s.WebhookExecute(uw.ID, uw.Token, true, &dgo.WebhookParams{
AvatarURL: msg.Author.AvatarURL(""),
Username: msg.Author.GlobalName,
Content: t,
})
}
if err != nil {
everr.AddData("WebhookID", uw.ID)
errs <- everr.Join(e.New("Error while trying to execute user webhook"), err)
return
}
if tdm.GuildID == "" {
tdm.GuildID = msg.GuildID
}
_, err = getTranslatedMessage(h.db, tdm, msg, c.Language)
if err != nil {
everr.AddData("WebhookID", uw.ID)
everr.AddData("TranslatedMessageID", uw.ID)
errs <- everr.Join(e.New("Error while trying to add translated message to dabase"), err)
return
}
}(c, errs)
}
wg.Wait()
for err := range errs {
everr.Join(err)
}
if len(errs) > 0 {
return everr
}
return nil
}
type MessageUpdate struct {
db gconf.DB
translator translator.Translator
}
func NewMessageUpdate(db gconf.DB, t translator.Translator) MessageUpdate {
return MessageUpdate{db, t}
}
func (h MessageUpdate) Serve(s *dgo.Session, ev *dgo.MessageUpdate) errors.EventErr {
if ev.Message.Author.Bot || ev.Type != dgo.MessageTypeDefault {
return nil
}
log := gconf.GetLogger(ev.Message.GuildID, s, h.db)
everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log)
msg, err := h.db.Message(ev.Message.GuildID, ev.Message.ChannelID, ev.Message.ID)
if e.Is(err, guilddb.ErrNotFound) {
log.Debug("Message is not in database, ignoring.",
slog.String("guild", ev.Message.GuildID),
slog.String("channel", ev.Message.ChannelID),
)
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get message from database"), err)
}
tmsgs, err := h.db.MessagesWithOrigin(msg.GuildID, msg.ChannelID, msg.ID)
if e.Is(err, guilddb.ErrNotFound) {
log.Debug("No translated message found, ignoring.",
slog.String("guild", ev.GuildID),
slog.String("channel", ev.ChannelID),
)
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get translated messages from database"), err)
}
var wg sync.WaitGroup
errs := make(chan errors.EventErr)
for _, m := range tmsgs {
if m.ID == msg.ID && m.GuildID == msg.GuildID {
continue
}
wg.Add(1)
go func(m guilddb.Message, errs chan<- errors.EventErr) {
defer wg.Done()
everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log)
everr.AddData("TranslatedMessageID", m.ID)
everr.AddData("TranslatedChannelID", m.ChannelID)
var channelID string
if dch, err := s.Channel(m.ChannelID); err != nil {
errs <- everr.Join(e.New("Failed to get information about translated channel"), err)
return
} else if dch.IsThread() {
channelID = dch.ParentID
} else {
channelID = dch.ID
}
uw, err := getUserWebhook(s, channelID, ev.Message.Author)
if err != nil {
errs <- everr.Join(e.New("Failed to get/set user webhook for translated channel"), err)
return
}
t, err := h.translator.Translate(msg.Language, m.Language, ev.Message.Content)
if err != nil {
errs <- everr.Join(e.New("Error while trying to translate message"), err)
return
}
_, err = s.WebhookMessageEdit(uw.ID, uw.Token, m.ID, &dgo.WebhookEdit{
Content: &t,
})
if err != nil {
everr.AddData("WebhookID", uw.ID)
errs <- everr.Join(e.New("Error while trying to execute user webhook"), err)
return
}
}(m, errs)
}
wg.Wait()
for err := range errs {
everr.Join(err)
}
if len(errs) > 0 {
return everr
}
return nil
}
type MessageDelete struct {
db gconf.DB
}
func NewMessageDelete(db gconf.DB) MessageDelete {
return MessageDelete{db}
}
func (h MessageDelete) Serve(s *dgo.Session, ev *dgo.MessageDelete) errors.EventErr {
if ev.Type != dgo.MessageTypeDefault {
return nil
}
log := gconf.GetLogger(ev.Message.GuildID, s, h.db)
everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log)
msg, err := h.db.Message(ev.Message.GuildID, ev.Message.ChannelID, ev.Message.ID)
if e.Is(err, guilddb.ErrNotFound) {
log.Debug("Message is not in database, ignoring.",
slog.String("guild", ev.Message.GuildID),
slog.String("channel", ev.Message.ChannelID),
)
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get message from database"), err)
}
var originChannelID, originID string
if msg.OriginID != nil && msg.OriginChannelID != nil {
oMsg, err := h.db.Message(ev.Message.GuildID, *msg.OriginChannelID, *msg.OriginID)
if err != nil {
originChannelID = *msg.OriginChannelID
originID = *msg.OriginID
} else {
msg = oMsg
originChannelID = oMsg.ChannelID
originID = oMsg.ID
}
} else {
originChannelID = msg.ChannelID
originID = msg.ID
}
tmsgs, err := h.db.MessagesWithOrigin(msg.GuildID, originChannelID, originID)
if e.Is(err, guilddb.ErrNotFound) {
log.Debug("No translated message found, ignoring.",
slog.String("guild", ev.GuildID),
slog.String("channel", ev.ChannelID),
)
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get translated messages from database"), err)
}
for _, m := range tmsgs {
if m.ID == msg.ID && m.ChannelID == msg.ChannelID && m.GuildID == msg.GuildID {
continue
}
go func(m guilddb.Message) {
if err := s.ChannelMessageDelete(m.ChannelID, m.ID); err != nil {
log.Warn("Failed to delete message",
slog.String("channel", m.ChannelID),
slog.String("message", m.ID),
slog.String("err", err.Error()),
)
}
}(m)
}
if err := s.ChannelMessageDelete(msg.ChannelID, msg.ID); err != nil {
log.Warn("Failed to delete message",
slog.String("channel", msg.ChannelID),
slog.String("message", msg.ID),
slog.String("err", err.Error()),
)
}
var wg sync.WaitGroup
errs := make(chan errors.EventErr)
for _, m := range append(tmsgs, msg) {
go func(m guilddb.Message, errs chan<- errors.EventErr) {
everr := errors.NewMessageErr[*dgo.MessageUpdate](s, ev.Message, log)
everr.AddData("TranslatedMessageID", m.ID)
everr.AddData("TranslatedChannelID", m.ChannelID)
err := h.db.MessageDeleteFromChannel(guilddb.NewChannel(m.GuildID, m.ID, translator.EN))
if err != nil && !e.Is(err, guilddb.ErrNoAffect) {
errs <- everr.Join(e.New("Failed to delete message from channel"), err)
return
}
err = h.db.ChannelDelete(guilddb.NewChannel(m.GuildID, m.ID, translator.EN))
if err != nil && !e.Is(err, guilddb.ErrNoAffect) {
errs <- everr.Join(e.New("Failed to delete message thread from channel"), err)
return
}
}(m, errs)
}
wg.Wait()
everrs := make([]error, 0, len(errs))
for err := range errs {
everrs = append(everrs, err)
}
if len(errs) > 0 {
return everr.Join(everrs...)
}
if err := h.db.MessageDelete(guilddb.NewMessage(msg.GuildID, msg.ChannelID, msg.ID, translator.EN)); err != nil {
return everr.Join(e.New("Failed to delete message from database"), err)
}
return nil
}
func getUserWebhook(s *dgo.Session, channelID string, user *dgo.User) (*dgo.Webhook, error) {
whName := "DISLATE_USER_WEBHOOK_" + user.ID
ws, err := s.ChannelWebhooks(channelID)
if err != nil {
return &dgo.Webhook{}, err
}
wi := slices.IndexFunc(ws, func(w *dgo.Webhook) bool {
return w.Name == whName
})
if wi > -1 {
return ws[wi], nil
}
w, err := s.WebhookCreate(channelID, whName, user.AvatarURL(""))
if err != nil {
return &dgo.Webhook{}, err
}
return w, nil
}
func getMessage(db gconf.DB, m *dgo.Message, lang translator.Language) (guilddb.Message, error) {
msg, err := db.Message(m.GuildID, m.ChannelID, m.ID)
if e.Is(err, guilddb.ErrNotFound) {
if err := db.MessageInsert(guilddb.NewMessage(m.GuildID, m.ChannelID, m.ID, lang)); err != nil {
return guilddb.Message{}, err
}
msg, err = db.Message(m.GuildID, m.ChannelID, m.ID)
if err != nil {
return guilddb.Message{}, err
}
}
return msg, nil
}
func getTranslatedMessage(
db gconf.DB,
m, original *dgo.Message,
lang translator.Language,
) (guilddb.Message, error) {
msg, err := db.Message(m.GuildID, m.ChannelID, m.ID)
if e.Is(err, guilddb.ErrNotFound) {
if err := db.MessageInsert(guilddb.NewTranslatedMessage(
m.GuildID,
m.ChannelID,
m.ID,
lang,
original.ChannelID,
original.ID,
)); err != nil {
return guilddb.Message{}, err
}
msg, err = db.Message(m.GuildID, m.ChannelID, m.ID)
if err != nil {
return guilddb.Message{}, err
}
} else if err != nil {
return guilddb.Message{}, err
}
return msg, nil
}

View File

@@ -1,497 +0,0 @@
package events
import (
e "errors"
"log/slog"
"slices"
"sync"
"forge.capytal.company/capytal/dislate/bot/events/errors"
"forge.capytal.company/capytal/dislate/bot/gconf"
"forge.capytal.company/capytal/dislate/translator"
gdb "forge.capytal.company/capytal/dislate/guilddb"
dgo "github.com/bwmarrin/discordgo"
)
type EThreadCreate struct {
db gconf.DB
translator translator.Translator
}
func NewEThreadCreate(db gconf.DB, t translator.Translator) EThreadCreate {
return EThreadCreate{db, t}
}
func (h EThreadCreate) Serve(s *dgo.Session, ev *dgo.ThreadCreate) errors.EventErr {
log := gconf.GetLogger(ev.GuildID, s, h.db)
everr := errors.NewThreadCreateErr(s, ev, log)
parentCh, err := h.db.Channel(ev.GuildID, ev.ParentID)
if e.Is(err, gdb.ErrNotFound) {
log.Debug("Parent channel of thread not in database, ignoring",
slog.String("thread", ev.ID),
slog.String("parent", ev.ParentID),
)
return nil
}
// INFO: Threads have the same ID as the origin message of them
threadMsg, err := h.db.Message(ev.GuildID, ev.ParentID, ev.ID)
var startMsg *dgo.Message
// If no thread message is found in database, it is probably a thread started without
// a source message or a forum post.
if e.Is(err, gdb.ErrNotFound) {
ms, err := s.ChannelMessages(ev.ID, 10, "", "", "")
if err != nil {
return everr.Join(e.New("Failed to get messages of thread"), err)
} else if len(ms) == 0 {
log.Debug("Failed to get messages of thread, empty slice returned, probably created by bot, ignoring",
slog.String("thread", ev.ID),
slog.String("parent", ev.ParentID),
)
return nil
}
threadMsg = gdb.NewMessage(ev.GuildID, ev.ParentID, ev.ID, parentCh.Language)
startMsg = ms[0]
} else if err != nil {
return everr.Join(e.New("Failed to get thread starter message from database"), err)
}
var originMsg gdb.Message
if threadMsg.OriginID != nil && threadMsg.OriginChannelID != nil {
oMsg, err := h.db.Message(ev.GuildID, *threadMsg.OriginChannelID, *threadMsg.OriginID)
if err != nil {
originMsg = threadMsg
} else {
originMsg = oMsg
}
} else {
originMsg = threadMsg
}
dth, err := s.Channel(ev.ID)
if err != nil {
return everr.Join(e.New("Failed to get discord thread"), err)
} else if !dth.IsThread() {
return everr.Join(e.New("Channel is not a thread"))
}
th := gdb.NewChannel(dth.GuildID, dth.ID, threadMsg.Language)
if err := h.db.ChannelInsert(th); e.Is(err, gdb.ErrNoAffect) {
if err = h.db.MessageInsert(threadMsg); err != nil && !e.Is(err, gdb.ErrNoAffect) {
return everr.Join(e.New("Failed to add thread started message to database"), err)
}
return nil
} else if err != nil {
return everr.Join(e.New("Failed to add thread channel to database"), err)
}
parentChannelGroup, err := h.db.ChannelGroup(parentCh.GuildID, parentCh.ID)
if e.Is(err, gdb.ErrNotFound) {
parentChannelGroup = gdb.ChannelGroup{parentCh}
} else if err != nil {
return everr.Join(e.New("Failed to get parent channel group"))
}
var wg sync.WaitGroup
tg := make(chan gdb.Channel, len(parentChannelGroup))
errs := make(chan errors.EventErr)
for _, pc := range parentChannelGroup {
if pc.ID == dth.ParentID {
continue
}
m, err := h.db.MessageWithOriginByLang(pc.GuildID, pc.ID, originMsg.ID, pc.Language)
if e.Is(err, gdb.ErrNotFound) && startMsg != nil {
wg.Add(1)
go func(pc gdb.Channel, tg chan<- gdb.Channel, errs chan<- errors.EventErr) {
defer wg.Done()
everr := errors.NewThreadCreateErr(s, ev, log)
everr.AddData("TranslatedParentID", pc.ID)
parentDCh, err := s.Channel(pc.ID)
if err != nil {
errs <- everr.Join(e.New("Failed to get translated parent channel object"), err)
return
}
content, err := h.translator.Translate(
parentCh.Language,
pc.Language,
startMsg.Content,
)
if err != nil {
errs <- everr.Join(e.New("Failed to translate forum post of thread"), err)
return
}
var dtth *dgo.Channel
var msg *dgo.Message
if parentDCh.Type == dgo.ChannelTypeGuildForum && startMsg != nil {
tags := slices.DeleteFunc(dth.AppliedTags, func(t string) bool {
return !slices.ContainsFunc(
parentDCh.AvailableTags,
func(pt dgo.ForumTag) bool {
return pt.Name == t
},
)
})
dtth, err = s.ForumThreadStartComplex(pc.ID, &dgo.ThreadStart{
Name: dth.Name,
AutoArchiveDuration: dth.ThreadMetadata.AutoArchiveDuration,
Type: dth.Type,
Invitable: dth.ThreadMetadata.Invitable,
RateLimitPerUser: dth.RateLimitPerUser,
AppliedTags: tags,
}, &dgo.MessageSend{
Content: content,
Embeds: startMsg.Embeds,
TTS: startMsg.TTS,
Components: startMsg.Components,
})
if err != nil {
errs <- everr.Join(e.New("Failed to translate forum post of thread"), err)
return
}
msg, err = s.ChannelMessage(dtth.ID, dtth.ID)
if err != nil {
errs <- everr.Join(e.New("Failed to get translated thread starter message"), err)
return
}
} else {
dtth, err = s.ThreadStartComplex(pc.ID, &dgo.ThreadStart{
Name: dth.Name,
AutoArchiveDuration: dth.ThreadMetadata.AutoArchiveDuration,
Type: dth.Type,
Invitable: dth.ThreadMetadata.Invitable,
RateLimitPerUser: dth.RateLimitPerUser,
})
if err != nil {
errs <- everr.Join(e.New("Failed to create thread"), err)
return
}
uw, err := getUserWebhook(s, pc.ID, startMsg.Author)
if err != nil {
errs <- everr.Join(e.New("Failed to get/set user webhook for parent channel of translated thread"), err)
return
}
msg, err = s.WebhookThreadExecute(uw.ID, uw.Token, true, dtth.ID, &dgo.WebhookParams{
AvatarURL: startMsg.Author.AvatarURL(""),
Username: startMsg.Author.GlobalName,
Content: content,
})
if err != nil {
errs <- everr.Join(e.New("Error while trying to execute user webhook"), err)
return
}
}
if err := h.db.ChannelInsert(gdb.NewChannel(dtth.GuildID, dtth.ID, pc.Language)); err != nil &&
!e.Is(err, gdb.ErrNoAffect) {
everr.AddData("TranslatedThreadID", dtth.ID)
errs <- everr.Join(e.New("Failed to add translated thread to database"), err)
return
}
err = h.db.MessageInsert(
gdb.NewTranslatedMessage(
dtth.GuildID,
dtth.ID,
msg.ID,
pc.Language,
startMsg.ChannelID,
startMsg.ID,
),
)
if err != nil {
errs <- everr.Join(e.New("Failed to add translated thread starter message to database"), err)
return
}
tg <- gdb.NewChannel(dtth.GuildID, dtth.ID, pc.Language)
}(
pc,
tg,
errs,
)
} else if err != nil {
return everr.Join(e.New("Failed to get thread translated start message"), err)
} else {
wg.Add(1)
go func(m gdb.Message, tg chan<- gdb.Channel, errs chan<- errors.EventErr) {
defer wg.Done()
everr := errors.NewThreadCreateErr(s, ev, log)
everr.AddData("TranslatedParentID", m.ChannelID)
dtth, err := s.MessageThreadStartComplex(
m.ChannelID,
m.ID,
&dgo.ThreadStart{
Name: dth.Name,
AutoArchiveDuration: dth.ThreadMetadata.AutoArchiveDuration,
Type: dth.Type,
Invitable: dth.ThreadMetadata.Invitable,
RateLimitPerUser: dth.RateLimitPerUser,
AppliedTags: dth.AppliedTags,
},
)
if err != nil {
errs <- everr.Join(e.New("Failed to create translated thread"), err)
return
}
everr.AddData("TranslatedThreadID", pc.ID)
if err := h.db.ChannelInsert(gdb.NewChannel(dtth.GuildID, dtth.ID, m.Language)); err != nil &&
!e.Is(err, gdb.ErrNoAffect) {
errs <- everr.Join(e.New("Failed to add translated thread to database"), err)
return
}
tg <- gdb.NewChannel(dtth.GuildID, dtth.ID, m.Language)
}(m, tg, errs)
}
}
wg.Wait()
everrs := make([]error, 0, len(errs))
for err := range errs {
everrs = append(everrs, err)
}
if len(errs) > 0 {
return everr.Join(everrs...)
}
var threadGroup gdb.ChannelGroup
for t := range tg {
threadGroup = append(threadGroup, t)
}
if err := h.db.ChannelGroupInsert(threadGroup); err != nil {
return everr.Join(e.New("Failed to add group of threads to database"), err)
}
thMsgs, err := s.ChannelMessages(th.ID, 10, "", "", "")
if err != nil {
return everr.Join(e.New("Failed to get thread messages"), err)
}
for _, m := range thMsgs {
if startMsg != nil && m.ID == startMsg.ID {
continue
}
if m.Content != "" {
m.GuildID = th.GuildID
NewMessageCreate(h.db, h.translator).sendMessage(log, s, m)
}
}
return nil
}
type ThreadCreate struct {
db gconf.DB
translator translator.Translator
session *dgo.Session
thread *dgo.Channel
originLang translator.Language
}
func NewThreadCreate(db gconf.DB, t translator.Translator) ThreadCreate {
return ThreadCreate{db, t, nil, nil, translator.EN}
}
func (h ThreadCreate) Serve(s *dgo.Session, ev *dgo.ThreadCreate) errors.EventErr {
log := gconf.GetLogger(ev.GuildID, s, h.db)
everr := errors.NewThreadCreateErr(s, ev, log)
parentCh, err := h.db.Channel(ev.GuildID, ev.ParentID)
if e.Is(err, gdb.ErrNotFound) {
log.Debug("Parent channel of thread not in database, ignoring",
slog.String("ThreadID", ev.ID),
slog.String("ParentID", ev.ParentID))
return nil
}
ms, err := s.ChannelMessages(ev.ID, 10, "", "", "")
if err != nil {
return everr.Join(e.New("Failed to get messages of thread"), err)
} else if len(ms) == 0 || (len(ms) == 1 && ms[0].Type == dgo.MessageTypeThreadStarterMessage) {
log.Debug("No messages found in thread, probably created by bot, ignoring",
slog.String("ThreadID", ev.ID),
slog.String("ParentID", ev.ParentID))
return nil
}
// INFO: Threads have the same ID as their starter messages
starterMsg, err := h.db.Message(parentCh.GuildID, parentCh.ID, ev.ID)
if e.Is(err, gdb.ErrNotFound) {
starterMsg = gdb.NewMessage(parentCh.GuildID, ev.ID, ev.ID, parentCh.Language)
err = h.db.MessageInsert(starterMsg)
if err != nil {
return everr.Join(e.New("Failed to add starter message to database"), err)
}
}
thread, err := s.Channel(starterMsg.ID)
if err != nil {
return everr.Join(e.New("Failed to get thread from discord"), err)
} else if !thread.IsThread() {
return everr.Join(e.New("Failed to get thread from discord, thread is not a thread somehow"), err)
}
parentChannelGroup, err := h.db.ChannelGroup(parentCh.GuildID, parentCh.ID)
if e.Is(err, gdb.ErrNotFound) {
log.Debug("Parent channel not in a group, ignoring",
slog.String("ThreadID", ev.ID),
slog.String("ParentID", ev.ParentID))
return nil
} else if err != nil {
return everr.Join(e.New("Failed to get parent channel group"))
}
var wg sync.WaitGroup
tg := make(chan gdb.Channel)
errs := make(chan error)
h.session = s
h.originLang = parentCh.Language
h.thread = thread
for _, pc := range parentChannelGroup {
if pc.ID == ev.ParentID {
continue
}
wg.Add(1)
go func(tg chan<- gdb.Channel, errs chan<- error) {
defer wg.Done()
t, err := h.startTranslatedThread(pc, starterMsg)
tg <- t
if err != nil {
errs <- err
}
log.Debug("FINISHED")
}(tg, errs)
}
wg.Wait()
everrs := make([]error, 0, len(errs))
for err := range errs {
log.Debug("ERR")
everrs = append(everrs, err)
}
if len(errs) > 0 {
log.Debug("ERR RETURN")
return everr.Join(everrs...)
}
log.Debug("FUNCTION 1")
if err := h.db.ChannelInsert(gdb.NewChannel(thread.GuildID, thread.ID, parentCh.Language)); err != nil {
return everr.Join(e.New("Failed to add thread channel to database"), err)
}
threadGroup := make(gdb.ChannelGroup, 0, len(tg))
for t := range tg {
threadGroup = append(threadGroup, t)
}
log.Debug("FUNCTION 2")
if err := h.db.ChannelGroupInsert(threadGroup); err != nil {
return everr.Join(e.New("Failed to add group of thread to database"), err)
}
thMsgs, err := s.ChannelMessages(thread.ID, 10, "", "", "")
if err != nil {
return everr.Join(e.New("Failed to get thread messages"), err)
}
log.Debug("FUNCTION 3")
for _, m := range thMsgs {
m.GuildID = thread.GuildID
err := NewMessageCreate(h.db, h.translator).sendMessage(log, s, m)
if err != nil {
return everr.Join(e.New("Failed to translate thread messages"), err)
}
}
return nil
}
func (h ThreadCreate) startTranslatedThread(
pc gdb.Channel,
sm gdb.Message,
) (gdb.Channel, error) {
if sm.OriginChannelID != nil && *sm.OriginChannelID == pc.ID {
m, err := h.db.Message(sm.GuildID, *sm.OriginChannelID, *sm.OriginID)
if err != nil {
return gdb.Channel{}, e.Join(
e.New("Failed to get origin message of starter message"),
err,
)
}
return h.startTranslatedMessageThread(m)
}
m, err := h.db.MessageWithOriginByLang(sm.GuildID, sm.ChannelID, sm.ID, pc.Language)
if e.Is(err, gdb.ErrNotFound) {
} else if err != nil {
return gdb.Channel{}, e.Join(
e.New("Failed to get translated message of starter message"),
err,
)
}
return h.startTranslatedMessageThread(m)
}
func (h ThreadCreate) startTranslatedMessageThread(
m gdb.Message,
) (gdb.Channel, error) {
name, err := h.translator.Translate(h.originLang, m.Language, h.thread.Name)
if err != nil {
return gdb.Channel{}, e.Join(e.New("Failed to translate thread name"), err)
}
th, err := h.session.MessageThreadStartComplex(m.ChannelID, m.ID, &dgo.ThreadStart{
Name: name,
AutoArchiveDuration: h.thread.ThreadMetadata.AutoArchiveDuration,
Type: h.thread.Type,
Invitable: h.thread.ThreadMetadata.Invitable,
RateLimitPerUser: h.thread.RateLimitPerUser,
AppliedTags: h.thread.AppliedTags,
})
if err != nil {
return gdb.Channel{}, e.Join(e.New("Failed to create thread"), err)
}
c := gdb.NewChannel(th.GuildID, th.ID, m.Language)
if err := h.db.ChannelInsert(c); err != nil {
return c, e.Join(e.New("Failed to insert thread on database"), err)
}
return c, nil
}

View File

@@ -1,63 +0,0 @@
package gconf
import (
"log/slog"
gdb "forge.capytal.company/capytal/dislate/guilddb"
dgo "github.com/bwmarrin/discordgo"
)
type Config struct {
Logger *slog.Logger
}
type ConfigString struct {
LoggingChannel *string `json:"logging_channel"`
LoggingLevel *slog.Level `json:"logging_level"`
}
type (
Guild gdb.Guild[ConfigString]
DB gdb.GuildDB[ConfigString]
)
func (g Guild) GetConfig(s *dgo.Session) (*Config, error) {
var l *slog.Logger
var err error
if g.Config.LoggingChannel != nil {
c, err := s.Channel(*g.Config.LoggingChannel)
if err != nil {
return nil, err
}
var lv slog.Level
if g.Config.LoggingLevel != nil {
lv = *g.Config.LoggingLevel
} else {
lv = slog.LevelInfo
}
l = slog.New(NewGuildHandler(s, c, &slog.HandlerOptions{
Level: lv,
}))
} else {
l = slog.New(disabledHandler{})
}
return &Config{l}, err
}
func GetLogger(guildID string, s *dgo.Session, db DB) *slog.Logger {
g, err := db.Guild(guildID)
if err != nil {
return slog.New(disabledHandler{})
}
c, err := Guild(g).GetConfig(s)
if err != nil {
return slog.New(disabledHandler{})
}
return c.Logger
}

View File

@@ -1,46 +0,0 @@
package gconf
import (
"context"
"log/slog"
dgo "github.com/bwmarrin/discordgo"
)
type guildHandler struct {
*slog.TextHandler
}
func NewGuildHandler(s *dgo.Session, c *dgo.Channel, opts *slog.HandlerOptions) guildHandler {
w := NewChannelWriter(s, c)
h := slog.NewTextHandler(w, opts)
return guildHandler{h}
}
type disabledHandler struct {
*slog.TextHandler
}
func (_ disabledHandler) Enabled(_ context.Context, _ slog.Level) bool {
return false
}
type channelWriter struct {
session *dgo.Session
channel *dgo.Channel
}
func NewChannelWriter(s *dgo.Session, c *dgo.Channel) channelWriter {
w := channelWriter{s, c}
return w
}
func (w channelWriter) Write(p []byte) (int, error) {
m, err := w.session.ChannelMessageSend(w.channel.ID, string(p))
if err != nil {
return 0, err
}
return len(m.Content), nil
}

View File

@@ -1,163 +0,0 @@
package guilddb
import (
"errors"
"forge.capytal.company/capytal/dislate/translator"
)
type Guild[C any] struct {
ID string
Config C
}
func NewGuild[C any](ID string, config C) Guild[C] {
return Guild[C]{ID, config}
}
type Channel struct {
GuildID string
ID string
Language translator.Language
}
func NewChannel(GuildID, ID string, lang translator.Language) Channel {
return Channel{GuildID, ID, lang}
}
type ChannelGroup []Channel
type Message struct {
GuildID string
ChannelID string
ID string
Language translator.Language
OriginChannelID *string
OriginID *string
}
func NewMessage(GuildID, ChannelID, ID string, lang translator.Language) Message {
return Message{GuildID, ChannelID, ID, lang, nil, nil}
}
func NewTranslatedMessage(
GuildID, ChannelID, ID string,
lang translator.Language,
OriginChannelID, OriginID string,
) Message {
return Message{GuildID, ChannelID, ID, lang, &OriginChannelID, &OriginID}
}
type GuildDB[C any] interface {
// Selects and returns a Message from the database, based on the
// key pair of Channel's ID and Message's ID.
//
// Will return ErrNotFound if no message is found or ErrInternal.
Message(guildID, channelID, ID string) (Message, error)
// Returns a slice of Messages with the provided Message.OriginChannelID and Message.OriginID.
//
// Will return ErrNotFound if no message is found (slice's length == 0) or ErrInternal.
MessagesWithOrigin(guildID, originChannelID, originID string) ([]Message, error)
// Returns a Messages with the provided Message.OriginChannelID, Message.OriginID
// and Message.Language.
//
// Will return ErrNotFound if no message is found or ErrInternal.
MessageWithOriginByLang(
guildID, originChannelId, originId string,
language translator.Language,
) (Message, error)
// Inserts a new Message object in the database.
//
// Message.ChannelID and Message.ID must be a unique pair and not already
// in the database. Implementations of this function may require Message.ChannelID
// to be an already stored Channel object, in the case that it isn't stored,
// ErrPreconditionFailed may be returned.
//
// Message.OriginID and Message.OriginChannelID should not be nil if the message
// is a translated one.
//
// Will return ErrNoAffect if the object already exists or ErrInternal.
MessageInsert(m Message) error
// Updates the Message object in the database. Message.ID and Message.ChannelID
// are used to find the correct message.
//
// Will return ErrNoAffect if no object was updated or ErrInternal.
MessageUpdate(m Message) error
// Deletes the Message object in the database. Message.ID and Message.ChannelID
// are used to find the correct message.
//
// Will return ErrNoAffect if no object was deleted or ErrInternal.
MessageDelete(m Message) error
// Deletes all messages in a Channel in the database. Channel.ID is used to find
// the correct messages.
//
// Will return ErrNoAffect if no object was deleted or ErrInternal.
MessageDeleteFromChannel(c Channel) error
// Selects and returns a Channel from the database, based on the
// ID provided.
//
// Will return ErrNotFound if no channel is found or ErrInternal.
Channel(guildID, ID string) (Channel, error)
// Inserts a new Channel object in the database.
//
// Channel.ID must be unique and not already in the database.
//
// Will return ErrNoAffect if the object already exists or ErrInternal.
ChannelInsert(c Channel) error
// Updates the Channel object in the database. Channel.ID is used to find the
// correct Channel.
//
// Will return ErrNoAffect if no object was updated or ErrInternal.
ChannelUpdate(c Channel) error
// Deletes the Channel object in the database. Channel.ID is used to find the
// correct Channel.
//
// Will return ErrNoAffect if no object was deleted or ErrInternal.
ChannelDelete(c Channel) error
// Selects and returns a ChannelGroup from the database. Finds a ChannelGroup
// that has a Channel if the provided ID.
//
// Channels cannot be in two ChannelGroup at the same time.
//
// Will return ErrNotFound if no channel is found or ErrInternal.
ChannelGroup(guildID, ID string) (ChannelGroup, error)
// Inserts a new ChannelGroup object in the database. ChannelGroup must be unique
// and not have Channels that are already in other groups.
//
// Will return ErrNoAffect if the object already exists or ErrInternal.
ChannelGroupInsert(g ChannelGroup) error
// Updates the ChannelGroup object in the database.
//
// Will return ErrNoAffect if no object was updated or ErrInternal.
ChannelGroupUpdate(g ChannelGroup) error
// Deletes the ChannelGroup object in the database.
//
// Will return ErrNoAffect if no object was deleted or ErrInternal.
ChannelGroupDelete(g ChannelGroup) error
// Selects and returns a Guild from the database.
//
// Will return ErrNotFound if no Guild is found or ErrInternal.
Guild(ID string) (Guild[C], error)
// Inserts a new Guild object in the database. Guild.ID must be unique and
// not already in the database.
//
// Will return ErrNoAffect if the object already exists or ErrInternal.
GuildInsert(g Guild[C]) error
// Delete a Guild from the database. Guild.ID is used to find the object.
//
// Will return ErrNoAffect if no object was deleted or ErrInternal.
GuildDelete(g Guild[C]) error
// Updates the Guild object in the database.
//
// Will return ErrNoAffect if no object was updated or ErrInternal.
GuildUpdate(g Guild[C]) error
}
var (
ErrNoAffect = errors.New("Not able to affect anything in the database")
ErrNotFound = errors.New("Object not found in the database")
ErrPreconditionFailed = errors.New("Precondition failed")
ErrInvalidObject = errors.New("Invalid object")
ErrInternal = errors.New("Internal error while trying to use database")
ErrConfigParsing = errors.New("Error while parsing Guild's config")
)

View File

@@ -1,564 +0,0 @@
package guilddb
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"forge.capytal.company/capytal/dislate/translator"
_ "github.com/tursodatabase/go-libsql"
)
type SQLiteDB[C any] struct {
sql *sql.DB
}
func NewSQLiteDB[C any](file string) (*SQLiteDB[C], error) {
db, err := sql.Open("libsql", file)
if err != nil {
return &SQLiteDB[C]{}, err
}
db.SetMaxOpenConns(1)
return &SQLiteDB[C]{db}, nil
}
func (db *SQLiteDB[C]) Close() error {
return db.sql.Close()
}
func (db *SQLiteDB[C]) Prepare() error {
if _, err := db.sql.Exec(`
CREATE TABLE IF NOT EXISTS guilds (
ID text NOT NULL,
Config text NOT NULL,
PRIMARY KEY(ID)
);
`); err != nil {
return errors.Join(ErrInternal, err)
}
if _, err := db.sql.Exec(`
CREATE TABLE IF NOT EXISTS channels (
GuildID text NOT NULL,
ID text NOT NULL,
Language text NOT NULL,
PRIMARY KEY(ID, GuildID),
FOREIGN KEY(GuildID) REFERENCES guilds(ID)
);
`); err != nil {
return errors.Join(ErrInternal, err)
}
if _, err := db.sql.Exec(`
CREATE TABLE IF NOT EXISTS channelGroups (
GuildID text NOT NULL,
Channels text NOT NULL,
PRIMARY KEY(Channels, GuildID),
FOREIGN KEY(GuildID) REFERENCES guilds(ID)
);
`); err != nil {
return errors.Join(ErrInternal, err)
}
if _, err := db.sql.Exec(`
CREATE TABLE IF NOT EXISTS messages (
GuildID text NOT NULL,
ChannelID text NOT NULL,
ID text NOT NULL,
Language text NOT NULL,
OriginChannelID text,
OriginID text,
PRIMARY KEY(ID, ChannelID, GuildID),
FOREIGN KEY(GuildID, ChannelID) REFERENCES channels(GuildID, ID),
FOREIGN KEY(GuildID, OriginChannelID, OriginID) REFERENCES messages(GuildID, ChannelID, ID)
);
`); err != nil {
return errors.Join(ErrInternal, err)
}
return nil
}
func (db *SQLiteDB[C]) Message(guildID, channelID, messageID string) (Message, error) {
return db.selectMessage(`
WHERE "GuildID" = $1 AND "ChannelID" = $2 AND "ID" = $3
`, guildID, channelID, messageID)
}
func (db *SQLiteDB[C]) MessagesWithOrigin(
guildID, originChannelID, originID string,
) ([]Message, error) {
return db.selectMessages(`
WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3
`, guildID, originChannelID, originID)
}
func (db *SQLiteDB[C]) MessageWithOriginByLang(
guildID, originChannelID, originID string,
language translator.Language,
) (Message, error) {
return db.selectMessage(`
WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3 AND "Language" = $4
`, guildID, originChannelID, originID, language)
}
func (db *SQLiteDB[C]) MessageInsert(m Message) error {
_, err := db.Channel(m.GuildID, m.ChannelID)
if errors.Is(err, ErrNotFound) {
return errors.Join(
ErrPreconditionFailed,
fmt.Errorf("Channel %s doesn't exists in the database", m.ChannelID),
)
} else if err != nil {
return errors.Join(
ErrInternal,
errors.New("Failed to check if Channel exists in the database"),
err,
)
}
r, err := db.sql.Exec(`
INSERT OR IGNORE INTO messages (GuildID, ChannelID, ID, Language, OriginChannelID, OriginID)
VALUES ($1, $2, $3, $4, $5, $6)
`, m.GuildID, m.ChannelID, m.ID, m.Language, m.OriginChannelID, m.OriginID)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) MessageUpdate(m Message) error {
r, err := db.sql.Exec(`
UPDATE messages
SET Language = $1, OriginChannelID = $2, OriginID = $3
WHERE "GuildID" = $4 AND "ChannelID" = $5 AND "ID" = $6
`, m.Language,
m.OriginChannelID,
m.OriginID,
m.GuildID,
m.ChannelID,
m.ID,
)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) MessageDelete(m Message) error {
_, err := db.sql.Exec(`
DELETE FROM messages
WHERE "GuildID" = $1 AND "OriginChannelID" = $2 AND "OriginID" = $3
`, m.GuildID, m.ChannelID, m.ID)
if err != nil && !errors.Is(err, ErrNoAffect) {
return errors.Join(ErrInternal, err)
}
r, err := db.sql.Exec(`
DELETE FROM messages
WHERE "GuildID" = $1 AND "ChannelID" = $2 AND "ID" = $3
`, m.GuildID, m.ChannelID, m.ID)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) MessageDeleteFromChannel(c Channel) error {
r, err := db.sql.Exec(`
DELETE FROM messages
WHERE "GuildID" = $1 AND "ChannelID" = $2
`, c.GuildID, c.ID)
if err != nil && !errors.Is(err, ErrNoAffect) {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) selectMessage(query string, args ...any) (Message, error) {
var m Message
err := db.sql.QueryRow(fmt.Sprintf(`
SELECT GuildID, ChannelID, ID, Language, OriginChannelID, OriginID FROM messages
%s
`, query), args...).
Scan(&m.GuildID, &m.ChannelID, &m.ID, &m.Language, &m.OriginChannelID, &m.OriginID)
if errors.Is(err, sql.ErrNoRows) {
return m, errors.Join(ErrNotFound, err)
} else if err != nil {
return m, errors.Join(ErrInternal, err)
}
return m, nil
}
func (db *SQLiteDB[C]) selectMessages(query string, args ...any) ([]Message, error) {
r, err := db.sql.Query(fmt.Sprintf(`
SELECT GuildID, ChannelID, ID, Language, OriginChannelID, OriginID FROM messages
%s
`, query), args...)
defer r.Close()
if err != nil {
return []Message{}, errors.Join(ErrInternal, err)
}
var ms []Message
for r.Next() {
var m Message
err = r.Scan(&m.GuildID, &m.ChannelID, &m.ID, &m.Language, &m.OriginChannelID, &m.OriginID)
if err != nil {
return ms, errors.Join(
ErrInternal,
fmt.Errorf("Query: %s\nArguments: %v", query, args),
err,
)
}
ms = append(ms, m)
}
if len(ms) == 0 {
return ms, errors.Join(
ErrNotFound,
fmt.Errorf("Query: %s\nArguments: %v", query, args),
)
}
return ms, err
}
func (db *SQLiteDB[C]) Channel(guildID, ID string) (Channel, error) {
return db.selectChannel(`
WHERE "GuildID" = $1 AND "ID" = $2
`, guildID, ID)
}
func (db *SQLiteDB[C]) ChannelInsert(c Channel) error {
r, err := db.sql.Exec(`
INSERT OR IGNORE INTO channels (GuildID, ID, Language)
VALUES ($1, $2, $3)
`, c.GuildID, c.ID, c.Language)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) ChannelUpdate(c Channel) error {
r, err := db.sql.Exec(`
UPDATE channels
SET Language = $1
WHERE "GuildID" = $2 AND "ID" = $3
`, c.Language, c.GuildID, c.ID)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) ChannelDelete(c Channel) error {
r, err := db.sql.Exec(`
DELETE FROM channels
WHERE "GuildID" = $1 AND "ID" = $2
`, c.ID, c.ID)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) ChannelGroup(guildID, channelID string) (ChannelGroup, error) {
var j string
err := db.sql.QueryRow(fmt.Sprintf(`
SELECT Channels FROM channelGroups, json_each(Channels)
WHERE "GuildID" = $1 AND json_each.value='%s';
`, channelID), guildID).Scan(&j)
if errors.Is(err, sql.ErrNoRows) {
return ChannelGroup{}, errors.Join(ErrNotFound, err)
} else if err != nil {
return ChannelGroup{}, errors.Join(ErrInternal, err)
}
var ids []string
err = json.Unmarshal([]byte(j), &ids)
if err != nil {
return ChannelGroup{}, errors.Join(ErrInternal, err)
}
for i, v := range ids {
ids[i] = fmt.Sprintf("\"ID\" = %s", v)
}
cs, err := db.selectChannels(fmt.Sprintf(`
WHERE %s AND "GuildID" = $1
`, strings.Join(ids, " OR ")), guildID)
if errors.Is(err, ErrNotFound) || len(cs) != len(ids) {
return ChannelGroup{}, errors.Join(
ErrPreconditionFailed,
fmt.Errorf(
"ChannelGroup has Channels that doesn't exist in the database, group: %s",
ids,
),
err,
)
} else if err != nil {
return ChannelGroup{}, errors.Join(ErrInternal, err)
}
return cs, nil
}
func (db *SQLiteDB[C]) ChannelGroupInsert(g ChannelGroup) error {
if len(g) == 0 {
return ErrNoAffect
}
var ids []string
for _, c := range g {
ids = append(ids, c.ID)
}
slices.Sort(ids)
j, err := json.Marshal(ids)
if err != nil {
return errors.Join(ErrInternal, err)
}
r, err := db.sql.Exec(fmt.Sprintf(`
INSERT OR IGNORE INTO channelGroups (GuildID, Channels)
VALUES ($1, json('%s'))
`, string(j)), g[0].GuildID)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) ChannelGroupUpdate(g ChannelGroup) error {
if len(g) != 0 {
return nil
}
var ids, idsq []string
for _, c := range g {
ids = append(ids, c.ID)
idsq = append(idsq, "json_each.value='"+c.ID+"'")
}
slices.Sort(ids)
r, err := db.sql.Exec(
fmt.Sprintf(`
UPDATE channelGroups, json_each(Channels)
SET Channels = $1
WHERE %s AND "GuildID" = $2
`, strings.Join(idsq, " OR ")),
strings.Join(ids, ","),
g[0].GuildID,
)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) ChannelGroupDelete(g ChannelGroup) error {
if len(g) != 0 {
return nil
}
var ids, idsq []string
for _, c := range g {
ids = append(ids, c.ID)
idsq = append(idsq, "json_each.value='"+c.ID+"'")
}
slices.Sort(ids)
r, err := db.sql.Exec(
fmt.Sprintf(`
DELETE FROM channelGroups, json_each(Channels)
WHERE %s AND "GuildID" = $2
`, strings.Join(idsq, " OR ")),
g[0].GuildID,
)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) selectChannel(query string, args ...any) (Channel, error) {
var c Channel
err := db.sql.QueryRow(fmt.Sprintf(`
SELECT GuildID, ID, Language FROM channels
%s
`, query), args...).Scan(&c.GuildID, &c.ID, &c.Language)
if errors.Is(err, sql.ErrNoRows) {
return c, errors.Join(ErrNotFound, err)
} else if err != nil {
return c, errors.Join(ErrInternal, err)
}
return c, nil
}
func (db *SQLiteDB[C]) selectChannels(query string, args ...any) ([]Channel, error) {
r, err := db.sql.Query(fmt.Sprintf(`
SELECT GuildID, ID, Language FROM channels
%s
`, query), args...)
defer r.Close()
if err != nil {
return []Channel{}, errors.Join(ErrInternal, err)
}
var cs []Channel
for r.Next() {
var c Channel
err = r.Scan(&c.GuildID, &c.ID, &c.Language)
if err != nil {
return cs, errors.Join(
ErrInternal,
fmt.Errorf("Query: %s\nArguments: %v", query, args),
err,
)
}
cs = append(cs, c)
}
if len(cs) == 0 {
return cs, errors.Join(
ErrNotFound,
fmt.Errorf("Query: %s\nArguments: %v", query, args),
)
}
return cs, err
}
func (db *SQLiteDB[C]) Guild(ID string) (Guild[C], error) {
var g struct {
ID string
Config string
}
if err := db.sql.QueryRow(`
SELECT "ID", "Config" FROM guilds
WHERE "ID" = $1
`, ID).Scan(&g.ID, &g.Config); errors.Is(err, sql.ErrNoRows) {
return Guild[C]{}, errors.Join(ErrNotFound, err)
} else if err != nil {
return Guild[C]{}, errors.Join(ErrInternal, err)
}
var c C
err := json.Unmarshal([]byte(g.Config), &c)
if err != nil {
return Guild[C]{}, errors.Join(ErrConfigParsing, err)
}
return Guild[C]{g.ID, c}, nil
}
func (db *SQLiteDB[C]) GuildInsert(g Guild[C]) error {
j, err := json.Marshal(g.Config)
if err != nil {
return errors.Join(ErrConfigParsing, err)
}
r, err := db.sql.Exec(`
INSERT OR IGNORE INTO guilds (ID, Config)
VALUES ($1, $2)
`, g.ID, string(j))
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) GuildUpdate(g Guild[C]) error {
j, err := json.Marshal(g.Config)
if err != nil {
return errors.Join(ErrConfigParsing, err)
}
r, err := db.sql.Exec(fmt.Sprintf(`
UPDATE guilds
SET "Config" = '%s'
WHERE "ID" = '%s'
`, string(j), g.ID))
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}
func (db *SQLiteDB[C]) GuildDelete(g Guild[C]) error {
r, err := db.sql.Exec(`
DELETE FROM guilds
WHERE "ID" = $1
`, g.ID)
if err != nil {
return errors.Join(ErrInternal, err)
} else if rows, _ := r.RowsAffected(); rows == 0 {
return ErrNoAffect
}
return nil
}