2017-01-05 23:08:29 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"text/tabwriter"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
log "github.com/Sirupsen/logrus"
|
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
// discordgo session
|
|
|
|
|
discord *discordgo.Session
|
|
|
|
|
|
|
|
|
|
// Map of Guild id's to *Play channels, used for queuing and rate-limiting guilds
|
2017-01-06 16:48:11 +01:00
|
|
|
queues = make(map[string]chan *Play)
|
2017-01-05 23:08:29 +01:00
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// bitrate Sound encoding settings
|
|
|
|
|
bitrate = 128
|
|
|
|
|
// maxQueueSize Sound encoding settings
|
|
|
|
|
maxQueueSize = 6
|
2017-01-05 23:08:29 +01:00
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// OWNER variable
|
2017-01-05 23:08:29 +01:00
|
|
|
OWNER string
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Play represents an individual use of the !airhorn command
|
|
|
|
|
type Play struct {
|
|
|
|
|
GuildID string
|
|
|
|
|
ChannelID string
|
|
|
|
|
UserID string
|
|
|
|
|
Sound *Sound
|
|
|
|
|
|
|
|
|
|
// The next play to occur after this, only used for chaining sounds like anotha
|
|
|
|
|
Next *Play
|
|
|
|
|
|
|
|
|
|
// If true, this was a forced play using a specific airhorn sound name
|
|
|
|
|
Forced bool
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// SoundCollection of Sounds
|
2017-01-05 23:08:29 +01:00
|
|
|
type SoundCollection struct {
|
|
|
|
|
Prefix string
|
|
|
|
|
Commands []string
|
|
|
|
|
Sounds []*Sound
|
|
|
|
|
ChainWith *SoundCollection
|
|
|
|
|
|
|
|
|
|
soundRange int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sound represents a sound clip
|
|
|
|
|
type Sound struct {
|
|
|
|
|
Name string
|
|
|
|
|
|
|
|
|
|
// Weight adjust how likely it is this song will play, higher = more likely
|
|
|
|
|
Weight int
|
|
|
|
|
|
|
|
|
|
// Delay (in milliseconds) for the bot to wait before sending the disconnect request
|
|
|
|
|
PartDelay int
|
|
|
|
|
|
|
|
|
|
// Buffer to store encoded PCM packets
|
|
|
|
|
buffer [][]byte
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// AIRHORN Array of all the sounds we have
|
|
|
|
|
var AIRHORN = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "airhorn",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!airhorn",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("default", 1000, 250),
|
|
|
|
|
createSound("reverb", 800, 250),
|
|
|
|
|
createSound("spam", 800, 0),
|
|
|
|
|
createSound("tripletap", 800, 250),
|
|
|
|
|
createSound("fourtap", 800, 250),
|
|
|
|
|
createSound("distant", 500, 250),
|
|
|
|
|
createSound("echo", 500, 250),
|
|
|
|
|
createSound("clownfull", 250, 250),
|
|
|
|
|
createSound("clownshort", 250, 250),
|
|
|
|
|
createSound("clownspam", 250, 0),
|
|
|
|
|
createSound("highfartlong", 200, 250),
|
|
|
|
|
createSound("highfartshort", 200, 250),
|
|
|
|
|
createSound("midshort", 100, 250),
|
|
|
|
|
createSound("truck", 10, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 21:16:13 +01:00
|
|
|
// BSTH collection
|
|
|
|
|
var BSTH = &SoundCollection{
|
|
|
|
|
Prefix: "bsth",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!bsth",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("achso", 1, 250),
|
|
|
|
|
createSound("angeben", 1, 250),
|
|
|
|
|
createSound("aufwiedersehen", 1, 250),
|
|
|
|
|
createSound("bluemchen", 1, 250),
|
|
|
|
|
createSound("blutsprobe", 1, 250),
|
|
|
|
|
createSound("chefkoch", 1, 250),
|
|
|
|
|
createSound("danke", 1, 250),
|
|
|
|
|
createSound("eindummer", 1, 250),
|
|
|
|
|
createSound("eleganz", 1, 250),
|
|
|
|
|
createSound("fettesau", 1, 250),
|
|
|
|
|
createSound("fett", 1, 250),
|
|
|
|
|
createSound("fressen", 1, 250),
|
|
|
|
|
createSound("geraeusch", 1, 250),
|
|
|
|
|
createSound("gottseimitdir", 1, 250),
|
|
|
|
|
createSound("gruss", 1, 250),
|
|
|
|
|
createSound("gulli", 1, 250),
|
|
|
|
|
createSound("harfenkonzert", 1, 250),
|
|
|
|
|
createSound("hosen", 1, 250),
|
|
|
|
|
createSound("indiehose", 1, 250),
|
|
|
|
|
createSound("kai", 1, 250),
|
|
|
|
|
createSound("kalkleiste", 1, 250),
|
|
|
|
|
createSound("keinegrauenzellen", 1, 250),
|
|
|
|
|
createSound("kohlen", 1, 250),
|
|
|
|
|
createSound("meinspruch", 1, 250),
|
|
|
|
|
createSound("miete", 1, 250),
|
|
|
|
|
createSound("nuesse", 1, 250),
|
|
|
|
|
createSound("ohneheu", 1, 250),
|
|
|
|
|
createSound("riecher", 1, 250),
|
|
|
|
|
createSound("sauer", 1, 250),
|
|
|
|
|
createSound("schatten", 1, 250),
|
|
|
|
|
createSound("scheitel", 1, 250),
|
|
|
|
|
createSound("solingen", 1, 250),
|
|
|
|
|
createSound("unglaublich", 1, 250),
|
|
|
|
|
createSound("unmoeglich", 1, 250),
|
|
|
|
|
createSound("vortreten", 1, 250),
|
|
|
|
|
createSound("zufaellig", 1, 250),
|
2017-01-06 23:50:24 +01:00
|
|
|
createSound("arschloch", 1, 250),
|
|
|
|
|
createSound("aufstossen", 1, 250),
|
|
|
|
|
createSound("ausholen", 1, 250),
|
|
|
|
|
createSound("blaehungen", 1, 250),
|
|
|
|
|
createSound("haesslich", 1, 250),
|
|
|
|
|
createSound("horchlappen", 1, 250),
|
|
|
|
|
createSound("interesseng", 1, 250),
|
|
|
|
|
createSound("keingrips", 1, 250),
|
|
|
|
|
createSound("kriegicheineis", 1, 250),
|
|
|
|
|
createSound("lachespaeter", 1, 250),
|
|
|
|
|
createSound("machtjanix", 1, 250),
|
|
|
|
|
createSound("nichtsofett", 1, 250),
|
|
|
|
|
createSound("schweine", 1, 250),
|
|
|
|
|
createSound("unfreundlich", 1, 250),
|
|
|
|
|
createSound("verheiratet", 1, 250),
|
|
|
|
|
createSound("zulaut", 1, 250),
|
2017-01-06 21:16:13 +01:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BSTH2 collection
|
|
|
|
|
var BSTH2 = &SoundCollection{
|
|
|
|
|
Prefix: "bsth2",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!bsth2",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("abendplanung", 1, 250),
|
|
|
|
|
createSound("alleine", 1, 250),
|
|
|
|
|
createSound("chefkoch", 1, 250),
|
|
|
|
|
createSound("fett", 1, 250),
|
|
|
|
|
createSound("hubschrauber", 1, 250),
|
|
|
|
|
createSound("perspektive", 1, 250),
|
|
|
|
|
createSound("platonisch", 1, 250),
|
|
|
|
|
createSound("qualle", 1, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// KHALED collection
|
|
|
|
|
var KHALED = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "another",
|
|
|
|
|
ChainWith: AIRHORN,
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!anotha",
|
|
|
|
|
"!anothaone",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("one", 1, 250),
|
|
|
|
|
createSound("one_classic", 1, 250),
|
|
|
|
|
createSound("one_echo", 1, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// CENA collection
|
|
|
|
|
var CENA = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "jc",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!johncena",
|
|
|
|
|
"!cena",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("airhorn", 1, 250),
|
|
|
|
|
createSound("echo", 1, 250),
|
|
|
|
|
createSound("full", 1, 250),
|
|
|
|
|
createSound("jc", 1, 250),
|
|
|
|
|
createSound("nameis", 1, 250),
|
|
|
|
|
createSound("spam", 1, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// ETHAN collection
|
|
|
|
|
var ETHAN = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "ethan",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!ethan",
|
|
|
|
|
"!eb",
|
|
|
|
|
"!ethanbradberry",
|
|
|
|
|
"!h3h3",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("areyou_classic", 100, 250),
|
|
|
|
|
createSound("areyou_condensed", 100, 250),
|
|
|
|
|
createSound("areyou_crazy", 100, 250),
|
|
|
|
|
createSound("areyou_ethan", 100, 250),
|
|
|
|
|
createSound("classic", 100, 250),
|
|
|
|
|
createSound("echo", 100, 250),
|
|
|
|
|
createSound("high", 100, 250),
|
|
|
|
|
createSound("slowandlow", 100, 250),
|
|
|
|
|
createSound("cuts", 30, 250),
|
|
|
|
|
createSound("beat", 30, 250),
|
|
|
|
|
createSound("sodiepop", 1, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// COW collection
|
|
|
|
|
var COW = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "cow",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!stan",
|
|
|
|
|
"!stanislav",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("herd", 10, 250),
|
|
|
|
|
createSound("moo", 10, 250),
|
|
|
|
|
createSound("x3", 1, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// BIRTHDAY collection
|
|
|
|
|
var BIRTHDAY = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "birthday",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!birthday",
|
|
|
|
|
"!bday",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("horn", 50, 250),
|
|
|
|
|
createSound("horn3", 30, 250),
|
|
|
|
|
createSound("sadhorn", 25, 250),
|
|
|
|
|
createSound("weakhorn", 25, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// WOW collection
|
|
|
|
|
var WOW = &SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
Prefix: "wow",
|
|
|
|
|
Commands: []string{
|
|
|
|
|
"!wowthatscool",
|
|
|
|
|
"!wtc",
|
|
|
|
|
},
|
|
|
|
|
Sounds: []*Sound{
|
|
|
|
|
createSound("thatscool", 50, 250),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// COLLECTIONS all collections
|
|
|
|
|
var COLLECTIONS = []*SoundCollection{
|
2017-01-05 23:08:29 +01:00
|
|
|
AIRHORN,
|
|
|
|
|
KHALED,
|
|
|
|
|
CENA,
|
|
|
|
|
ETHAN,
|
|
|
|
|
COW,
|
|
|
|
|
BIRTHDAY,
|
|
|
|
|
WOW,
|
2017-01-06 21:16:13 +01:00
|
|
|
BSTH,
|
2017-01-06 21:24:14 +01:00
|
|
|
BSTH2,
|
2017-01-05 23:08:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a Sound struct
|
|
|
|
|
func createSound(Name string, Weight int, PartDelay int) *Sound {
|
|
|
|
|
return &Sound{
|
|
|
|
|
Name: Name,
|
|
|
|
|
Weight: Weight,
|
|
|
|
|
PartDelay: PartDelay,
|
|
|
|
|
buffer: make([][]byte, 0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// Load soundcollection
|
2017-01-05 23:08:29 +01:00
|
|
|
func (sc *SoundCollection) Load() {
|
|
|
|
|
for _, sound := range sc.Sounds {
|
|
|
|
|
sc.soundRange += sound.Weight
|
|
|
|
|
sound.Load(sc)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// Random select sound
|
|
|
|
|
func (sc *SoundCollection) Random() *Sound {
|
2017-01-05 23:08:29 +01:00
|
|
|
var (
|
|
|
|
|
i int
|
2017-01-06 16:48:11 +01:00
|
|
|
number = randomRange(0, sc.soundRange)
|
2017-01-05 23:08:29 +01:00
|
|
|
)
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
for _, sound := range sc.Sounds {
|
2017-01-05 23:08:29 +01:00
|
|
|
i += sound.Weight
|
|
|
|
|
|
|
|
|
|
if number < i {
|
|
|
|
|
return sound
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load attempts to load an encoded sound file from disk
|
|
|
|
|
// DCA files are pre-computed sound files that are easy to send to Discord.
|
|
|
|
|
// If you would like to create your own DCA files, please use:
|
|
|
|
|
// https://github.com/nstafie/dca-rs
|
|
|
|
|
// eg: dca-rs --raw -i <input wav file> > <output file>
|
|
|
|
|
func (s *Sound) Load(c *SoundCollection) error {
|
|
|
|
|
path := fmt.Sprintf("audio/%v_%v.dca", c.Prefix, s.Name)
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(path)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println("error opening dca file :", err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var opuslen int16
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println("error reading from dca file :", err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
fmt.Println("error reading from dca file :", err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// append encoded pcm data to the buffer
|
|
|
|
|
s.buffer = append(s.buffer, InBuf)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 16:48:11 +01:00
|
|
|
// Play plays this sound over the specified VoiceConnection
|
2017-01-05 23:08:29 +01:00
|
|
|
func (s *Sound) Play(vc *discordgo.VoiceConnection) {
|
|
|
|
|
vc.Speaking(true)
|
|
|
|
|
defer vc.Speaking(false)
|
|
|
|
|
|
|
|
|
|
for _, buff := range s.buffer {
|
|
|
|
|
vc.OpusSend <- buff
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempts to find the current users voice channel inside a given guild
|
|
|
|
|
func getCurrentVoiceChannel(user *discordgo.User, guild *discordgo.Guild) *discordgo.Channel {
|
|
|
|
|
for _, vs := range guild.VoiceStates {
|
|
|
|
|
if vs.UserID == user.ID {
|
|
|
|
|
channel, _ := discord.State.Channel(vs.ChannelID)
|
|
|
|
|
return channel
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns a random integer between min and max
|
|
|
|
|
func randomRange(min, max int) int {
|
|
|
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
|
|
|
return rand.Intn(max-min) + min
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepares a play
|
|
|
|
|
func createPlay(user *discordgo.User, guild *discordgo.Guild, coll *SoundCollection, sound *Sound) *Play {
|
|
|
|
|
// Grab the users voice channel
|
|
|
|
|
channel := getCurrentVoiceChannel(user, guild)
|
|
|
|
|
if channel == nil {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"user": user.ID,
|
|
|
|
|
"guild": guild.ID,
|
|
|
|
|
}).Warning("Failed to find channel to play sound in")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the play
|
|
|
|
|
play := &Play{
|
|
|
|
|
GuildID: guild.ID,
|
|
|
|
|
ChannelID: channel.ID,
|
|
|
|
|
UserID: user.ID,
|
|
|
|
|
Sound: sound,
|
|
|
|
|
Forced: true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we didn't get passed a manual sound, generate a random one
|
|
|
|
|
if play.Sound == nil {
|
|
|
|
|
play.Sound = coll.Random()
|
|
|
|
|
play.Forced = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the collection is a chained one, set the next sound
|
|
|
|
|
if coll.ChainWith != nil {
|
|
|
|
|
play.Next = &Play{
|
|
|
|
|
GuildID: play.GuildID,
|
|
|
|
|
ChannelID: play.ChannelID,
|
|
|
|
|
UserID: play.UserID,
|
|
|
|
|
Sound: coll.ChainWith.Random(),
|
|
|
|
|
Forced: play.Forced,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return play
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepares and enqueues a play into the ratelimit/buffer guild queue
|
|
|
|
|
func enqueuePlay(user *discordgo.User, guild *discordgo.Guild, coll *SoundCollection, sound *Sound) {
|
|
|
|
|
play := createPlay(user, guild, coll, sound)
|
|
|
|
|
if play == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we already have a connection to this guild
|
|
|
|
|
// yes, this isn't threadsafe, but its "OK" 99% of the time
|
|
|
|
|
_, exists := queues[guild.ID]
|
|
|
|
|
|
|
|
|
|
if exists {
|
2017-01-06 16:48:11 +01:00
|
|
|
if len(queues[guild.ID]) < maxQueueSize {
|
2017-01-05 23:08:29 +01:00
|
|
|
queues[guild.ID] <- play
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2017-01-06 16:48:11 +01:00
|
|
|
queues[guild.ID] = make(chan *Play, maxQueueSize)
|
2017-01-05 23:08:29 +01:00
|
|
|
playSound(play, nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Play a sound
|
|
|
|
|
func playSound(play *Play, vc *discordgo.VoiceConnection) (err error) {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"play": play,
|
|
|
|
|
}).Info("Playing sound")
|
|
|
|
|
|
|
|
|
|
if vc == nil {
|
|
|
|
|
vc, err = discord.ChannelVoiceJoin(play.GuildID, play.ChannelID, false, false)
|
|
|
|
|
// vc.Receive = false
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"error": err,
|
|
|
|
|
}).Error("Failed to play sound")
|
|
|
|
|
delete(queues, play.GuildID)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we need to change channels, do that now
|
|
|
|
|
if vc.ChannelID != play.ChannelID {
|
|
|
|
|
vc.ChangeChannel(play.ChannelID, false, false)
|
|
|
|
|
time.Sleep(time.Millisecond * 125)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sleep for a specified amount of time before playing the sound
|
|
|
|
|
time.Sleep(time.Millisecond * 32)
|
|
|
|
|
|
|
|
|
|
// Play the sound
|
|
|
|
|
play.Sound.Play(vc)
|
|
|
|
|
|
|
|
|
|
// If this is chained, play the chained sound
|
|
|
|
|
if play.Next != nil {
|
|
|
|
|
playSound(play.Next, vc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If there is another song in the queue, recurse and play that
|
|
|
|
|
if len(queues[play.GuildID]) > 0 {
|
2017-01-06 16:48:11 +01:00
|
|
|
play = <-queues[play.GuildID]
|
2017-01-05 23:08:29 +01:00
|
|
|
playSound(play, vc)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the queue is empty, delete it
|
|
|
|
|
time.Sleep(time.Millisecond * time.Duration(play.Sound.PartDelay))
|
|
|
|
|
delete(queues, play.GuildID)
|
|
|
|
|
vc.Disconnect()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func onReady(s *discordgo.Session, event *discordgo.Ready) {
|
2017-01-06 16:48:11 +01:00
|
|
|
log.Info("Received READY payload.")
|
2017-01-05 23:08:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scontains(key string, options ...string) bool {
|
|
|
|
|
for _, item := range options {
|
|
|
|
|
if item == key {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func displayBotStats(cid string) {
|
|
|
|
|
stats := runtime.MemStats{}
|
|
|
|
|
runtime.ReadMemStats(&stats)
|
|
|
|
|
|
|
|
|
|
users := 0
|
|
|
|
|
for _, guild := range discord.State.Ready.Guilds {
|
|
|
|
|
users += len(guild.Members)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w := &tabwriter.Writer{}
|
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
|
|
|
|
|
|
w.Init(buf, 0, 4, 0, ' ', 0)
|
|
|
|
|
fmt.Fprintf(w, "```\n")
|
|
|
|
|
fmt.Fprintf(w, "Discordgo: \t%s\n", discordgo.VERSION)
|
|
|
|
|
fmt.Fprintf(w, "Go: \t%s\n", runtime.Version())
|
|
|
|
|
fmt.Fprintf(w, "Memory: \t%s / %s (%s total allocated)\n", humanize.Bytes(stats.Alloc), humanize.Bytes(stats.Sys), humanize.Bytes(stats.TotalAlloc))
|
|
|
|
|
fmt.Fprintf(w, "Tasks: \t%d\n", runtime.NumGoroutine())
|
|
|
|
|
fmt.Fprintf(w, "Servers: \t%d\n", len(discord.State.Ready.Guilds))
|
|
|
|
|
fmt.Fprintf(w, "Users: \t%d\n", users)
|
|
|
|
|
fmt.Fprintf(w, "```\n")
|
|
|
|
|
w.Flush()
|
|
|
|
|
discord.ChannelMessageSend(cid, buf.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func utilGetMentioned(s *discordgo.Session, m *discordgo.MessageCreate) *discordgo.User {
|
|
|
|
|
for _, mention := range m.Mentions {
|
|
|
|
|
if mention.ID != s.State.Ready.User.ID {
|
|
|
|
|
return mention
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func airhornBomb(cid string, guild *discordgo.Guild, user *discordgo.User, cs string) {
|
|
|
|
|
count, _ := strconv.Atoi(cs)
|
|
|
|
|
discord.ChannelMessageSend(cid, ":ok_hand:"+strings.Repeat(":trumpet:", count))
|
|
|
|
|
|
|
|
|
|
// Cap it at something
|
|
|
|
|
if count > 100 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
play := createPlay(user, guild, AIRHORN, nil)
|
|
|
|
|
vc, err := discord.ChannelVoiceJoin(play.GuildID, play.ChannelID, true, true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < count; i++ {
|
|
|
|
|
AIRHORN.Random().Play(vc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vc.Disconnect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handles bot operator messages, should be refactored (lmao)
|
|
|
|
|
func handleBotControlMessages(s *discordgo.Session, m *discordgo.MessageCreate, parts []string, g *discordgo.Guild) {
|
|
|
|
|
if scontains(parts[1], "status") {
|
|
|
|
|
displayBotStats(m.ChannelID)
|
|
|
|
|
} else if scontains(parts[1], "bomb") && len(parts) >= 4 {
|
|
|
|
|
airhornBomb(m.ChannelID, g, utilGetMentioned(s, m), parts[3])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 18:44:59 +01:00
|
|
|
func setIdleStatus(s *discordgo.Session) {
|
2017-01-07 22:37:47 +01:00
|
|
|
games := []string{
|
|
|
|
|
"Terranigma",
|
|
|
|
|
"Secret of Mana",
|
|
|
|
|
"Quake III Arena",
|
|
|
|
|
"Duke Nukem 3D",
|
|
|
|
|
"Monkey Island 2: LeChuck's Revenge",
|
|
|
|
|
"Turtles in Time",
|
|
|
|
|
"Unreal Tournament",
|
|
|
|
|
"Half-Life",
|
|
|
|
|
"Half-Life 2",
|
|
|
|
|
"Warcraft II",
|
|
|
|
|
"Starcraft",
|
|
|
|
|
"Diablo",
|
|
|
|
|
"Diablo II",
|
|
|
|
|
"A Link to the Past",
|
|
|
|
|
"Ocarina of Time",
|
|
|
|
|
"Star Fox",
|
|
|
|
|
"Tetris",
|
|
|
|
|
"Pokémon Red",
|
|
|
|
|
"Pokémon Blue",
|
|
|
|
|
"Die Siedler II",
|
|
|
|
|
"Day of the Tentacle",
|
|
|
|
|
"Maniac Mansion",
|
|
|
|
|
"Prince of Persia",
|
|
|
|
|
"Super Mario Kart",
|
|
|
|
|
"Pac-Man",
|
|
|
|
|
"Frogger",
|
|
|
|
|
"Donkey Kong",
|
|
|
|
|
"Donkey Kong Country",
|
|
|
|
|
"Asteroids",
|
|
|
|
|
"Doom",
|
|
|
|
|
"Breakout",
|
|
|
|
|
"Street Fighter II",
|
|
|
|
|
"Wolfenstein 3D",
|
|
|
|
|
"Mega Man",
|
|
|
|
|
"Myst",
|
|
|
|
|
"R-Type",
|
|
|
|
|
}
|
2017-01-06 19:10:55 +01:00
|
|
|
for {
|
|
|
|
|
s.UpdateStreamingStatus(1, "", "")
|
|
|
|
|
s.UpdateStatus(0, games[randomRange(0, len(games))])
|
2017-01-06 23:52:43 +01:00
|
|
|
time.Sleep(time.Duration(randomRange(5, 15)) * time.Minute)
|
2017-01-06 19:10:55 +01:00
|
|
|
}
|
2017-01-06 18:44:59 +01:00
|
|
|
}
|
|
|
|
|
|
2017-01-05 23:08:29 +01:00
|
|
|
func onMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
2017-01-05 23:18:00 +01:00
|
|
|
if m.Content == "ping" || m.Content == "pong" {
|
|
|
|
|
// If the message is "ping" reply with "Pong!"
|
|
|
|
|
if m.Content == "ping" {
|
2017-01-06 16:48:11 +01:00
|
|
|
s.ChannelMessageSend(m.ChannelID, "Pong!")
|
2017-01-05 23:18:00 +01:00
|
|
|
}
|
2017-01-05 23:08:29 +01:00
|
|
|
|
2017-01-05 23:18:00 +01:00
|
|
|
// If the message is "pong" reply with "Ping!"
|
|
|
|
|
if m.Content == "pong" {
|
2017-01-06 16:48:11 +01:00
|
|
|
s.ChannelMessageSend(m.ChannelID, "Ping!")
|
2017-01-05 23:18:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Updating bot status
|
|
|
|
|
s.UpdateStatus(0, "Ping Pong with "+m.Author.Username)
|
|
|
|
|
}
|
2017-01-05 23:08:29 +01:00
|
|
|
if len(m.Content) <= 0 || (m.Content[0] != '!' && len(m.Mentions) < 1) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msg := strings.Replace(m.ContentWithMentionsReplaced(), s.State.Ready.User.Username, "username", 1)
|
|
|
|
|
parts := strings.Split(strings.ToLower(msg), " ")
|
|
|
|
|
|
|
|
|
|
channel, _ := discord.State.Channel(m.ChannelID)
|
|
|
|
|
if channel == nil {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"channel": m.ChannelID,
|
|
|
|
|
"message": m.ID,
|
|
|
|
|
}).Warning("Failed to grab channel")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guild, _ := discord.State.Guild(channel.GuildID)
|
|
|
|
|
if guild == nil {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"guild": channel.GuildID,
|
|
|
|
|
"channel": channel,
|
|
|
|
|
"message": m.ID,
|
|
|
|
|
}).Warning("Failed to grab guild")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If this is a mention, it should come from the owner (otherwise we don't care)
|
|
|
|
|
if len(m.Mentions) > 0 && m.Author.ID == OWNER && len(parts) > 0 {
|
|
|
|
|
mentioned := false
|
|
|
|
|
for _, mention := range m.Mentions {
|
|
|
|
|
mentioned = (mention.ID == s.State.Ready.User.ID)
|
|
|
|
|
if mentioned {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mentioned {
|
|
|
|
|
handleBotControlMessages(s, m, parts, guild)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the collection for the command we got
|
|
|
|
|
for _, coll := range COLLECTIONS {
|
|
|
|
|
if scontains(parts[0], coll.Commands...) {
|
|
|
|
|
|
|
|
|
|
// If they passed a specific sound effect, find and select that (otherwise play nothing)
|
|
|
|
|
var sound *Sound
|
|
|
|
|
if len(parts) > 1 {
|
|
|
|
|
for _, s := range coll.Sounds {
|
|
|
|
|
if parts[1] == s.Name {
|
|
|
|
|
sound = s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if sound == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
go enqueuePlay(m.Author, guild, coll, sound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
var (
|
|
|
|
|
Token = flag.String("t", "", "Discord Authentication Token")
|
|
|
|
|
Shard = flag.String("s", "", "Shard ID")
|
|
|
|
|
ShardCount = flag.String("c", "", "Number of shards")
|
|
|
|
|
Owner = flag.String("o", "", "Owner ID")
|
|
|
|
|
err error
|
|
|
|
|
)
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
|
|
if *Owner != "" {
|
|
|
|
|
OWNER = *Owner
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Preload all the sounds
|
|
|
|
|
log.Info("Preloading sounds...")
|
|
|
|
|
for _, coll := range COLLECTIONS {
|
|
|
|
|
coll.Load()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a discord session
|
|
|
|
|
log.Info("Starting discord session...")
|
2017-01-06 16:48:11 +01:00
|
|
|
discord, err = discordgo.New("Bot " + *Token)
|
2017-01-05 23:08:29 +01:00
|
|
|
if err != nil {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"error": err,
|
|
|
|
|
}).Fatal("Failed to create discord session")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set sharding info
|
|
|
|
|
discord.ShardID, _ = strconv.Atoi(*Shard)
|
|
|
|
|
discord.ShardCount, _ = strconv.Atoi(*ShardCount)
|
|
|
|
|
|
|
|
|
|
if discord.ShardCount <= 0 {
|
|
|
|
|
discord.ShardCount = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discord.AddHandler(onReady)
|
|
|
|
|
discord.AddHandler(onMessageCreate)
|
|
|
|
|
|
|
|
|
|
err = discord.Open()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
|
"error": err,
|
|
|
|
|
}).Fatal("Failed to create discord websocket connection")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-06 19:10:55 +01:00
|
|
|
go setIdleStatus(discord)
|
2017-01-05 23:08:29 +01:00
|
|
|
// We're running!
|
2017-01-06 16:48:11 +01:00
|
|
|
log.Info("Gidbig is ready. Quit with CTRL-C.")
|
2017-01-05 23:08:29 +01:00
|
|
|
|
|
|
|
|
// Wait for a signal to quit
|
|
|
|
|
c := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(c, os.Interrupt, os.Kill)
|
|
|
|
|
<-c
|
|
|
|
|
}
|