diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..fcb6b32 --- /dev/null +++ b/commands.go @@ -0,0 +1,316 @@ +package main + +import ( + "github.com/bwmarrin/discordgo" + "github.com/bwmarrin/lit" + "strings" + "time" +) + +var ( + // Commands + commands = []*discordgo.ApplicationCommand{ + { + Name: "say", + Description: "Says text out loud", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "text", + Description: "Text to say out loud", + Required: true, + }, + }, + }, + { + Name: "bestemmia", + Description: "Generates a bestemmia n times", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionInteger, + Name: "n", + Description: "Number of times to generate bestemmia", + Required: false, + }, + }, + }, + { + Name: "treno", + Description: "Fakes train announcement given it's number", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "train", + Description: "Train number", + Required: true, + }, + }, + }, + { + Name: "covid", + Description: "Says covid data out loud for current day in Italy", + }, + { + Name: "preghiera", + Description: "Randomly select a custom command", + }, + { + Name: "stop", + Description: "Stops every command", + }, + { + Name: "addcustom", + Description: "Creates a custom command. will be replace with a random god and with an adjective", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "customCommand", + Description: "Command name", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "text", + Description: "Text to say out loud", + Required: true, + }, + }, + }, + { + Name: "rmcustom", + Description: "Removes a custom command", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "customCommand", + Description: "Command name to remove", + Required: true, + }, + }, + }, + { + Name: "custom", + Description: "Calls a custom command. Use /listcustom for a list", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "customCommand", + Description: "Command name to remove", + Required: true, + }, + }, + }, + { + Name: "listcustom", + Description: "Listes all custom command for the server", + }, + } + + // Handler + commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + // Generates random bestemmie + "bestemmia": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + vs := findUserVoiceState(s, i.Member.User.ID, i.GuildID) + if vs == nil { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Error", "You're not in a voice channel in this guild!"). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + return + } + + // Locks the mutex for the current server + server[vs.GuildID].mutex.Lock() + + // Join the provided voice channel. + vc, err := s.ChannelVoiceJoin(vs.GuildID, vs.ChannelID, false, true) + if err != nil { + lit.Error("Can't connect to voice channel, %s", err) + server[vs.GuildID].mutex.Unlock() + return + } + + // If a number is given, we repeat the bestemmia n times + if len(i.Data.Options) > 0 { + var ( + cont uint64 + n = i.Data.Options[0].UintValue() + ) + + for cont = 0; cont < n; cont++ { + if server[vs.GuildID].stop { + bstm := bestemmia() + + if cont == 0 { + sendEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Bestemmia", bstm). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + } else { + modfyInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Bestemmia", bstm). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + } + + playSound2(genAudio(strings.ToUpper(bstm)), vc, s) + } else { + // Resets the stop boolean + server[vs.GuildID].stop = true + break + } + } + } else { + // Else, we only do the command once + bstm := bestemmia() + + sendEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Bestemmia", bstm). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + + playSound2(genAudio(strings.ToUpper(bstm)), vc, s) + } + + // Deletes interaction as we have finished + err = s.InteractionResponseDelete(s.State.User.ID, i.Interaction) + if err != nil { + lit.Error("InteractionResponseDelete failed: %s", err.Error()) + } + + // Disconnect from the provided voice channel. + err = vc.Disconnect() + if err != nil { + lit.Error("Can't disconnect from voice channel, %s", err) + } + + // Releases the mutex lock for the server + server[vs.GuildID].mutex.Unlock() + }, + + // Says text out lout + "say": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + vs := findUserVoiceState(s, i.Member.User.ID, i.GuildID) + if vs != nil { + text := i.Data.Options[0].StringValue() + + sendEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Say", text). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + + playSound(s, vs.GuildID, vs.ChannelID, genAudio(emojiToDescription(text))) + + err := s.InteractionResponseDelete(s.State.User.ID, i.Interaction) + if err != nil { + lit.Error("InteractionResponseDelete failed: %s", err.Error()) + } + } else { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Error", "You're not in a voice channel in this guild!"). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + } + }, + + // Stops all commands + "stop": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + server[i.GuildID].stop = false + + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Stop", "Stopped"). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*3) + }, + + // Fakes train announcement + "treno": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + vs := findUserVoiceState(s, i.Member.User.ID, i.GuildID) + if vs != nil { + trainAnnounce := searchAndGetTrain(i.Data.Options[0].StringValue()) + if trainAnnounce == "" { + trainAnnounce = "Nessun treno trovato, agagagaga!" + } + sendEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Treno", trainAnnounce). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + + playSound(s, vs.GuildID, vs.ChannelID, genAudio(trainAnnounce)) + + err := s.InteractionResponseDelete(s.State.User.ID, i.Interaction) + if err != nil { + lit.Error("InteractionResponseDelete failed: %s", err.Error()) + } + } + }, + + // Says covid data out lout + "covid": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + vs := findUserVoiceState(s, i.Member.User.ID, i.GuildID) + if vs != nil { + covid := getCovid() + + sendEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Covid", covid). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + + _, _ = s.ChannelMessageSend(i.ChannelID, covid) + playSound(s, vs.GuildID, vs.ChannelID, genAudio(covid)) + } + }, + + // Adds a custom command + "addcustom": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := addCommand(i.Data.Options[0].StringValue(), i.Data.Options[1].StringValue(), i.GuildID) + if err != nil { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Error", err.Error()). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + } else { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Successful", "Custom command added!"). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + } + }, + + // Removes a custom command + "rmcustom": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := removeCustom(i.Data.Options[0].StringValue(), i.GuildID) + if err != nil { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Error", err.Error()). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + } else { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Successful", "Command removed successfully!"). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + } + }, + + // Select a random custom command + "preghiera": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + vs := findUserVoiceState(s, i.Member.User.ID, i.GuildID) + if vs != nil { + text := advancedReplace(advancedReplace(getRand(server[i.GuildID].customCommands), "", gods), "", adjectives) + + sendEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Preghiera", text). + SetColor(0x7289DA).MessageEmbed, i.Interaction) + + playSound(s, vs.GuildID, vs.ChannelID, genAudio(text)) + + err := s.InteractionResponseDelete(s.State.User.ID, i.Interaction) + if err != nil { + lit.Error("InteractionResponseDelete failed: %s", err.Error()) + } + } + }, + + // Plays the custom command if it exist + "custom": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + command := i.Data.Options[0].StringValue() + if server[i.GuildID].customCommands[command] != "" { + vs := findUserVoiceState(s, i.Member.User.ID, i.GuildID) + if vs != nil { + playSound(s, vs.GuildID, vs.ChannelID, genAudio(advancedReplace(advancedReplace(server[i.GuildID].customCommands[command], "", gods), "", adjectives))) + } + } else { + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Error", "Command doesn't exist!"). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*5) + } + }, + + // List all of the custom commands for the server + "listcustom": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + message := "" + + for c := range server[i.GuildID].customCommands { + message += c + ", " + } + + message = message[:len(message)-2] + + sendAndDeleteEmbedInteraction(s, NewEmbed().SetTitle(s.State.User.Username).AddField("Commands", message). + SetColor(0x7289DA).MessageEmbed, i.Interaction, time.Second*30) + }, + } +) diff --git a/commands_test.go b/commands_test.go new file mode 100644 index 0000000..2886868 --- /dev/null +++ b/commands_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/bwmarrin/discordgo" + "testing" +) + +// Checks if for every commands there's a function to handle that +func TestCommands(t *testing.T) { + for _, c := range commands { + if commandHandlers[c.Name] == nil { + t.Errorf("Declared command %s in application command slice, but there's no handler.", c.Name) + } + } + + for ch := range commandHandlers { + if !findCommandInCommandHandlers(commands, ch) { + t.Errorf("Declared command handler %s, but there's no command for it.", ch) + } + } +} + +func findCommandInCommandHandlers(commands []*discordgo.ApplicationCommand, el string) bool { + for _, c := range commands { + if c.Name == el { + return true + } + } + + return false +} diff --git a/covid.go b/covid.go index a06e081..e2ace02 100644 --- a/covid.go +++ b/covid.go @@ -11,7 +11,6 @@ import ( ) func getCovid() string { - var ( covid covid p = message.NewPrinter(language.Italian) diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..6ee60d6 --- /dev/null +++ b/embed.go @@ -0,0 +1,236 @@ +package main + +import "github.com/bwmarrin/discordgo" + +// Embed ... +type Embed struct { + *discordgo.MessageEmbed +} + +// Constants for message embed character limits +const ( + EmbedLimitTitle = 256 + EmbedLimitDescription = 2048 + EmbedLimitFieldValue = 1024 + EmbedLimitFieldName = 256 + EmbedLimitField = 25 + EmbedLimitFooter = 2048 + EmbedLimit = 4000 +) + +// NewEmbed returns a new embed object +func NewEmbed() *Embed { + return &Embed{&discordgo.MessageEmbed{}} +} + +// SetTitle ... +func (e *Embed) SetTitle(name string) *Embed { + e.Title = name + return e +} + +// SetDescription [desc] +func (e *Embed) SetDescription(description string) *Embed { + if len(description) > 2048 { + description = description[:2048] + } + e.Description = description + return e +} + +// AddField [name] [value] +func (e *Embed) AddField(name, value string) *Embed { + if len(value) > 1024 { + value = value[:1024] + } + + if len(name) > 1024 { + name = name[:1024] + } + + e.Fields = append(e.Fields, &discordgo.MessageEmbedField{ + Name: name, + Value: value, + }) + + return e + +} + +// SetFooter [Text] [iconURL] +func (e *Embed) SetFooter(args ...string) *Embed { + iconURL := "" + text := "" + proxyURL := "" + + switch { + case len(args) > 2: + proxyURL = args[2] + fallthrough + case len(args) > 1: + iconURL = args[1] + fallthrough + case len(args) > 0: + text = args[0] + case len(args) == 0: + return e + } + + e.Footer = &discordgo.MessageEmbedFooter{ + IconURL: iconURL, + Text: text, + ProxyIconURL: proxyURL, + } + + return e +} + +// SetImage ... +func (e *Embed) SetImage(args ...string) *Embed { + var URL string + var proxyURL string + + if len(args) == 0 { + return e + } + if len(args) > 0 { + URL = args[0] + } + if len(args) > 1 { + proxyURL = args[1] + } + e.Image = &discordgo.MessageEmbedImage{ + URL: URL, + ProxyURL: proxyURL, + } + return e +} + +// SetThumbnail ... +func (e *Embed) SetThumbnail(args ...string) *Embed { + var URL string + var proxyURL string + + if len(args) == 0 { + return e + } + if len(args) > 0 { + URL = args[0] + } + if len(args) > 1 { + proxyURL = args[1] + } + e.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: URL, + ProxyURL: proxyURL, + } + return e +} + +// SetAuthor ... +func (e *Embed) SetAuthor(args ...string) *Embed { + var ( + name string + iconURL string + URL string + proxyURL string + ) + + if len(args) == 0 { + return e + } + if len(args) > 0 { + name = args[0] + } + if len(args) > 1 { + iconURL = args[1] + } + if len(args) > 2 { + URL = args[2] + } + if len(args) > 3 { + proxyURL = args[3] + } + + e.Author = &discordgo.MessageEmbedAuthor{ + Name: name, + IconURL: iconURL, + URL: URL, + ProxyIconURL: proxyURL, + } + + return e +} + +// SetURL ... +func (e *Embed) SetURL(url string) *Embed { + e.URL = url + return e +} + +// SetColor ... +func (e *Embed) SetColor(clr int) *Embed { + e.Color = clr + return e +} + +// InlineAllFields sets all fields in the embed to be inline +func (e *Embed) InlineAllFields() *Embed { + for _, v := range e.Fields { + v.Inline = true + } + return e +} + +// Truncate truncates any embed value over the character limit. +func (e *Embed) Truncate() *Embed { + e.TruncateDescription() + e.TruncateFields() + e.TruncateFooter() + e.TruncateTitle() + return e +} + +// TruncateFields truncates fields that are too long +func (e *Embed) TruncateFields() *Embed { + if len(e.Fields) > 25 { + e.Fields = e.Fields[:EmbedLimitField] + } + + for _, v := range e.Fields { + + if len(v.Name) > EmbedLimitFieldName { + v.Name = v.Name[:EmbedLimitFieldName] + } + + if len(v.Value) > EmbedLimitFieldValue { + v.Value = v.Value[:EmbedLimitFieldValue] + } + + } + return e +} + +// TruncateDescription ... +func (e *Embed) TruncateDescription() *Embed { + if len(e.Description) > EmbedLimitDescription { + e.Description = e.Description[:EmbedLimitDescription] + } + return e +} + +// TruncateTitle ... +func (e *Embed) TruncateTitle() *Embed { + if len(e.Title) > EmbedLimitTitle { + e.Title = e.Title[:EmbedLimitTitle] + } + return e +} + +// TruncateFooter ... +func (e *Embed) TruncateFooter() *Embed { + if e.Footer != nil && len(e.Footer.Text) > EmbedLimitFooter { + e.Footer.Text = e.Footer.Text[:EmbedLimitFooter] + } + return e +} diff --git a/emoji.go b/emoji.go index 23ffe5a..6046e4b 100644 --- a/emoji.go +++ b/emoji.go @@ -1,9 +1,9 @@ package main import ( - jsoniter "github.com/json-iterator/go" "github.com/bwmarrin/lit" "github.com/forPelevin/gomoji" + jsoniter "github.com/json-iterator/go" "io/ioutil" "os" "strings" diff --git a/go.mod b/go.mod index f02cf08..e8b4045 100644 --- a/go.mod +++ b/go.mod @@ -3,31 +3,31 @@ module roberto go 1.14 require ( - github.com/bwmarrin/discordgo v0.23.2 + github.com/bwmarrin/discordgo v0.23.3-0.20210314162722-182d9b48f34b github.com/bwmarrin/lit v0.0.0-20190813132558-fd4b44871312 github.com/forPelevin/gomoji v0.0.0-20210102155214-f0681219d254 github.com/fsnotify/fsnotify v1.4.10-0.20200417215612-7f4cf4dd2b52 // indirect github.com/goodsign/monday v1.0.1-0.20201007115131-c065b60ec611 github.com/json-iterator/go v1.1.10 - github.com/klauspost/compress v1.11.12 // indirect - github.com/magiconair/properties v1.8.4 // indirect + github.com/klauspost/compress v1.11.13 // indirect + github.com/magiconair/properties v1.8.5 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pelletier/go-toml v1.8.2-0.20201124181426-2e01f733df54 // indirect - github.com/spf13/afero v1.5.1 // indirect + github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 // indirect github.com/spf13/jwalterweatherman v1.1.1-0.20200824194747-ce7498392da6 // indirect github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c // indirect github.com/spf13/viper v1.7.2-0.20201203004352-bba82cfc61da - github.com/tidwall/gjson v1.6.8 // indirect - github.com/tidwall/pretty v1.1.0 // indirect + github.com/tidwall/gjson v1.7.3 // indirect github.com/valyala/fasthttp v1.22.0 // indirect - golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect golang.org/x/mod v0.4.2 // indirect - golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 // indirect + golang.org/x/sys v0.0.0-20210324051608-47abb6519492 // indirect golang.org/x/text v0.3.5 golang.org/x/tools v0.1.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect modernc.org/ccgo/v3 v3.9.1 // indirect + modernc.org/libc v1.9.2 // indirect modernc.org/sqlite v1.10.0 modernc.org/strutil v1.1.1 // indirect ) diff --git a/go.sum b/go.sum index 47da8f3..fc8f73d 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= -github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= -github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/bwmarrin/discordgo v0.23.3-0.20210314162722-182d9b48f34b h1:hS1GR/OTQll44KPNT00/a6xevcCy4L9ZfPepUdUzV5Y= +github.com/bwmarrin/discordgo v0.23.3-0.20210314162722-182d9b48f34b/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= github.com/bwmarrin/lit v0.0.0-20190813132558-fd4b44871312 h1:g5NUBR8yN3F2RWN4vrkd8wVr7vyLxr0hbq4I6PAZQc0= github.com/bwmarrin/lit v0.0.0-20190813132558-fd4b44871312/go.mod h1:z4cxqo0ARxc42jGHYZT4XoICeLf9OVOkysN7LuqBZ24= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -196,8 +196,8 @@ github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.12 h1:famVnQVu7QwryBN4jNseQdUKES71ZAOnB6UQQJPZvqk= -github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -213,8 +213,8 @@ github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+s github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= -github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -322,8 +322,8 @@ github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4l github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= -github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 h1:GbJaXkBXPYlxE45H4g2wo0Hb4TGzv/YbHVA1OGqx+mo= github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -353,8 +353,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= github.com/tetafro/godot v1.3.2/go.mod h1:ah7jjYmOMnIjS9ku2krapvGQrFNtTLo9Z/qB3dGU1eU= github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= -github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= -github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= +github.com/tidwall/gjson v1.7.3 h1:9dOulDrkCJf1mwljVMhXNQr9ZL2NvajRX7A1R8c6Qxw= +github.com/tidwall/gjson v1.7.3/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -396,8 +396,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -489,8 +489,8 @@ golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 h1:pDMpM2zh2MT0kHy037cKlSby2nEhD50SYqwQk76Nm40= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -623,8 +623,9 @@ modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.8.0/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/libc v1.8.1 h1:y9oPIhwcaFXxX7kMp6Qb2ZLKzr0mDkikWN3CV5GS63o= modernc.org/libc v1.8.1/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.2 h1:PYr5miNafdDGOxvBAOKN29CHXf93t4Y6YDmwkbu+8Uk= +modernc.org/libc v1.9.2/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2 h1:+yFk8hBprV+4c0U9GjFtL+dV3N8hOJ8JCituQcMShFY= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= diff --git a/main.go b/main.go index a84a5bc..b2a6411 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,10 @@ package main import ( - "crypto/sha1" "database/sql" - "encoding/base32" - "encoding/binary" - "fmt" "github.com/bwmarrin/discordgo" "github.com/bwmarrin/lit" "github.com/spf13/viper" - "io" "io/ioutil" "math/rand" _ "modernc.org/sqlite" @@ -24,8 +19,6 @@ import ( var ( // Discord bot token token string - // Prefix for discord commands - prefix string // Server server = make(map[string]*Server) // Array of adjectives @@ -60,7 +53,6 @@ func init() { } else { // Config file found token = viper.GetString("token") - prefix = viper.GetString("prefix") // Set lit.LogLevel to the given value switch strings.ToLower(viper.GetString("loglevel")) { @@ -104,11 +96,6 @@ func main() { return } - if prefix == "" { - lit.Error("No prefix provided. Please modify config.yml") - return - } - // Create a new Discord session using the provided bot token. dg, err := discordgo.New("Bot " + token) if err != nil { @@ -116,9 +103,16 @@ func main() { return } - dg.AddHandler(messageCreate) - dg.AddHandler(guildCreate) + // Add events handler dg.AddHandler(ready) + dg.AddHandler(guildCreate) + + // Add commands handler + dg.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if h, ok := commandHandlers[i.Data.Name]; ok { + h(s, i) + } + }) // We set the intents that we use dg.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsGuildMessages | discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates) @@ -130,6 +124,26 @@ func main() { return } + // Checks for unused commands and deletes them + if cmds, err := dg.ApplicationCommands(dg.State.User.ID, ""); err == nil { + for _, c := range cmds { + if commandHandlers[c.Name] == nil { + _ = dg.ApplicationCommandDelete(dg.State.User.ID, "", c.ID) + lit.Info("Deleted unused command %s", c.Name) + } + } + } + + // And add commands used + lit.Info("Adding used commands...") + for _, v := range commands { + _, err := dg.ApplicationCommandCreate(dg.State.User.ID, "", v) + if err != nil { + lit.Error("Cannot create '%v' command: %v", v.Name, err) + } + } + lit.Info("Commands added!") + // Wait here until CTRL-C or other term signal is received. lit.Info("roberto is now running. Press CTRL-C to exit.") sc := make(chan os.Signal, 1) @@ -142,7 +156,7 @@ func main() { func ready(s *discordgo.Session, _ *discordgo.Ready) { // Set the playing status. - err := s.UpdateGameStatus(0, prefix+"help") + err := s.UpdateGameStatus(0, "Serving "+strconv.Itoa(len(s.State.Guilds))+" guilds!") if err != nil { lit.Error("Can't set status, %s", err) } @@ -152,350 +166,3 @@ func guildCreate(_ *discordgo.Session, e *discordgo.GuildCreate) { initializeServer(e.Guild.ID) } -func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - // Ignore messages sent from the bot, messages if the user is a bot, and messages without the prefix - if s.State.User.ID == m.Author.ID || m.Author.Bot || !strings.HasPrefix(m.Content, prefix) { - return - } - - // Split the message on spaces - splittedMessage := strings.Split(m.Content, " ") - - command := strings.TrimPrefix(strings.ToLower(splittedMessage[0]), prefix) - - lowerMessage := strings.ToLower(strings.TrimPrefix(m.Content, splittedMessage[0]+" ")) - - switch command { - case "bestemmia": - go deleteMessage(s, m.Message) - - vs := findUserVoiceState(s, m.Author.ID) - if vs == nil { - return - } - - // Locks the mutex for the current server - server[vs.GuildID].mutex.Lock() - - // Join the provided voice channel. - vc, err := s.ChannelVoiceJoin(vs.GuildID, vs.ChannelID, false, true) - if err != nil { - lit.Error("Can't connect to voice channel, %s", err) - server[vs.GuildID].mutex.Unlock() - return - } - - // If a number possibly exist - if len(splittedMessage) > 1 { - n, err := strconv.Atoi(splittedMessage[1]) - if err == nil { - // And we can convert it to a n, we repeat the sound for n times - - for i := 0; i < n; i++ { - if server[vs.GuildID].stop { - playSound2(genAudio(strings.ToUpper(bestemmia())), vc, s) - } else { - // Resets the stop boolean - server[vs.GuildID].stop = true - break - } - } - } - } else { - // Else, we only do the command once - playSound2(genAudio(strings.ToUpper(bestemmia())), vc, s) - } - - // Disconnect from the provided voice channel. - err = vc.Disconnect() - if err != nil { - lit.Error("Can't disconnect from voice channel, %s", err) - } - - // Releases the mutex lock for the server - server[vs.GuildID].mutex.Unlock() - break - - case "say": - go deleteMessage(s, m.Message) - - vs := findUserVoiceState(s, m.Author.ID) - if vs != nil { - playSound(s, vs.GuildID, vs.ChannelID, genAudio(emojiToDescription(strings.TrimPrefix(lowerMessage, prefix+"say ")))) - } - break - - case "stop": - server[m.GuildID].stop = false - go deleteMessage(s, m.Message) - break - - case "treno": - go deleteMessage(s, m.Message) - - vs := findUserVoiceState(s, m.Author.ID) - if vs != nil { - trainAnnounce := searchAndGetTrain(lowerMessage) - if trainAnnounce != "" { - playSound(s, vs.GuildID, vs.ChannelID, genAudio(trainAnnounce)) - } else { - playSound(s, vs.GuildID, vs.ChannelID, genAudio("Nessun treno trovato, agagagaga!")) - } - - } - break - - case "covid": - go deleteMessage(s, m.Message) - - vs := findUserVoiceState(s, m.Author.ID) - if vs != nil { - covid := getCovid() - - _, _ = s.ChannelMessageSend(m.ChannelID, covid) - playSound(s, vs.GuildID, vs.ChannelID, genAudio(covid)) - } - break - - // Adds a custom command - case "custom": - go deleteMessage(s, m.Message) - - if len(splittedMessage) > 2 { - addCommand(splittedMessage[1], strings.TrimPrefix(lowerMessage, prefix+"custom "+splittedMessage[1]+" "), m.GuildID) - } - break - - // Removes a custom command - case "rmcustom": - go deleteMessage(s, m.Message) - - removeCustom(strings.TrimPrefix(m.Content, prefix+"rmcustom "), m.GuildID) - break - - case prefix + "preghiera": - go deleteMessage(s, m.Message) - - vs := findUserVoiceState(s, m.Author.ID) - if vs != nil { - playSound(s, vs.GuildID, vs.ChannelID, genAudio(advancedReplace(advancedReplace(getRand(server[m.GuildID].customCommands), "", gods), "", adjectives))) - } - break - - // Prints out supported commands - case "help", "h": - go deleteMessage(s, m.Message) - - message := "Supported commands:\n```" + - prefix + "say - Says text out loud\n" + - prefix + "bestemmia - Generates a bestemmia n times\n" + - prefix + "treno - Fakes train announcement given it's number\n" + - prefix + "covid - Says covid data out loud for current day in Italy\n" + - prefix + "preghiera - Randomly select a custom command\n" + - prefix + "custom - Creates a custom command to say text out loud. The bot will replace with a random god and with a random adjective\n" + - prefix + "rmcustom - Removes a custom command\n" + - "```" - // If we have custom commands, we add them to the help message - if len(server[m.GuildID].customCommands) > 0 { - message += "\nCustom commands:\n```" - - for k := range server[m.GuildID].customCommands { - message += k + ", " - } - - message = strings.TrimSuffix(message, ", ") - message += "```" - } - - mex, err := s.ChannelMessageSend(m.ChannelID, message) - if err != nil { - fmt.Println(err) - break - } - - time.Sleep(time.Second * 30) - - err = s.ChannelMessageDelete(m.ChannelID, mex.ID) - if err != nil { - fmt.Println(err) - } - break - - // We search for possible custom commands - default: - if server[m.GuildID].customCommands[command] != "" { - go deleteMessage(s, m.Message) - - vs := findUserVoiceState(s, m.Author.ID) - if vs != nil { - playSound(s, vs.GuildID, vs.ChannelID, genAudio(advancedReplace(advancedReplace(server[m.GuildID].customCommands[command], "", gods), "", adjectives))) - } - break - } - } -} - -// genAudio generates a dca file from a string -func genAudio(text string) string { - h := sha1.New() - h.Write([]byte(text)) - uuid := strings.ToUpper(base32.HexEncoding.EncodeToString(h.Sum(nil))) - - gen(text, uuid) - - return uuid + ".dca" -} - -// playSound plays a file to the provided channel. -func playSound(s *discordgo.Session, guildID, channelID, fileName string) { - var opuslen int16 - - file, err := os.Open("./temp/" + fileName) - if err != nil { - lit.Error("Error opening dca file: %s", err) - return - } - defer file.Close() - - // Locks the mutex for the current server - server[guildID].mutex.Lock() - - // Join the provided voice channel. - vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) - if err != nil { - server[guildID].mutex.Unlock() - return - } - - // Start speaking. - _ = vc.Speaking(true) - server[guildID].stop = true - - // Channel to send ok messages - c1 := make(chan string, 1) - - for { - // Read opus frame length from dca file. - err = binary.Read(file, binary.LittleEndian, &opuslen) - - // If this is the end of the file, just return. - if err == io.EOF || err == io.ErrUnexpectedEOF { - break - } - - if err != nil { - lit.Error("Error reading from dca file: %s", err) - break - } - - // Read encoded pcm from dca file. - InBuf := make([]byte, opuslen) - err = binary.Read(file, binary.LittleEndian, &InBuf) - - // Should not be any end of file errors - if err != nil { - lit.Error("Error reading from dca file: %s", err) - break - } - - // Stream data to discord - if server[guildID].stop { - // Send data in a goroutine - go func() { - vc.OpusSend <- InBuf - c1 <- "ok" - }() - - // So if the bot gets disconnect/moved we can rejoin the original channel and continue playing songs - select { - case _ = <-c1: - break - case <-time.After(time.Second / 3): - vc, _ = s.ChannelVoiceJoin(guildID, channelID, false, true) - } - } else { - break - } - } - - // Resets the stop boolean - server[guildID].stop = true - - // Stop speaking - _ = vc.Speaking(false) - - // Disconnect from the provided voice channel. - err = vc.Disconnect() - if err != nil { - lit.Error("Can't disconnect from voice channel, %s", err) - return - } - - // Releases the mutex lock for the server - server[guildID].mutex.Unlock() -} - -// playSound2 plays a file to the provided channel given a voice connection. -func playSound2(fileName string, vc *discordgo.VoiceConnection, s *discordgo.Session) { - var opuslen int16 - - file, err := os.Open("./temp/" + fileName) - if err != nil { - lit.Error("Error opening dca file: %s", err) - return - } - defer file.Close() - - // Start speaking. - _ = vc.Speaking(true) - - // Channel to send ok messages - c1 := make(chan string, 1) - - guildID := vc.GuildID - channelID := vc.ChannelID - - for { - // Read opus frame length from dca file. - err = binary.Read(file, binary.LittleEndian, &opuslen) - - // If this is the end of the file, just return. - if err == io.EOF || err == io.ErrUnexpectedEOF { - break - } - - if err != nil { - lit.Error("Error reading from dca file: %s", err) - break - } - - // Read encoded pcm from dca file. - InBuf := make([]byte, opuslen) - err = binary.Read(file, binary.LittleEndian, &InBuf) - - // Should not be any end of file errors - if err != nil { - lit.Error("Error reading from dca file: %s", err) - break - } - - // Stream data to discord - // Send data in a goroutine - go func() { - vc.OpusSend <- InBuf - c1 <- "ok" - }() - - // So if the bot gets disconnect/moved we can rejoin the original channel and continue playing songs - select { - case _ = <-c1: - break - case <-time.After(time.Second / 3): - vc, _ = s.ChannelVoiceJoin(guildID, channelID, false, true) - } - - } - - // Stop speaking - _ = vc.Speaking(false) -} diff --git a/sound.go b/sound.go new file mode 100644 index 0000000..1afe710 --- /dev/null +++ b/sound.go @@ -0,0 +1,168 @@ +package main + +import ( + "encoding/binary" + "github.com/bwmarrin/discordgo" + "github.com/bwmarrin/lit" + "io" + "os" + "time" +) + +// playSound plays a file to the provided channel. +func playSound(s *discordgo.Session, guildID, channelID, fileName string) { + var opuslen int16 + + file, err := os.Open("./temp/" + fileName) + if err != nil { + lit.Error("Error opening dca file: %s", err) + return + } + + // Locks the mutex for the current server + server[guildID].mutex.Lock() + + // Join the provided voice channel. + vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) + if err != nil { + server[guildID].mutex.Unlock() + return + } + + // Start speaking. + _ = vc.Speaking(true) + server[guildID].stop = true + + // Channel to send ok messages + c1 := make(chan string, 1) + + for { + // Read opus frame length from dca file. + err = binary.Read(file, binary.LittleEndian, &opuslen) + + // If this is the end of the file, just return. + if err == io.EOF || err == io.ErrUnexpectedEOF { + break + } + + if err != nil { + lit.Error("Error reading from dca file: %s", err) + break + } + + // Read encoded pcm from dca file. + InBuf := make([]byte, opuslen) + err = binary.Read(file, binary.LittleEndian, &InBuf) + + // Should not be any end of file errors + if err != nil { + lit.Error("Error reading from dca file: %s", err) + break + } + + // Stream data to discord + if server[guildID].stop { + // Send data in a goroutine + go func() { + vc.OpusSend <- InBuf + c1 <- "ok" + }() + + // So if the bot gets disconnect/moved we can rejoin the original channel and continue playing songs + select { + case _ = <-c1: + break + case <-time.After(time.Second / 3): + vc, _ = s.ChannelVoiceJoin(guildID, channelID, false, true) + } + } else { + break + } + } + + // Close the file + _ = file.Close() + + // Resets the stop boolean + server[guildID].stop = true + + // Stop speaking + _ = vc.Speaking(false) + + // Disconnect from the provided voice channel. + err = vc.Disconnect() + if err != nil { + lit.Error("Can't disconnect from voice channel, %s", err) + return + } + + // Releases the mutex lock for the server + server[guildID].mutex.Unlock() +} + +// playSound2 plays a file to the provided channel given a voice connection. +func playSound2(fileName string, vc *discordgo.VoiceConnection, s *discordgo.Session) { + var opuslen int16 + + file, err := os.Open("./temp/" + fileName) + if err != nil { + lit.Error("Error opening dca file: %s", err) + return + } + + // Start speaking. + _ = vc.Speaking(true) + + // Channel to send ok messages + c1 := make(chan string, 1) + + guildID := vc.GuildID + channelID := vc.ChannelID + + for { + // Read opus frame length from dca file. + err = binary.Read(file, binary.LittleEndian, &opuslen) + + // If this is the end of the file, just return. + if err == io.EOF || err == io.ErrUnexpectedEOF { + break + } + + if err != nil { + lit.Error("Error reading from dca file: %s", err) + break + } + + // Read encoded pcm from dca file. + InBuf := make([]byte, opuslen) + err = binary.Read(file, binary.LittleEndian, &InBuf) + + // Should not be any end of file errors + if err != nil { + lit.Error("Error reading from dca file: %s", err) + break + } + + // Stream data to discord + // Send data in a goroutine + go func() { + vc.OpusSend <- InBuf + c1 <- "ok" + }() + + // So if the bot gets disconnect/moved we can rejoin the original channel and continue playing songs + select { + case _ = <-c1: + break + case <-time.After(time.Second / 3): + vc, _ = s.ChannelVoiceJoin(guildID, channelID, false, true) + } + + } + + // Close the file + _ = file.Close() + + // Stop speaking + _ = vc.Speaking(false) +} diff --git a/utilities.go b/utilities.go index f30f3b0..c29f7d0 100644 --- a/utilities.go +++ b/utilities.go @@ -1,7 +1,10 @@ package main import ( + "crypto/sha1" "database/sql" + "encoding/base32" + "errors" "github.com/bwmarrin/discordgo" "github.com/bwmarrin/lit" "math/rand" @@ -10,27 +13,21 @@ import ( "runtime" "strings" "sync" + "time" ) const ( tblCustomCommands = "CREATE TABLE IF NOT EXISTS \"customCommands\" (\"server\" VARCHAR(18) NOT NULL,\"command\" VARCHAR(50) NOT NULL,\"text\" VARCHAR(2000) NOT NULL);" ) -// deleteMessage delete a message -func deleteMessage(s *discordgo.Session, m *discordgo.Message) { - lit.Info("%s: %s", m.Author.Username, m.Content) - err := s.ChannelMessageDelete(m.ChannelID, m.ID) - if err != nil { - lit.Error("Can't delete message, %s", err) - } -} - // findUserVoiceState finds the voicestate of a user -func findUserVoiceState(session *discordgo.Session, userID string) *discordgo.VoiceState { - for _, guild := range session.State.Guilds { - for _, vs := range guild.VoiceStates { - if vs.UserID == userID { - return vs +func findUserVoiceState(s *discordgo.Session, userID string, guidID string) *discordgo.VoiceState { + for _, g := range s.State.Guilds { + if g.ID == guidID { + for _, vs := range g.VoiceStates { + if vs.UserID == userID { + return vs + } } } } @@ -65,12 +62,12 @@ func execQuery(query string, db *sql.DB) { } // Adds a custom command to db and to the command map -func addCommand(command string, text string, guild string) { +func addCommand(command string, text string, guild string) error { initializeServer(guild) // If the text is already in the map, we ignore it if server[guild].customCommands[command] == text { - return + return errors.New("command already exists") } // Else, we add it to the map @@ -82,24 +79,35 @@ func addCommand(command string, text string, guild string) { _, err := stm.Exec(guild, command, text) if err != nil { lit.Error("Error inserting into the database, %s", err) + return errors.New("error inserting into the database: " + err.Error()) } _ = stm.Close() + + return nil } // Removes a custom command from the db and from the command map -func removeCustom(command string, guild string) { +func removeCustom(command string, guild string) error { + + if server[guild].customCommands[command] == "" { + return errors.New("command doesn't exist") + } + // Remove from DB stm, _ := db.Prepare("DELETE FROM customCommands WHERE server=? AND command=?") _, err := stm.Exec(guild, command) if err != nil { lit.Error("Error removing from the database, %s", err) + return errors.New("error removing from the database: " + err.Error()) } _ = stm.Close() // Remove from the map delete(server[guild].customCommands, command) + + return nil } // Loads custom command from the database @@ -200,3 +208,47 @@ func initializeServer(guildID string) { } } } + +// Sends embed as response to an interaction +func sendEmbedInteraction(s *discordgo.Session, embed *discordgo.MessageEmbed, i *discordgo.Interaction) { + sliceEmbed := []*discordgo.MessageEmbed{embed} + err := s.InteractionRespond(i, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionApplicationCommandResponseData{Embeds: sliceEmbed}}) + if err != nil { + lit.Error("InteractionRespond failed: %s", err) + return + } +} + +// Sends and delete after three second an embed in a given channel +func sendAndDeleteEmbedInteraction(s *discordgo.Session, embed *discordgo.MessageEmbed, i *discordgo.Interaction, wait time.Duration) { + sendEmbedInteraction(s, embed, i) + + time.Sleep(wait) + + err := s.InteractionResponseDelete(s.State.User.ID, i) + if err != nil { + lit.Error("InteractionResponseDelete failed: %s", err) + return + } +} + +// Modify an already sent interaction +func modfyInteraction(s *discordgo.Session, embed *discordgo.MessageEmbed, i *discordgo.Interaction) { + sliceEmbed := []*discordgo.MessageEmbed{embed} + err := s.InteractionResponseEdit(s.State.User.ID, i, &discordgo.WebhookEdit{Embeds: sliceEmbed}) + if err != nil { + lit.Error("InteractionResponseEdit failed: %s", err) + return + } +} + +// genAudio generates a dca file from a string +func genAudio(text string) string { + h := sha1.New() + h.Write([]byte(text)) + uuid := strings.ToUpper(base32.HexEncoding.EncodeToString(h.Sum(nil))) + + gen(text, uuid) + + return uuid + ".dca" +}