2021-08-19 14:19:14 +00:00
import * as logger from "./logger.js" ;
import fs from "fs" ;
import format from "format-duration" ;
2022-06-14 05:38:01 +00:00
import { Shoukaku , Connectors } from "shoukaku" ;
2022-08-26 20:53:38 +00:00
import { setTimeout } from "timers/promises" ;
2020-06-27 17:18:26 +00:00
2021-08-19 14:19:14 +00:00
export const players = new Map ( ) ;
export const queues = new Map ( ) ;
export const skipVotes = new Map ( ) ;
2020-07-06 20:19:30 +00:00
2021-08-19 14:19:14 +00:00
export let manager ;
2022-10-11 15:46:10 +00:00
export let nodes = JSON . parse ( fs . readFileSync ( new URL ( "../config/servers.json" , import . meta . url ) , { encoding : "utf8" } ) ) . lava ;
2021-08-19 14:19:14 +00:00
export let connected = false ;
2020-07-16 14:28:09 +00:00
2022-09-11 04:18:44 +00:00
export function connect ( client ) {
2022-09-24 03:25:16 +00:00
manager = new Shoukaku ( new Connectors . OceanicJS ( client ) , nodes , { moveOnDisconnect : true , resume : true , reconnectInterval : 500 , reconnectTries : 1 } ) ;
2022-06-14 05:38:01 +00:00
manager . on ( "error" , ( node , error ) => {
2020-06-27 17:18:26 +00:00
logger . error ( ` An error occurred on Lavalink node ${ node } : ${ error } ` ) ;
} ) ;
2022-10-25 03:03:57 +00:00
manager . on ( "debug" , ( node , info ) => {
logger . debug ( ` Debug event from Lavalink node ${ node } : ${ info } ` ) ;
} ) ;
2022-06-28 05:40:54 +00:00
manager . once ( "ready" , ( ) => {
2022-06-14 05:38:01 +00:00
logger . log ( ` Successfully connected to ${ manager . nodes . size } Lavalink node(s). ` ) ;
connected = true ;
} ) ;
}
2022-10-11 15:46:10 +00:00
export async function reload ( client ) {
if ( ! manager ) connect ( client ) ;
2022-06-14 05:38:01 +00:00
const activeNodes = manager . nodes ;
2022-10-11 15:46:10 +00:00
const json = await fs . promises . readFile ( new URL ( "../config/servers.json" , import . meta . url ) , { encoding : "utf8" } ) ;
nodes = JSON . parse ( json ) . lava ;
2022-06-14 05:38:01 +00:00
const names = nodes . map ( ( a ) => a . name ) ;
for ( const name in activeNodes ) {
if ( ! names . includes ( name ) ) {
manager . removeNode ( name ) ;
}
}
for ( const node of nodes ) {
if ( ! activeNodes . has ( node . name ) ) {
manager . addNode ( node ) ;
}
}
2022-10-11 15:46:10 +00:00
if ( ! manager . nodes . size ) connected = false ;
2022-06-14 05:38:01 +00:00
return manager . nodes . size ;
2021-08-19 14:19:14 +00:00
}
2020-06-27 17:18:26 +00:00
2022-04-05 03:05:28 +00:00
export async function play ( client , sound , options , music = false ) {
2022-10-11 15:46:10 +00:00
if ( ! connected ) return { content : "I'm not connected to any audio servers!" , flags : 64 } ;
2022-09-01 01:00:34 +00:00
if ( ! manager ) return { content : "The sound commands are still starting up!" , flags : 64 } ;
if ( ! options . channel . guild ) return { content : "This command only works in servers!" , flags : 64 } ;
2022-10-04 18:23:23 +00:00
if ( ! options . member . voiceState ) return { content : "You need to be in a voice channel first!" , flags : 64 } ;
2022-09-24 04:50:59 +00:00
if ( ! options . channel . guild . permissionsOf ( client . user . id . toString ( ) ) . has ( "CONNECT" ) ) return { content : "I can't join this voice channel!" , flags : 64 } ;
2022-11-26 19:31:40 +00:00
const voiceChannel = options . channel . guild . channels . get ( options . member . voiceState . channelID ) ? ? await client . rest . channels . get ( options . member . voiceState . channelID ) ;
2022-09-24 04:50:59 +00:00
if ( ! voiceChannel . permissionsOf ( client . user . id . toString ( ) ) . has ( "CONNECT" ) ) return { content : "I don't have permission to join this voice channel!" , flags : 64 } ;
2022-09-24 03:25:16 +00:00
if ( ! music && manager . players . has ( options . channel . guildID ) ) return { content : "I can't play a sound effect while other audio is playing!" , flags : 64 } ;
2022-10-11 15:46:10 +00:00
const node = manager . getNode ( ) ;
2022-06-14 05:38:01 +00:00
if ( ! music && ! nodes . filter ( obj => obj . name === node . name ) [ 0 ] . local ) {
2021-01-04 16:29:18 +00:00
sound = sound . replace ( /\.\// , "https://raw.githubusercontent.com/esmBot/esmBot/master/" ) ;
}
2022-06-14 05:38:01 +00:00
let response ;
2022-01-08 21:54:34 +00:00
try {
2022-06-14 05:38:01 +00:00
response = await node . rest . resolve ( sound ) ;
2022-09-01 01:00:34 +00:00
if ( ! response ) return { content : "🔊 I couldn't get a response from the audio server." , flags : 64 } ;
if ( response . loadType === "NO_MATCHES" || response . loadType === "LOAD_FAILED" ) return { content : "I couldn't find that song!" , flags : 64 } ;
2022-07-22 05:22:23 +00:00
} catch ( e ) {
logger . error ( e ) ;
2022-09-01 01:00:34 +00:00
return { content : "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit." , flags : 64 } ;
2022-01-08 21:54:34 +00:00
}
2022-09-24 03:25:16 +00:00
const oldQueue = queues . get ( voiceChannel . guildID ) ;
2022-09-01 01:00:34 +00:00
if ( ! response . tracks || response . tracks . length === 0 ) return { content : "I couldn't find that song!" , flags : 64 } ;
2022-10-10 16:53:45 +00:00
if ( process . env . YT _DISABLED === "true" && response . tracks [ 0 ] . info . sourceName === "youtube" ) return "YouTube playback is disabled on this instance." ;
2020-09-22 20:33:07 +00:00
if ( music ) {
2022-06-14 05:38:01 +00:00
const sortedTracks = response . tracks . map ( ( val ) => { return val . track ; } ) ;
const playlistTracks = response . playlistInfo . selectedTrack ? sortedTracks : [ sortedTracks [ 0 ] ] ;
2022-09-24 03:25:16 +00:00
queues . set ( voiceChannel . guildID , oldQueue ? [ ... oldQueue , ... playlistTracks ] : playlistTracks ) ;
2020-09-22 20:33:07 +00:00
}
2022-09-24 03:25:16 +00:00
const playerMeta = players . get ( options . channel . guildID ) ;
2022-07-23 21:02:04 +00:00
let player ;
2022-09-24 03:25:16 +00:00
if ( node . players . has ( voiceChannel . guildID ) ) {
player = node . players . get ( voiceChannel . guildID ) ;
2022-07-23 21:02:04 +00:00
} else if ( playerMeta ? . player ) {
const storedState = playerMeta ? . player ? . connection . state ;
if ( storedState && storedState === 1 ) {
player = playerMeta ? . player ;
}
}
const connection = player ? ? await node . joinChannel ( {
2022-09-24 03:25:16 +00:00
guildId : voiceChannel . guildID ,
2022-06-14 05:38:01 +00:00
channelId : voiceChannel . id ,
shardId : voiceChannel . guild . shard . id ,
deaf : true
} ) ;
2020-07-06 20:19:30 +00:00
2022-07-23 21:02:04 +00:00
if ( oldQueue ? . length && music ) {
2022-06-14 05:38:01 +00:00
return ` Your ${ response . playlistInfo . name ? "playlist" : "tune" } \` ${ response . playlistInfo . name ? response . playlistInfo . name . trim ( ) : ( response . tracks [ 0 ] . info . title !== "" ? response . tracks [ 0 ] . info . title . trim ( ) : "(blank)" ) } \` has been added to the queue! ` ;
2019-09-13 20:02:41 +00:00
} else {
2022-07-23 21:02:04 +00:00
nextSong ( client , options , connection , response . tracks [ 0 ] . track , response . tracks [ 0 ] . info , music , voiceChannel , playerMeta ? . host ? ? options . member . id , playerMeta ? . loop ? ? false , playerMeta ? . shuffle ? ? false ) ;
2020-12-11 19:52:02 +00:00
return ;
2019-09-13 20:02:41 +00:00
}
2021-08-19 14:19:14 +00:00
}
2020-07-06 20:19:30 +00:00
2022-04-05 03:05:28 +00:00
export async function nextSong ( client , options , connection , track , info , music , voiceChannel , host , loop = false , shuffle = false , lastTrack = null ) {
2022-09-24 03:25:16 +00:00
skipVotes . delete ( voiceChannel . guildID ) ;
2020-07-06 20:19:30 +00:00
const parts = Math . floor ( ( 0 / info . length ) * 10 ) ;
Class commands, improved sharding, and many other changes (#88)
* Load commands recursively
* Sort commands
* Missed a couple of spots
* missed even more spots apparently
* Ported commands in "fun" category to new class-based format, added babel eslint plugin
* Ported general commands, removed old/unneeded stuff, replaced moment with day, many more fixes I lost track of
* Missed a spot
* Removed unnecessary abort-controller package, add deprecation warning for mongo database
* Added imagereload, clarified premature end message
* Fixed docker-compose path issue, added total bot uptime to stats, more fixes for various parts
* Converted image commands into classes, fixed reload, ignore another WS event, cleaned up command handler and image runner
* Converted music/soundboard commands to class format
* Cleanup unnecessary logs
* awful tag command class port
* I literally somehow just learned that you can leave out the constructor in classes
* Pass client directly to commands/events, cleaned up command handler
* Migrated bot to eris-sharder, fixed some error handling stuff
* Remove unused modules
* Fixed type returning
* Switched back to Eris stable
* Some fixes and cleanup
* might wanna correct this
* Implement image command ratelimiting
* Added Bot token prefix, added imagestats, added running endpoint to API
2021-04-12 16:16:12 +00:00
let playingMessage ;
2022-09-24 03:25:16 +00:00
if ( music && lastTrack === track && players . has ( voiceChannel . guildID ) ) {
playingMessage = players . get ( voiceChannel . guildID ) . playMessage ;
Class commands, improved sharding, and many other changes (#88)
* Load commands recursively
* Sort commands
* Missed a couple of spots
* missed even more spots apparently
* Ported commands in "fun" category to new class-based format, added babel eslint plugin
* Ported general commands, removed old/unneeded stuff, replaced moment with day, many more fixes I lost track of
* Missed a spot
* Removed unnecessary abort-controller package, add deprecation warning for mongo database
* Added imagereload, clarified premature end message
* Fixed docker-compose path issue, added total bot uptime to stats, more fixes for various parts
* Converted image commands into classes, fixed reload, ignore another WS event, cleaned up command handler and image runner
* Converted music/soundboard commands to class format
* Cleanup unnecessary logs
* awful tag command class port
* I literally somehow just learned that you can leave out the constructor in classes
* Pass client directly to commands/events, cleaned up command handler
* Migrated bot to eris-sharder, fixed some error handling stuff
* Remove unused modules
* Fixed type returning
* Switched back to Eris stable
* Some fixes and cleanup
* might wanna correct this
* Implement image command ratelimiting
* Added Bot token prefix, added imagestats, added running endpoint to API
2021-04-12 16:16:12 +00:00
} else {
2022-01-06 00:48:08 +00:00
try {
2022-09-24 03:25:16 +00:00
const content = ! music ? { content : "🔊 Playing sound..." } : {
2022-01-06 00:48:08 +00:00
embeds : [ {
color : 16711680 ,
author : {
name : "Now Playing" ,
2022-09-24 16:26:56 +00:00
iconURL : client . user . avatarURL ( )
2022-01-06 00:48:08 +00:00
} ,
fields : [ {
2022-08-25 15:36:12 +00:00
name : "ℹ ️ Title" ,
2022-07-23 21:02:04 +00:00
value : info . title ? . trim ( ) !== "" ? info . title : "(blank)"
2022-01-06 00:48:08 +00:00
} ,
{
2022-08-25 15:36:12 +00:00
name : "🎤 Artist" ,
2022-07-23 21:02:04 +00:00
value : info . author ? . trim ( ) !== "" ? info . author : "(blank)"
2022-01-06 00:48:08 +00:00
} ,
{
2022-08-25 15:36:12 +00:00
name : "💬 Channel" ,
2022-01-06 00:48:08 +00:00
value : voiceChannel . name
} ,
2022-07-22 05:22:23 +00:00
{
2022-08-25 15:36:12 +00:00
name : "🌐 Node" ,
2022-07-23 21:02:04 +00:00
value : connection . node ? . name ? ? "Unknown"
2022-07-22 05:22:23 +00:00
} ,
2022-01-06 00:48:08 +00:00
{
name : ` ${ "▬" . repeat ( parts ) } 🔘 ${ "▬" . repeat ( 10 - parts ) } ` ,
value : ` 0:00/ ${ info . isStream ? "∞" : format ( info . length ) } `
} ]
Class commands, improved sharding, and many other changes (#88)
* Load commands recursively
* Sort commands
* Missed a couple of spots
* missed even more spots apparently
* Ported commands in "fun" category to new class-based format, added babel eslint plugin
* Ported general commands, removed old/unneeded stuff, replaced moment with day, many more fixes I lost track of
* Missed a spot
* Removed unnecessary abort-controller package, add deprecation warning for mongo database
* Added imagereload, clarified premature end message
* Fixed docker-compose path issue, added total bot uptime to stats, more fixes for various parts
* Converted image commands into classes, fixed reload, ignore another WS event, cleaned up command handler and image runner
* Converted music/soundboard commands to class format
* Cleanup unnecessary logs
* awful tag command class port
* I literally somehow just learned that you can leave out the constructor in classes
* Pass client directly to commands/events, cleaned up command handler
* Migrated bot to eris-sharder, fixed some error handling stuff
* Remove unused modules
* Fixed type returning
* Switched back to Eris stable
* Some fixes and cleanup
* might wanna correct this
* Implement image command ratelimiting
* Added Bot token prefix, added imagestats, added running endpoint to API
2021-04-12 16:16:12 +00:00
} ]
2022-04-05 03:05:28 +00:00
} ;
if ( options . type === "classic" ) {
2022-09-24 03:25:16 +00:00
playingMessage = await client . rest . channels . createMessage ( options . channel . id , content ) ;
2022-04-05 03:05:28 +00:00
} else {
2022-10-30 05:17:36 +00:00
if ( ( Date . now ( ) - options . interaction . createdAt ) >= 900000 ) { // discord interactions are only valid for 15 minutes
playingMessage = await client . rest . channels . createMessage ( options . channel . id , content ) ;
} else if ( lastTrack && lastTrack !== track ) {
playingMessage = await options . interaction . createFollowup ( content ) ;
} else {
playingMessage = await options . interaction [ options . interaction . acknowledged ? "editOriginal" : "createMessage" ] ( content ) ;
if ( ! playingMessage ) playingMessage = await options . interaction . getOriginal ( ) ;
}
2022-04-05 03:05:28 +00:00
}
2022-01-06 00:48:08 +00:00
} catch {
// no-op
}
Class commands, improved sharding, and many other changes (#88)
* Load commands recursively
* Sort commands
* Missed a couple of spots
* missed even more spots apparently
* Ported commands in "fun" category to new class-based format, added babel eslint plugin
* Ported general commands, removed old/unneeded stuff, replaced moment with day, many more fixes I lost track of
* Missed a spot
* Removed unnecessary abort-controller package, add deprecation warning for mongo database
* Added imagereload, clarified premature end message
* Fixed docker-compose path issue, added total bot uptime to stats, more fixes for various parts
* Converted image commands into classes, fixed reload, ignore another WS event, cleaned up command handler and image runner
* Converted music/soundboard commands to class format
* Cleanup unnecessary logs
* awful tag command class port
* I literally somehow just learned that you can leave out the constructor in classes
* Pass client directly to commands/events, cleaned up command handler
* Migrated bot to eris-sharder, fixed some error handling stuff
* Remove unused modules
* Fixed type returning
* Switched back to Eris stable
* Some fixes and cleanup
* might wanna correct this
* Implement image command ratelimiting
* Added Bot token prefix, added imagestats, added running endpoint to API
2021-04-12 16:16:12 +00:00
}
2022-06-29 02:54:15 +00:00
connection . removeAllListeners ( "exception" ) ;
2022-07-22 05:22:23 +00:00
connection . removeAllListeners ( "stuck" ) ;
2021-09-13 22:24:15 +00:00
connection . removeAllListeners ( "end" ) ;
2022-08-25 15:36:12 +00:00
connection . setVolume ( 0.70 ) ;
2022-06-14 05:38:01 +00:00
connection . playTrack ( { track } ) ;
2022-10-25 03:03:57 +00:00
players . set ( voiceChannel . guildID , { player : connection , type : music ? "music" : "sound" , host , voiceChannel , originalChannel : options . channel , loop , shuffle , playMessage : playingMessage } ) ;
connection . once ( "exception" , ( exception ) => errHandle ( exception , client , connection , playingMessage , voiceChannel , options ) ) ;
2022-07-22 05:22:23 +00:00
connection . on ( "stuck" , ( ) => {
const nodeName = manager . getNode ( ) . name ;
connection . move ( nodeName ) ;
connection . resume ( ) ;
} ) ;
2021-09-13 22:24:15 +00:00
connection . on ( "end" , async ( data ) => {
if ( data . reason === "REPLACED" ) return ;
2022-09-24 03:25:16 +00:00
let queue = queues . get ( voiceChannel . guildID ) ;
const player = players . get ( voiceChannel . guildID ) ;
2021-12-12 05:44:49 +00:00
if ( player && process . env . STAYVC === "true" ) {
player . type = "idle" ;
2022-09-24 03:25:16 +00:00
players . set ( voiceChannel . guildID , player ) ;
2021-12-12 05:44:49 +00:00
}
2021-09-13 22:24:15 +00:00
let newQueue ;
2022-07-23 21:02:04 +00:00
if ( player ? . shuffle ) {
2021-09-20 17:26:40 +00:00
if ( player . loop ) {
queue . push ( queue . shift ( ) ) ;
} else {
queue = queue . slice ( 1 ) ;
}
queue . unshift ( queue . splice ( Math . floor ( Math . random ( ) * queue . length ) , 1 ) [ 0 ] ) ;
newQueue = queue ;
2022-07-23 21:02:04 +00:00
} else if ( player ? . loop ) {
2021-09-13 22:24:15 +00:00
queue . push ( queue . shift ( ) ) ;
newQueue = queue ;
} else {
newQueue = queue ? queue . slice ( 1 ) : [ ] ;
}
2022-09-24 03:25:16 +00:00
queues . set ( voiceChannel . guildID , newQueue ) ;
2021-12-12 05:44:49 +00:00
if ( newQueue . length !== 0 ) {
2022-06-14 05:38:01 +00:00
const newTrack = await connection . node . rest . decode ( newQueue [ 0 ] ) ;
2022-04-05 03:05:28 +00:00
nextSong ( client , options , connection , newQueue [ 0 ] , newTrack , music , voiceChannel , host , player . loop , player . shuffle , track ) ;
2021-12-12 05:44:49 +00:00
try {
2022-09-09 19:55:03 +00:00
if ( options . type === "classic" ) {
if ( newQueue [ 0 ] !== track && playingMessage . channel . messages . has ( playingMessage . id ) ) await playingMessage . delete ( ) ;
if ( newQueue [ 0 ] !== track && player . playMessage . channel . messages . has ( player . playMessage . id ) ) await player . playMessage . delete ( ) ;
}
2021-12-12 05:44:49 +00:00
} catch {
// no-op
}
} else if ( process . env . STAYVC !== "true" ) {
2022-08-26 20:53:38 +00:00
await setTimeout ( 400 ) ;
2022-09-24 03:25:16 +00:00
connection . node . leaveChannel ( voiceChannel . guildID ) ;
players . delete ( voiceChannel . guildID ) ;
queues . delete ( voiceChannel . guildID ) ;
skipVotes . delete ( voiceChannel . guildID ) ;
2022-09-24 16:26:56 +00:00
try {
const content = ` 🔊 The voice channel session in \` ${ voiceChannel . name } \` has ended. ` ;
if ( options . type === "classic" ) {
await client . rest . channels . createMessage ( options . channel . id , { content } ) ;
} else {
2022-10-30 05:17:36 +00:00
if ( ( Date . now ( ) - options . interaction . createdAt ) >= 900000 ) {
await client . rest . channels . createMessage ( options . channel . id , { content } ) ;
} else {
await options . interaction . createFollowup ( { content } ) ;
}
2022-09-24 16:26:56 +00:00
}
} catch {
// no-op
2022-06-11 20:23:41 +00:00
}
2022-09-09 19:55:03 +00:00
}
if ( options . type === "classic" ) {
2021-09-13 22:24:15 +00:00
try {
2022-03-17 19:28:35 +00:00
if ( playingMessage . channel . messages . has ( playingMessage . id ) ) await playingMessage . delete ( ) ;
2022-07-23 21:02:04 +00:00
if ( player ? . playMessage . channel . messages . has ( player . playMessage . id ) ) await player . playMessage . delete ( ) ;
2021-09-13 22:24:15 +00:00
} catch {
// no-op
2020-11-05 21:40:18 +00:00
}
2021-09-13 22:24:15 +00:00
}
} ) ;
2021-12-12 05:44:49 +00:00
}
2022-10-25 03:03:57 +00:00
export async function errHandle ( exception , client , connection , playingMessage , voiceChannel , options , closed ) {
try {
if ( playingMessage . channel . messages . has ( playingMessage . id ) ) await playingMessage . delete ( ) ;
const playMessage = players . get ( voiceChannel . guildID ) . playMessage ;
if ( playMessage . channel . messages . has ( playMessage . id ) ) await playMessage . delete ( ) ;
} catch {
// no-op
}
players . delete ( voiceChannel . guildID ) ;
queues . delete ( voiceChannel . guildID ) ;
skipVotes . delete ( voiceChannel . guildID ) ;
logger . error ( exception ) ;
try {
connection . node . leaveChannel ( voiceChannel . guildID ) ;
} catch {
// no-op
}
2022-10-30 05:17:36 +00:00
connection . removeAllListeners ( "exception" ) ;
2022-10-25 03:03:57 +00:00
connection . removeAllListeners ( "stuck" ) ;
connection . removeAllListeners ( "end" ) ;
try {
const content = closed ? ` 🔊 I got disconnected by Discord and tried to reconnect; however, I got this error instead: \n \` \` \` ${ exception } \` \` \` ` : ` 🔊 Looks like there was an error regarding sound playback: \n \` \` \` ${ exception . type } : ${ exception . error } \` \` \` ` ;
if ( options . type === "classic" ) {
await client . rest . channels . createMessage ( playingMessage . channel . id , { content } ) ;
} else {
2022-10-30 05:17:36 +00:00
if ( ( Date . now ( ) - options . interaction . createdAt ) >= 900000 ) {
await client . rest . channels . createMessage ( options . channel . id , { content } ) ;
} else {
await options . interaction . createFollowup ( { content } ) ;
}
2022-10-25 03:03:57 +00:00
}
} catch {
// no-op
}
}