/* shred.ing-discord - audio streaming c2+util Copyright 2025 Corwin Brust 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 . 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 token = fs.readFileSync( shredConfig.tokenFile ).toString(); const uri = shredConfig.uri; const bot = new eris.Client( token, { seedVoiceConnections: true, reconnectDelay: 17000 }); 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) => { // Join the user's voice channel if(msgID) { bot.createMessage(msgID, "Error joining voice channel: " + err.message); // Notify the user if there is an error } console.log({'error joining voice channel': err,chan:joinID}); // Log the error }).then((connection) => { //connection.play( uri, { voiceDataTimeout: -1 } ); // Play the file and notify the user const ac = { playing: false, timer: false }; ac.stop = function() { connection.stopPlaying(); }; ac.play = function() { connection.play( uri, { voiceDataTimeout: -1 }); 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}**`); } }, ac.after = function() { //if(msgID) bot.createMessage(msgID, `Finished **${uri}**`); // Say when the file has finished playing ac.playing = false; console.log(`Finished in ${ joinID }`); }, 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.log('ERROR: failed in maybeStart ' + err); } }; activeChannels[ joinID ] = ac; ac.timer = setInterval( ac.maybeStart, 1000 ); }); }; 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.log('ERROR: failed to autojoin channel #' + i + ' guild:' + autoJoin[i].guild + ' ID:'+ autoJoin[i].channel + ' -> ' + err); } } }; let title = null; let trackUri = ''; function formatArtist(artist,album,year) { var rv = ''; if(album) { rv = rv ? rv + ' ' + album : album; } if(year && album != year) { rv = rv ? `${rv}, /${year}/` : year } if(artist) { rv = rv ? (rv.includes('(')?`${artist}, ${rv}` : `${artist} (${rv})`) : artist; } 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,fileUri:fileUri,artist:artist,album:album,date:date,year:year,filename:filename}); const rv = //'[🔗]('+ fileUri +') ' //+ ' ' + title + ' ' + formatArtist(artist,album,year||date) return [rv,fileUri]; } const setStatus = async function() { try { const response = await fetch( shredConfig.titleUri ); if(!response.ok) { //await msg.channel.createMessage( 'Playing (error retreiving title)' ); //throw new Error(`Title response status: ${response.status}`); console.log(`ERROR: Title status response: ${response.status}`); } const json = await response.json(); // if(json && json.title && json.title !== title) { // //await msg.channel.createMessage( "Playing: " + json.title ); // title = json.title; // console.log("New track title: " + json.title); // bot.editStatus("online", [ { name: title, type: 0, url: "https://shred.ing" }]); // } //const {t,ar,al,dt,yr,fn} = await response.json(); if(json) { let [newTitle,newLink] = formatTrackTitle(Object.fromEntries( json )); if(newTitle && newTitle != title) { console.log("New track title: " + newTitle + ' at ' + newLink); title = newTitle; trackUri = newLink; bot.editStatus("online", [ { name: title, type: 0, url: newLink}]); } } } catch (err) { console.log('ERROR: fetching title ' + err); } }; 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 { // treat everything else a request for current track title // const response = await fetch( 'https://shred.ing/json' ); // if(!response.ok) { // await msg.channel.createMessage( 'Playing (error retreiving title)' ); // throw new Error(`Title response status: ${response.status}`); // } // const json = await response.json(); await msg.channel.createMessage( "Playing: " + title + "\n\nDownload: " + trackUri + "\n" ); //setStatus(); } } 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.warn('Failed to respond to mention.'); console.warn(err); } } }); bot.on('error', err => { console.warn(err); }); bot.connect(); const autoJoinTimer = setTimeout( doAutoJoin, 10000 );