discord-playback-bot/bot.js

274 lines
7.6 KiB
JavaScript
Raw Permalink Normal View History

2026-05-22 06:16:50 +05:30
/*
shred.ing-discord - audio streaming c2+util
Copyright 2025 Corwin Brust <corwin@bru.st>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Commentary:
Thanks to numerious kind folks on IRC on SO, as well ihis helpful post
that turned me onto Eris: https://www.toptal.com/chatbot/how-to-make-a-discord-bot
*/
const fs = require('fs');
//const eris = require('eris');
const eris = require("@projectdysnomia/dysnomia");
const configEnvVar = 'SHRED_BOT_CONFIG';
const configFile = process.env.hasOwnProperty( configEnvVar )
? process.env[ configEnvVar ]
: '/opt/shred.ing/discord-channels-simple.json';
const shredConfig = JSON.parse( fs.readFileSync( configFile ) );
function writeConfig() { fs.writeFileSync( configFile, JSON.stringify( shredConfig ) ) }
const uri = shredConfig.uri;
const bot = new eris.Client(fs.readFileSync(shredConfig.tokenFile).toString(), {
2026-05-22 06:16:50 +05:30
seedVoiceConnections: true,
reconnectDelay: 17000
});
bot.on('error', err => console.debug({
what:"error",
where:"bot",
error:err
}));
process.on("unhandledRejection", err => console.debug({
what:"unhandledRejection",
where:"process",
error:err
}));
process.on("uncaughtException", err => console.debug({
what:"uncaughtException",
where:"process",
error:err
}));
2026-05-22 06:16:50 +05:30
const joinAndPlay = function (msg) {
_internal_join_and_play( msg.member.voiceState.channelID, msg.channel.id);
};
let activeChannels = {};
const _internal_join_and_play = function( joinID, msgID ) {
bot.joinVoiceChannel(joinID).catch((err) => {
2026-05-22 06:16:50 +05:30
if(msgID) {
bot.createMessage(msgID, "Error joining voice channel: " + err.message);
2026-05-22 06:16:50 +05:30
}
console.debug({
what:"catch",
where:"join",
joinID:joinID,
msgID:msgID,
error:err
});
2026-05-22 06:16:50 +05:30
}).then((connection) => {
const ac = { playing: false, timer: false };
ac.stop = function() { connection.stopPlaying(); };
ac.play = function() {
connection.play( uri, { voiceDataTimeout: 17000 });
2026-05-22 06:16:50 +05:30
ac.playing = true
};
ac.before = function() {
if (connection.playing) { // Stop playing if the connection is playing something
connection.stopPlaying();
}
};
ac.when = function() {
//if(msgID) bot.createMessage(msgID, `Now playing **${uri}**`);
2026-05-22 06:16:50 +05:30
},
ac.after = function() {
ac.playing = false;
//console.log(`Finished in ${ joinID }`);
2026-05-22 06:16:50 +05:30
},
ac.cancel = function() {
if(ac.timer) clearInterval( ac.timer );
if(ac.playing) ac.stop();
};
ac.start = function() {
ac.before();
ac.play();
ac.when();
connection.once("end", () => { ac.after() });
};
ac.maybeStart = function() {
try {
if(! ac.playing ) {
ac.start();
}
} catch(err) {
console.debug({
what:"catch",
where:"maybeStart",
error:err
});
2026-05-22 06:16:50 +05:30
}
};
activeChannels[ joinID ] = ac;
ac.timer = setInterval( ac.maybeStart, 100 );
2026-05-22 06:16:50 +05:30
});
};
const doAutoJoin = function () {
const autoJoin = shredConfig.autoJoin;
for(var i = 0; i < autoJoin.length; ++i) {
try {
//console.debug({autoJoin:autoJoin[i],botGuilds:bot.guilds,chanGet:bot.guilds.get( autoJoin[i].guild )});
//const vc = bot.guilds.get( autoJoin[i].guild ).channels.get( autoJoin[i].channel );
//_internal_join_and_play( vc.id );
_internal_join_and_play( autoJoin[i].channel );
}
catch (err) {
console.debug({
what:"catch",
where:"doAutoJoin",
guild:auoJoin[i].guild,
id:autoJoin[i].channel,
error:err
});
2026-05-22 06:16:50 +05:30
}
}
};
let title = null;
let trackUri = '';
function formatArtist(artist,album,year) {
var rv = '';
if(album) {
rv = rv ? rv + ' ' + album : album;
}
if(year && album != year) {
2026-05-22 10:40:18 +05:30
rv = rv ? `${rv}, *${year}*` : year
2026-05-22 06:16:50 +05:30
}
if(artist) {
2026-05-22 10:40:18 +05:30
rv = rv ? (rv.includes('(')?`__${artist}__, ${rv}` : `__${artist}__ (${rv})`) : artist;
2026-05-22 06:16:50 +05:30
}
return rv;
}
function formatTrackTitle(meta) {
if(! meta) return '';
const {title,artist,album,date,year,filename} = meta;
const who = formatArtist(artist,album,year||date);
const fileUri
= 'https://f.shred.ing/'
+ encodeURI(
filename.replace('/spokes/gw11/storage/ftp/music/','')
);
//console.log({title:title,who:who,fUri:fileUri,artist:artist,album:album,date:date,year:year,fname:filename});
2026-05-22 10:40:18 +05:30
const rv = title +' '+ formatArtist(artist,album,year||date);
2026-05-22 06:16:50 +05:30
return [rv,fileUri];
}
const setStatus = async function() {
try {
const response = await fetch( shredConfig.titleUri );
if(!response.ok) {
console.debug({
what:"request.ok",
where:"setStatus",
error:err
});
2026-05-22 06:16:50 +05:30
}
const json = await response.json();
if(json) {
let [newTitle,newLink] = formatTrackTitle(Object.fromEntries( json ));
if(newTitle && newTitle != title) {
//console.log("Playing[discord]: " + newTitle + ' at ' + newLink);
2026-05-22 06:16:50 +05:30
title = newTitle;
trackUri = newLink;
bot.editStatus("online", [ { name: title, type: 0, url: newLink}]);
}
}
} catch (err) {
console.debug({
what:"catch",
where:"setStatus",
error:err
});
2026-05-22 06:16:50 +05:30
}
};
let timer = setInterval( setStatus, 10000 );
// Every time a message is sent anywhere the bot is present,
// this event will fire and we will check if the bot was mentioned.
// If it was, the bot will attempt to respond with "Present".
bot.on('messageCreate', async (msg) => {
const botWasMentioned = msg.mentions.find(
mentionedUser => mentionedUser.id === bot.user.id,
);
if (botWasMentioned) {
try {
if( msg.content.indexOf( 'stop' ) != -1) {
// request to stop
if(! msg.member.voiceState.channelID ) {
// user is not in a voice channel
bot.createMessage(msg.channel.id, "You are not in a voice channel.");
} else {
const chan = bot.guilds.get( msg.guildID ).channels.get( msg.member.voiceState.channelID );
if(chan.id && activeChannels[ chan.id ]) activeChannels[ chan.id ].cancel()
chan.leave();
}
} else if( msg.content.indexOf( 'join' ) != -1) {
// request to join a channel
if(! msg.member.voiceState.channelID ) {
// user is not in a voice channel
bot.createMessage(msg.channel.id, "You are not in a voice channel.");
} else {
joinAndPlay( msg );
}
} else if( msg.content.indexOf( 'reconnect' ) != -1) {
// request to join a channel
if( msg.member.voiceState.channelID)
joinAndPlay( msg );
2026-05-22 06:16:50 +05:30
} else {
// treat everything else a request for current track title
2026-05-22 10:40:18 +05:30
await msg.channel.createMessage(title +"\n\n[Download/Permalink]("+ trackUri +")\n");
2026-05-22 06:16:50 +05:30
}
} catch (err) {
// There are various reasons why sending a message may fail.
// The API might time out or choke and return a 5xx status,
// or the bot may not have permission to send the
// message (403 status).
console.debug({
what:"replyMessageError",
where:"messageCreate",
error:err
})
2026-05-22 06:16:50 +05:30
}
}
});
let autoJoinTimer;
function setAutoJoinTimeout( waitTime ) {
if(autoJoinTimer) return;
autoJoinTimer = setTimeout( () => {
autoJoinTimer=null;
doAutoJoin();
}, waitTime );
return autoJoinTimer;
}
2026-05-22 06:16:50 +05:30
bot.connect();
setAutoJoinTimeout( 1000 );