start: a crufty yet working beginning
This commit is contained in:
parent
5a09204ab0
commit
06866ef7da
3 changed files with 283 additions and 0 deletions
247
bot.js
Normal file
247
bot.js
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
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 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 );
|
||||
14
discord-channels-simple.json
Normal file
14
discord-channels-simple.json
Normal file
|
|
@ -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": [ ]
|
||||
}
|
||||
22
package.json
Normal file
22
package.json
Normal file
|
|
@ -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 <corwin@bru.st>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue