2023-03-15 14:09:09 +00:00
import * as logger from "./logger.js" ;
import fs from "fs" ;
import format from "format-duration" ;
import { Shoukaku , Connectors } from "shoukaku" ;
import { setTimeout } from "timers/promises" ;
export const players = new Map ( ) ;
export const queues = new Map ( ) ;
export const skipVotes = new Map ( ) ;
export let manager ;
export let nodes = JSON . parse ( fs . readFileSync ( new URL ( "../config/servers.json" , import . meta . url ) , { encoding : "utf8" } ) ) . lava ;
export let connected = false ;
export function connect ( client ) {
manager = new Shoukaku ( new Connectors . OceanicJS ( client ) , nodes , { moveOnDisconnect : true , resume : true , reconnectInterval : 500 , reconnectTries : 1 } ) ;
manager . on ( "error" , ( node , error ) => {
logger . error ( ` An error occurred on Lavalink node ${ node } : ${ error } ` ) ;
} ) ;
manager . on ( "debug" , ( node , info ) => {
logger . debug ( ` Debug event from Lavalink node ${ node } : ${ info } ` ) ;
} ) ;
manager . once ( "ready" , ( ) => {
logger . log ( ` Successfully connected to ${ manager . nodes . size } Lavalink node(s). ` ) ;
connected = true ;
} ) ;
}
export async function reload ( client ) {
if ( ! manager ) connect ( client ) ;
const activeNodes = manager . nodes ;
const json = await fs . promises . readFile ( new URL ( "../config/servers.json" , import . meta . url ) , { encoding : "utf8" } ) ;
nodes = JSON . parse ( json ) . lava ;
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 ) ;
}
}
if ( ! manager . nodes . size ) connected = false ;
return manager . nodes . size ;
}
export async function play ( client , sound , options , music = false ) {
if ( ! connected ) return { content : "I'm not connected to any audio servers!" , flags : 64 } ;
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 } ;
if ( ! options . member . voiceState ) return { content : "You need to be in a voice channel first!" , flags : 64 } ;
if ( ! options . channel . guild . permissionsOf ( client . user . id . toString ( ) ) . has ( "CONNECT" ) ) return { content : "I can't join this voice channel!" , flags : 64 } ;
const voiceChannel = options . channel . guild . channels . get ( options . member . voiceState . channelID ) ? ? await client . rest . channels . get ( options . member . voiceState . channelID ) ;
if ( ! voiceChannel . permissionsOf ( client . user . id . toString ( ) ) . has ( "CONNECT" ) ) return { content : "I don't have permission to join this voice channel!" , flags : 64 } ;
if ( ! music && manager . players . has ( options . channel . guildID ) ) return { content : "I can't play a sound effect while other audio is playing!" , flags : 64 } ;
const node = manager . getNode ( ) ;
if ( ! music && ! nodes . filter ( obj => obj . name === node . name ) [ 0 ] . local ) {
sound = sound . replace ( /\.\// , "https://raw.githubusercontent.com/esmBot/esmBot/master/" ) ;
}
let response ;
try {
response = await node . rest . resolve ( sound ) ;
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 } ;
} catch ( e ) {
logger . error ( e ) ;
return { content : "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit." , flags : 64 } ;
}
const oldQueue = queues . get ( voiceChannel . guildID ) ;
if ( ! response . tracks || response . tracks . length === 0 ) return { content : "I couldn't find that song!" , flags : 64 } ;
if ( process . env . YT _DISABLED === "true" && response . tracks [ 0 ] . info . sourceName === "youtube" ) return "YouTube playback is disabled on this instance." ;
if ( music ) {
const sortedTracks = response . tracks . map ( ( val ) => { return val . track ; } ) ;
const playlistTracks = response . playlistInfo . selectedTrack ? sortedTracks : [ sortedTracks [ 0 ] ] ;
queues . set ( voiceChannel . guildID , oldQueue ? [ ... oldQueue , ... playlistTracks ] : playlistTracks ) ;
}
const playerMeta = players . get ( options . channel . guildID ) ;
let player ;
if ( node . players . has ( voiceChannel . guildID ) ) {
player = node . players . get ( voiceChannel . guildID ) ;
} else if ( playerMeta ? . player ) {
const storedState = playerMeta ? . player ? . connection . state ;
if ( storedState && storedState === 1 ) {
player = playerMeta ? . player ;
}
}
const connection = player ? ? await node . joinChannel ( {
guildId : voiceChannel . guildID ,
channelId : voiceChannel . id ,
shardId : voiceChannel . guild . shard . id ,
deaf : true
} ) ;
if ( oldQueue ? . length && music ) {
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! ` ;
} else {
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 ) ;
return ;
}
}
export async function nextSong ( client , options , connection , track , info , music , voiceChannel , host , loop = false , shuffle = false , lastTrack = null ) {
skipVotes . delete ( voiceChannel . guildID ) ;
const parts = Math . floor ( ( 0 / info . length ) * 10 ) ;
let playingMessage ;
if ( music && lastTrack === track && players . has ( voiceChannel . guildID ) ) {
playingMessage = players . get ( voiceChannel . guildID ) . playMessage ;
} else {
try {
const content = ! music ? { content : "🔊 Playing sound..." } : {
embeds : [ {
color : 16711680 ,
author : {
name : "Now Playing" ,
iconURL : client . user . avatarURL ( )
} ,
fields : [ {
name : "ℹ ️ Title" ,
value : info . title ? . trim ( ) !== "" ? info . title : "(blank)"
} ,
{
name : "🎤 Artist" ,
value : info . author ? . trim ( ) !== "" ? info . author : "(blank)"
} ,
{
name : "💬 Channel" ,
value : voiceChannel . name
} ,
{
name : "🌐 Node" ,
value : connection . node ? . name ? ? "Unknown"
} ,
{
name : ` ${ "▬" . repeat ( parts ) } 🔘 ${ "▬" . repeat ( 10 - parts ) } ` ,
value : ` 0:00/ ${ info . isStream ? "∞" : format ( info . length ) } `
} ]
} ]
} ;
if ( options . type === "classic" ) {
playingMessage = await client . rest . channels . createMessage ( options . channel . id , content ) ;
} else {
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 ( ) ;
}
}
} catch {
// no-op
}
}
connection . removeAllListeners ( "exception" ) ;
connection . removeAllListeners ( "stuck" ) ;
connection . removeAllListeners ( "end" ) ;
connection . setVolume ( 0.70 ) ;
connection . playTrack ( { track } ) ;
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 ) ) ;
connection . on ( "stuck" , ( ) => {
const nodeName = manager . getNode ( ) . name ;
connection . move ( nodeName ) ;
connection . resume ( ) ;
} ) ;
connection . on ( "end" , async ( data ) => {
if ( data . reason === "REPLACED" ) return ;
let queue = queues . get ( voiceChannel . guildID ) ;
const player = players . get ( voiceChannel . guildID ) ;
if ( player && process . env . STAYVC === "true" ) {
player . type = "idle" ;
players . set ( voiceChannel . guildID , player ) ;
}
let newQueue ;
if ( player ? . shuffle ) {
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 ;
} else if ( player ? . loop ) {
queue . push ( queue . shift ( ) ) ;
newQueue = queue ;
} else {
newQueue = queue ? queue . slice ( 1 ) : [ ] ;
}
queues . set ( voiceChannel . guildID , newQueue ) ;
if ( newQueue . length !== 0 ) {
const newTrack = await connection . node . rest . decode ( newQueue [ 0 ] ) ;
nextSong ( client , options , connection , newQueue [ 0 ] , newTrack , music , voiceChannel , host , player . loop , player . shuffle , track ) ;
try {
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 ( ) ;
}
} catch {
// no-op
}
} else if ( process . env . STAYVC !== "true" ) {
await setTimeout ( 400 ) ;
connection . node . leaveChannel ( voiceChannel . guildID ) ;
players . delete ( voiceChannel . guildID ) ;
queues . delete ( voiceChannel . guildID ) ;
skipVotes . delete ( voiceChannel . guildID ) ;
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 {
if ( ( Date . now ( ) - options . interaction . createdAt ) >= 900000 ) {
await client . rest . channels . createMessage ( options . channel . id , { content } ) ;
} else {
await options . interaction . createFollowup ( { content } ) ;
}
}
} catch {
// no-op
}
}
if ( options . type === "classic" ) {
try {
if ( playingMessage . channel . messages . has ( playingMessage . id ) ) await playingMessage . delete ( ) ;
if ( player ? . playMessage . channel . messages . has ( player . playMessage . id ) ) await player . playMessage . delete ( ) ;
} catch {
// no-op
}
}
} ) ;
}
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
}
connection . removeAllListeners ( "exception" ) ;
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 {
if ( ( Date . now ( ) - options . interaction . createdAt ) >= 900000 ) {
await client . rest . channels . createMessage ( options . channel . id , { content } ) ;
} else {
await options . interaction . createFollowup ( { content } ) ;
}
}
} catch {
// no-op
}
2022-10-25 03:03:57 +00:00
}