diff --git a/changelog.md b/changelog.md index 756a7c5..a9a9260 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 4.2.0 - 2019-10-20 +### Added +- Universal song handler with polka and sax commands + +### Changed +- Updated discordgo library up to v0.20.1 + ## 4.1.0 - 2019-07-28 ### Added - Song player handler diff --git a/go.mod b/go.mod index 8372b61..7449d4d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module galched-bot require ( - github.com/bwmarrin/discordgo v0.19.0 + github.com/bwmarrin/discordgo v0.20.1 github.com/gempir/go-twitch-irc/v2 v2.2.0 + github.com/gorilla/websocket v1.4.1 // indirect github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.3.0 // indirect go.uber.org/atomic v1.3.2 @@ -10,4 +11,8 @@ require ( go.uber.org/fx v1.9.0 go.uber.org/goleak v0.10.0 // indirect go.uber.org/multierr v1.1.0 // indirect + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect + golang.org/x/sys v0.0.0-20191018095205-727590c5006e // indirect ) + +go 1.13 diff --git a/go.sum b/go.sum index bcabf38..5faca35 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY= -github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/bwmarrin/discordgo v0.20.1 h1:Ihh3/mVoRwy3otmaoPDUioILBJq4fdWkpsi83oj2Lmk= +github.com/bwmarrin/discordgo v0.20.1/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gempir/go-twitch-irc v1.1.0 h1:Q9gQGI/3yJzYwlYDlFsGJzWfpaqubMExfmBXNpOC6W0= -github.com/gempir/go-twitch-irc v1.1.0/go.mod h1:Pc661rsUSmkQXvI9W2bNyLt4ZrMAgHZPnVwMQEJ0fdo= github.com/gempir/go-twitch-irc/v2 v2.2.0 h1:9iYRr/PkT5tqnD9J0awBXtwS4R4DatA5cMQbsua6OvM= github.com/gempir/go-twitch-irc/v2 v2.2.0/go.mod h1:0HXoEr9l7gNjwajosptV0w0xGpHeU6gsD7JDlfvjTYI= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -27,3 +27,13 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191018095205-727590c5006e h1:ZtoklVMHQy6BFRHkbG6JzK+S6rX82//Yeok1vMlizfQ= +golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/modules/discord/discord.go b/modules/discord/discord.go index 11e47c6..62c266e 100644 --- a/modules/discord/discord.go +++ b/modules/discord/discord.go @@ -1,17 +1,12 @@ package discord import ( - "encoding/binary" "fmt" - "io" "log" - "os" "galched-bot/modules/settings" "galched-bot/modules/subday" - "go.uber.org/atomic" - "github.com/bwmarrin/discordgo" "github.com/pkg/errors" ) @@ -36,15 +31,9 @@ func New(s *settings.Settings, subday *subday.Subday) (*Discord, error) { processor.AddHandler(subdayHandler) } - polka, err := loadSong(s.PolkaPath) - if err != nil { - return nil, errors.Wrap(err, "cannot read polka song") + for _, songHandler := range SongHandlers(s) { + processor.AddHandler(songHandler) } - processor.AddHandler(&polkaHandler{ - polka: polka, - voiceChannel: s.DiscordVoiceChannel, - lock: atomic.NewBool(false), - }) log.Printf("discord: added %d message handlers", len(processor.handlers)) if len(processor.handlers) > 0 { @@ -60,48 +49,6 @@ func New(s *settings.Settings, subday *subday.Subday) (*Discord, error) { }, nil } -func loadSong(path string) ([][]byte, error) { - file, err := os.Open(path) - if err != nil { - return nil, errors.Wrap(err, "error opening dca file") - } - - var ( - opuslen int16 - buffer = make([][]byte, 0) - ) - - 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 { - err := file.Close() - if err != nil { - return nil, err - } - return buffer, nil - } - - if err != nil { - return nil, errors.Wrap(err, "error reading from dca file") - } - - // 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 { - return nil, errors.Wrap(err, "error reading from dca file") - } - - // Append encoded pcm data to the buffer. - buffer = append(buffer, InBuf) - } -} - func LogMessage(m *discordgo.MessageCreate) { log.Printf("discord: msg [%s]: %s", m.Author.Username, m.Content) } diff --git a/modules/discord/polkahandler.go b/modules/discord/polkahandler.go deleted file mode 100644 index 5f3dc3d..0000000 --- a/modules/discord/polkahandler.go +++ /dev/null @@ -1,92 +0,0 @@ -package discord - -import ( - "log" - "time" - - "github.com/bwmarrin/discordgo" - "go.uber.org/atomic" -) - -type polkaHandler struct { - polka [][]byte - voiceChannel string - lock *atomic.Bool -} - -func (h *polkaHandler) Signature() string { - return "!song" -} - -func (h *polkaHandler) Description() string { - return "сыграть гимн галчед (только для избранных)" -} - -func (h *polkaHandler) IsValid(msg string) bool { - return msg == "!song" -} - -func (h *polkaHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { - if m.Author.Username != "YoMedved" && m.Author.Username != "Rummy_Quamox" && m.Author.Username != "Lidiya_owl" { - log.Printf("discord: unathorized polka message from %s", m.Author.Username) - return - } - - // Find the channel that the message came from. - c, err := s.State.Channel(m.ChannelID) - if err != nil { - // Could not find channel. - return - } - - // Find the guild for that channel. - g, err := s.State.Guild(c.GuildID) - if err != nil { - // Could not find guild. - return - } - - // Look for the message sender in that guild's current voice states. - LogMessage(m) - if h.lock.CAS(false, true) { - defer h.lock.Store(false) - err = h.playSound(s, g.ID, h.voiceChannel) - if err != nil { - log.Println("discord: error playing sound:", err) - } - time.Sleep(10 * time.Second) - return - } -} - -// playSound plays the current buffer to the provided channel. -func (h *polkaHandler) playSound(s *discordgo.Session, guildID, channelID string) (err error) { - - // Join the provided voice channel. - vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) - if err != nil { - return err - } - - // Sleep for a specified amount of time before playing the sound - time.Sleep(250 * time.Millisecond) - - // Start speaking. - vc.Speaking(true) - - // Send the buffer data. - for _, buff := range h.polka { - vc.OpusSend <- buff - } - - // Stop speaking - vc.Speaking(false) - - // Sleep for a specified amount of time before ending. - time.Sleep(250 * time.Millisecond) - - // Disconnect from the provided voice channel. - vc.Disconnect() - - return nil -} diff --git a/modules/discord/songhandlers.go b/modules/discord/songhandlers.go new file mode 100644 index 0000000..cc76886 --- /dev/null +++ b/modules/discord/songhandlers.go @@ -0,0 +1,191 @@ +package discord + +import ( + "encoding/binary" + "io" + "log" + "os" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" + "go.uber.org/atomic" + + "galched-bot/modules/settings" +) + +type ( + songData [][]byte + + SongHandler struct { + globalLock *atomic.Bool + songLock *atomic.Bool + + song songData + signature string + description string + voiceChannel string + permissions []string + timeout time.Duration + } +) + +func (h *SongHandler) Signature() string { + return h.signature +} + +func (h *SongHandler) Description() string { + return h.description +} + +func (h *SongHandler) IsValid(msg string) bool { + return msg == h.signature +} + +func (h *SongHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { + var permitted bool + for i := range h.permissions { + if m.Author.Username == h.permissions[i] { + permitted = true + break + } + } + if len(h.permissions) > 0 && !permitted { + log.Printf("discord: unathorized %s message from %s", + h.signature, m.Author.Username) + return + } + + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // Could not find channel. + return + } + + // Find the guild for that channel. + g, err := s.State.Guild(c.GuildID) + if err != nil { + // Could not find guild. + return + } + + // Look for the message sender in that guild's current voice states. + LogMessage(m) + if h.globalLock.CAS(false, true) { + if h.songLock.CAS(false, true) { + + err = playSound(s, g.ID, h.voiceChannel, h.song) + if err != nil { + log.Println("discord: error playing sound:", err) + } + + h.globalLock.Store(false) + time.Sleep(h.timeout) + defer h.songLock.Store(false) + + return + } + h.globalLock.Store(false) + } +} + +func SongHandlers(s *settings.Settings) []MessageHandler { + result := make([]MessageHandler, 0, len(s.Songs)) + g := new(atomic.Bool) + + for i := range s.Songs { + song, err := loadSong(s.Songs[i].Path) + if err != nil { + log.Println("discord: error loading song file", err) + continue + } + handler := &SongHandler{ + globalLock: g, + songLock: new(atomic.Bool), + song: song, + signature: s.Songs[i].Signature, + description: s.Songs[i].Description, + voiceChannel: s.DiscordVoiceChannel, + permissions: s.Songs[i].Permissions, + timeout: s.Songs[i].Timeout, + } + result = append(result, handler) + } + + return result +} + +// playSound plays the current buffer to the provided channel. +func playSound(s *discordgo.Session, guildID, channelID string, song songData) error { + + // Join the provided voice channel. + vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) + if err != nil { + return err + } + + // Sleep for a specified amount of time before playing the sound + time.Sleep(250 * time.Millisecond) + + // Start speaking. + vc.Speaking(true) + + // Send the buffer data. + for _, buff := range song { + vc.OpusSend <- buff + } + + // Stop speaking + vc.Speaking(false) + + // Sleep for a specified amount of time before ending. + time.Sleep(250 * time.Millisecond) + + // Disconnect from the provided voice channel. + vc.Disconnect() + + return nil +} + +func loadSong(path string) (songData, error) { + file, err := os.Open(path) + if err != nil { + return nil, errors.Wrap(err, "error opening dca file") + } + + var ( + opuslen int16 + buffer = make(songData, 0) + ) + + 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 { + err := file.Close() + if err != nil { + return nil, err + } + return buffer, nil + } + + if err != nil { + return nil, errors.Wrap(err, "error reading from dca file") + } + + // 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 { + return nil, errors.Wrap(err, "error reading from dca file") + } + + // Append encoded pcm data to the buffer. + buffer = append(buffer, InBuf) + } +} diff --git a/modules/settings/settings.go b/modules/settings/settings.go index b6bfa6c..f174a2d 100644 --- a/modules/settings/settings.go +++ b/modules/settings/settings.go @@ -3,10 +3,11 @@ package settings import ( "io/ioutil" "log" + "time" ) const ( - version = "4.1.0" + version = "4.2.0" twitchUser = "galchedbot" twitchIRCRoom = "galched" discordTokenPath = "./tokens/.discordtoken" @@ -21,6 +22,14 @@ const ( ) type ( + SongInfo struct { + Path string + Signature string + Description string + Permissions []string + Timeout time.Duration + } + Settings struct { Version string DiscordToken string @@ -29,8 +38,8 @@ type ( TwitchToken string SubdayDataPath string PermittedRoles []string - PolkaPath string DiscordVoiceChannel string + Songs []SongInfo } ) @@ -51,8 +60,22 @@ func New() (*Settings, error) { TwitchUser: twitchUser, TwitchIRCRoom: twitchIRCRoom, SubdayDataPath: subdayDataPath, - PolkaPath: "songs/polka.dca", DiscordVoiceChannel: "301793085522706432", PermittedRoles: []string{subRole1, subRole2, galchedRole, smorcRole}, + Songs: []SongInfo{ + { + Path: "songs/polka.dca", + Signature: "!song", + Description: "сыграть гимн галчед (только для избранных", + Permissions: []string{"AlexV", "Rummy_Quamox", "Lidiya_owl"}, + Timeout: 10 * time.Second, + }, + { + Path: "songs/whisper.dca", + Signature: "!sax", + Description: "kreygasm", + Timeout: 20 * time.Second, + }, + }, }, nil }