From 3ef54d816991fa59fa28af189e01f26cabc92cd6 Mon Sep 17 00:00:00 2001 From: Corwin Brust Date: Fri, 22 May 2026 06:16:50 +0530 Subject: [PATCH] start: a crufty yet working beginning --- bot.js | 247 +++++++++++++++++++++++++++++++++++ discord-channels-simple.json | 14 ++ package.json | 22 ++++ 3 files changed, 283 insertions(+) create mode 100644 bot.js create mode 100644 discord-channels-simple.json create mode 100644 package.json diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..e7f57bd --- /dev/null +++ b/bot.js @@ -0,0 +1,247 @@ +/* + 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 ); diff --git a/discord-channels-simple.json b/discord-channels-simple.json new file mode 100644 index 0000000..e9adec5 --- /dev/null +++ b/discord-channels-simple.json @@ -0,0 +1,14 @@ +{ + "tokenFile": "/path/to/.token", + "uri": "https://shred.ing/ogg", + "titleUri":"https://shred.ing/autodj.meta", + "reconnectDelayMS": 1000, + "titleFetchIntervalMS": 5000, + "autoJoin": [ + { + "guild": "DISCORD_GUILD_ID", + "channel": "DISCORD_CHANNEL_ID" + } + ], + "active": [ ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d7f7cc0 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "shred.bot", + "version": "1.0.0", + "description": "discord audio relay for shred.ing", + "keywords": [ + "shred.ing" + ], + "license": "GPL-3.0-or-later", + "author": "Corwin Brust ", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@discordjs/voice": "^0.19.0", + "@projectdysnomia/dysnomia": "^0.2.3", + "body-parser": "^2.2.0", + "eris": "^0.18.0", + "express": "^5.1.0" + } +}