2021-07-28 17:30:01 +02:00
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"dcbot/dca0"
"dcbot/util"
"github.com/bwmarrin/discordgo"
)
////////////////////////////////
// Helper functions.
////////////////////////////////
// The first return value contains the voice channel ID, if it was found. If it
// was not found, it is set to "".
// The second return value indicates whether the voice channel was found.
func GetUserVoiceChannel ( g * discordgo . Guild , userID string ) ( string , bool ) {
for _ , vs := range g . VoiceStates {
if vs . UserID == userID {
return vs . ChannelID , true
}
}
return "" , false
}
////////////////////////////////
// Structs.
////////////////////////////////
type Playback struct {
Track
CmdCh chan dca0 . Command
RespCh chan dca0 . Response
Paused bool
Loop bool // Whether playback is looping right now.
}
type Track struct {
Title string // Title, if any.
Url string // Short URL, for example from YouTube.
MediaUrl string // Long URL of the associated media file.
}
// All methods of Client are thread safe, however manual locking is required
// when accessing any fields.
type Client struct {
sync . RWMutex
// The discordgo session.
s * discordgo . Session
// TextChannelID and VoiceChannelID indicate the current channels through
// which the bot should send text / audio. They may be set to "".
TextChannelID string
VoiceChannelID string
// Current audio playback.
Playback * Playback
// Queue.
Queue [ ] * Track
}
func NewClient ( s * discordgo . Session ) * Client {
return & Client {
s : s ,
}
}
func ( c * Client ) Messagef ( format string , a ... interface { } ) {
c . RLock ( )
if c . TextChannelID == "" {
fmt . Printf ( format + "\n" , a ... )
} else {
c . s . ChannelMessageSend ( c . TextChannelID , fmt . Sprintf ( format , a ... ) )
}
c . RUnlock ( )
}
// Updates the text channel and voice channel IDs. May set them to "" if there
// are none associated with the message.
func ( c * Client ) UpdateChannels ( g * discordgo . Guild , m * discordgo . Message ) {
c . Lock ( )
c . TextChannelID = m . ChannelID
vc , _ := GetUserVoiceChannel ( g , m . Author . ID )
c . VoiceChannelID = vc
c . Unlock ( )
}
func ( c * Client ) GetTextChannelID ( ) string {
c . RLock ( )
ret := c . TextChannelID
c . RUnlock ( )
return ret
}
func ( c * Client ) GetVoiceChannelID ( ) string {
c . RLock ( )
ret := c . TextChannelID
c . RUnlock ( )
return ret
}
// Returns a COPY of c.Playback. Modifications to the returned Playback struct
// are NOT preserved, as it is a copy.
func ( c * Client ) GetPlaybackInfo ( ) ( p Playback , ok bool ) {
c . RLock ( )
ret := c . Playback
c . RUnlock ( )
if ret == nil {
return Playback { } , false
} else {
return * ret , true
}
}
func ( c * Client ) QueueLen ( ) int {
c . RLock ( )
l := len ( c . Queue )
c . RUnlock ( )
return l
}
// Similarly to GetPlaybackInfo, this function returns a COPY. Any modifications
// are NOT preserved.
// ok field returns false if the index is out of bounds.
func ( c * Client ) QueueAt ( i int ) ( t Track , ok bool ) {
l := c . QueueLen ( )
2021-07-28 19:32:28 +02:00
if i >= l || i < 0 {
2021-07-28 17:30:01 +02:00
return Track { } , false
}
c . RLock ( )
ret := c . Queue [ i ]
c . RUnlock ( )
return * ret , true
}
func ( c * Client ) QueuePushBack ( t * Track ) {
c . Lock ( )
c . Queue = append ( c . Queue , t )
c . Unlock ( )
}
func ( c * Client ) QueuePushFront ( t * Track ) {
c . Lock ( )
c . Queue = append ( [ ] * Track { t } , c . Queue ... )
c . Unlock ( )
}
func ( c * Client ) QueuePopFront ( ) ( t Track , ok bool ) {
t , ok = c . QueueAt ( 0 )
if ok {
c . Lock ( )
c . Queue = c . Queue [ 1 : ]
c . Unlock ( )
}
return t , ok
}
// Deletes a single item at any position.
// Returns false if i was out of bounds.
func ( c * Client ) QueueDelete ( i int ) bool {
if i >= c . QueueLen ( ) {
return false
}
c . Lock ( )
c . Queue = append ( c . Queue [ : i ] , c . Queue [ i + 1 : ] ... )
c . Unlock ( )
return true
}
// Swaps two items in the queue.
// Returns false if a or b is out of bounds.
func ( c * Client ) QueueSwap ( a , b int ) bool {
if a == b {
return true
}
l := c . QueueLen ( )
2021-07-28 19:32:28 +02:00
if a >= l || b >= l || a < 0 || b < 0 {
2021-07-28 17:30:01 +02:00
return false
}
c . Lock ( )
c . Queue [ a ] , c . Queue [ b ] = c . Queue [ b ] , c . Queue [ a ]
c . Unlock ( )
return true
}
func ( c * Client ) QueueClear ( ) {
c . Lock ( )
c . Queue = nil
c . Unlock ( )
}
////////////////////////////////
// Global variables.
////////////////////////////////
var clients map [ string ] * Client // Guild ID to client
var mClients sync . Mutex
var cfg Config
////////////////////////////////
// Main program.
////////////////////////////////
func main ( ) {
if err := ReadConfig ( & cfg ) ; err != nil {
fmt . Println ( err )
if err := WriteDefaultConfig ( ) ; err != nil {
fmt . Println ( "Failed to create the default configuration file:" , err )
return
}
fmt . Println ( "Wrote the default configuration to " + configFile + "." )
fmt . Println ( "You will have to manually configure the token by editing " + configFile + "." )
return
}
if cfg . Token == tokenDefaultString {
fmt . Println ( "Please set your bot token in " + configFile + " first." )
return
}
// Check if all binary dependencies are installed correctly.
const notInstalledErrMsg = "Unable to find %s in the specified path '%s', please make sure it's installed correctly.\nYou can manually set its path by editing %s\n"
if ! util . CheckInstalled ( cfg . YtdlPath , "--version" ) {
fmt . Printf ( notInstalledErrMsg , "youtube-dl" , cfg . YtdlPath , configFile )
return
}
if ! util . CheckInstalled ( cfg . FfmpegPath , "-version" ) {
fmt . Printf ( notInstalledErrMsg , "ffmpeg" , cfg . FfmpegPath , configFile )
return
}
// Initialize client map.
clients = make ( map [ string ] * Client )
// Initialize bot.
dg , err := discordgo . New ( "Bot " + cfg . Token )
if err != nil {
fmt . Println ( "Error creating Discord session:" , err )
return
}
dg . AddHandler ( ready )
dg . AddHandler ( banAdd )
dg . AddHandler ( messageCreate )
// What information we need about guilds.
dg . Identify . Intents = discordgo . IntentsGuilds | discordgo . IntentsGuildMessages | discordgo . IntentsGuildVoiceStates | discordgo . IntentsGuildBans
// Open the websocket and begin listening.
err = dg . Open ( )
if err != nil {
fmt . Println ( "Error opening Discord session:" , err )
return
}
// Wait here until Ctrl+c or other term signal is received.
fmt . Println ( "Bot is now running. Press Ctrl+c to exit." )
sc := make ( chan os . Signal , 1 )
signal . Notify ( sc , syscall . SIGINT , syscall . SIGTERM , os . Interrupt , os . Kill )
<- sc
fmt . Println ( "\nSignal received, closing Discord session." )
// Cleanly close down the Discord session.
dg . Close ( )
}
func ready ( s * discordgo . Session , event * discordgo . Ready ) {
u := s . State . User
fmt . Println ( "Logged in as" , u . Username + "#" + u . Discriminator + "." )
s . UpdateListeningStatus ( cfg . Prefix + "help" )
}
func banAdd ( s * discordgo . Session , event * discordgo . GuildBanAdd ) {
fmt . Println ( event )
}
func messageCreate ( s * discordgo . Session , m * discordgo . MessageCreate ) {
// Ignore messages from the bot itself.
if m . Author . ID == s . State . User . ID {
return
}
var g * discordgo . Guild
g , err := s . State . Guild ( m . GuildID )
if err != nil {
// Could not find guild.
s . ChannelMessageSend ( m . ChannelID , "This bot only works in guilds (servers)." )
return
}
var c * Client
mClients . Lock ( )
{
var ok bool
if c , ok = clients [ m . GuildID ] ; ! ok {
c = NewClient ( s )
clients [ m . GuildID ] = c
}
}
mClients . Unlock ( )
// Update the text and voice channels associated with the client.
c . UpdateChannels ( g , m . Message )
args , ok := CmdGetArgs ( m . Content )
if ! ok {
// Not a command.
return
}
if len ( args ) == 0 {
c . Messagef ( "No command specified. Type `%shelp` for help." , cfg . Prefix )
return
}
switch args [ 0 ] {
case "help" :
commandHelp ( c )
case "play" :
commandPlay ( s , g , c , args [ 1 : ] )
case "seek" :
commandSeek ( c , args [ 1 : ] )
case "pos" :
commandPos ( c )
case "loop" :
commandLoop ( c )
case "add" :
commandAdd ( c , args [ 1 : ] , false )
case "queue" :
commandQueue ( c )
case "pause" :
commandPause ( c )
case "stop" :
commandStop ( c )
case "skip" :
commandSkip ( c )
case "delete" :
commandDelete ( c , args [ 1 : ] )
case "swap" :
commandSwap ( c , args [ 1 : ] )
case "shuffle" :
commandShuffle ( c )
}
}