mirror of
https://github.com/Myxelium/RandomMemerBot.git
synced 2026-07-08 22:15:09 +00:00
Documentation and variable renaming
Made it easier for other developers to understand the code?
This commit is contained in:
154
bot.ts
154
bot.ts
@@ -1,12 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* This file contains the code for a Discord bot that joins a random voice channel in a random guild and plays a random sound file.
|
||||||
|
* It uses the @discordjs/voice library for voice connections and the node-schedule library for scheduling the next join.
|
||||||
|
* The bot also starts a web server using the startServer function from the webserver module.
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
import {
|
import {
|
||||||
joinVoiceChannel,
|
joinVoiceChannel,
|
||||||
createAudioPlayer,
|
createAudioPlayer,
|
||||||
createAudioResource,
|
createAudioResource,
|
||||||
entersState,
|
entersState,
|
||||||
AudioPlayerStatus,
|
AudioPlayerStatus,
|
||||||
VoiceConnectionStatus
|
VoiceConnectionStatus,
|
||||||
|
VoiceConnection
|
||||||
} from '@discordjs/voice';
|
} from '@discordjs/voice';
|
||||||
import { ChannelType, Client, GatewayIntentBits } from 'discord.js';
|
import { ChannelType, Client, GatewayIntentBits, GuildBasedChannel } from 'discord.js';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import * as fileSystem from 'fs';
|
import * as fileSystem from 'fs';
|
||||||
import { startServer as startWebServer } from './client/webserver';
|
import { startServer as startWebServer } from './client/webserver';
|
||||||
@@ -14,14 +22,14 @@ import * as schedule from 'node-schedule';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
export var nextPlayBackTime: string = '';
|
export var nextPlayBackTime: string = ''; // Export so it can be used in the webserver module aswell.
|
||||||
const minTime = parseInt(process.env.INTERVALMIN_MINUTES!, 10); // Minimum interval in minutes
|
const minTimeInterval = parseInt(process.env.INTERVALMIN_MINUTES!, 10); // Minimum interval in minutes.
|
||||||
const maxTime = convertHoursToMinutes(parseFloat(process.env.INTERVALMAX_HOURS!)); // Maximum interval in minutes
|
const maxTimeInterval = convertHoursToMinutes(parseFloat(process.env.INTERVALMAX_HOURS!)); // Maximum interval in minutes.
|
||||||
const voiceChannelRetries = parseInt(process.env.VOICECHANNELRETRIES!, 10); // Number of retries to find a voice channel with members in it
|
const voiceChannelRetries = parseInt(process.env.VOICECHANNELRETRIES!, 10); // Number of retries to find a voice channel with members in it.
|
||||||
|
|
||||||
const token = process.env.TOKEN;
|
const discordApplicationToken = process.env.TOKEN; // Discord bot token from .env file (required) More info: https://discordgsm.com/guide/how-to-get-a-discord-bot-token
|
||||||
const soundsDir = './sounds/';
|
const soundsDirectory = './sounds/';
|
||||||
const client = new Client({
|
const discordClient = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
@@ -32,18 +40,18 @@ const client = new Client({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export class LoggerColors {
|
export class LoggerColors {
|
||||||
public static readonly Green = "\x1b[32m%s\x1b[0m";
|
public static readonly Green: string = "\x1b[32m%s\x1b[0m";
|
||||||
public static readonly Yellow = "\x1b[33m%s\x1b[0m";
|
public static readonly Yellow: string = "\x1b[33m%s\x1b[0m";
|
||||||
public static readonly Cyan = "\x1b[36m%s\x1b[0m";
|
public static readonly Cyan: string = "\x1b[36m%s\x1b[0m";
|
||||||
public static readonly Red = "\x1b[31m%s\x1b[0m";
|
public static readonly Red: string = "\x1b[31m%s\x1b[0m";
|
||||||
public static readonly Teal = "\x1b[35m%s\x1b[0m";
|
public static readonly Teal: string = "\x1b[35m%s\x1b[0m";
|
||||||
}
|
}
|
||||||
|
|
||||||
client.login(token);
|
discordClient.login(discordApplicationToken);
|
||||||
|
|
||||||
client.on('ready', async () => {
|
discordClient.on('ready', async () => {
|
||||||
console.log(LoggerColors.Green, `Add to server by: https://discord.com/oauth2/authorize?client_id=${client.application?.id}&permissions=70379584&scope=bot`);
|
console.log(LoggerColors.Green, `Add to server by: https://discord.com/oauth2/authorize?client_id=${discordClient.application?.id}&permissions=70379584&scope=bot`);
|
||||||
console.log(`Logged in as ${client.user?.tag}!`);
|
console.log(`Logged in as ${discordClient.user?.tag}!`);
|
||||||
|
|
||||||
joinRandomChannel(voiceChannelRetries);
|
joinRandomChannel(voiceChannelRetries);
|
||||||
startWebServer();
|
startWebServer();
|
||||||
@@ -52,22 +60,22 @@ client.on('ready', async () => {
|
|||||||
/**
|
/**
|
||||||
* Joins a random voice channel in a random guild and plays a random sound file.
|
* Joins a random voice channel in a random guild and plays a random sound file.
|
||||||
* @param retries - The number of retries to attempt if no voice channels are found.
|
* @param retries - The number of retries to attempt if no voice channels are found.
|
||||||
* @returns void
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
async function joinRandomChannel(retries = 12) {
|
async function joinRandomChannel(retries = 12) {
|
||||||
if (!client.guilds.cache.size) {
|
if (!discordClient.guilds.cache.size) {
|
||||||
console.log(LoggerColors.Red, 'No guilds found');
|
console.log(LoggerColors.Red, 'No guilds found');
|
||||||
scheduleNextJoin();
|
scheduleNextJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const guild = client.guilds.cache.random();
|
const randomlyPickedDiscordServer = discordClient.guilds.cache.random();
|
||||||
const voiceChannels = guild?.channels.cache.filter(channel =>
|
const accessableVoiceChannels = randomlyPickedDiscordServer?.channels.cache.filter(channel =>
|
||||||
channel.type === ChannelType.GuildVoice &&
|
channel.type === ChannelType.GuildVoice &&
|
||||||
channel.members.size > 0
|
channel.members.size > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!voiceChannels?.size) {
|
if (!accessableVoiceChannels?.size) {
|
||||||
if (retries > 0) {
|
if (retries > 0) {
|
||||||
console.log(LoggerColors.Yellow, `No voice channels found, retrying in 5 seconds... (${retries} retries left)`);
|
console.log(LoggerColors.Yellow, `No voice channels found, retrying in 5 seconds... (${retries} retries left)`);
|
||||||
setTimeout(() => joinRandomChannel(retries - 1), 5000); // Wait for 5 seconds before retrying
|
setTimeout(() => joinRandomChannel(retries - 1), 5000); // Wait for 5 seconds before retrying
|
||||||
@@ -80,35 +88,30 @@ async function joinRandomChannel(retries = 12) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceChannel = voiceChannels.random();
|
const randomlyPickedVoiceChannel = accessableVoiceChannels.random();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const connection = joinVoiceChannel({
|
// Join the voice channel
|
||||||
channelId: voiceChannel!.id,
|
const voiceChannelConnection = joinVoiceChannel({
|
||||||
guildId: voiceChannel!.guild.id,
|
channelId: randomlyPickedVoiceChannel!.id,
|
||||||
adapterCreator: voiceChannel!.guild.voiceAdapterCreator,
|
guildId: randomlyPickedVoiceChannel!.guild.id,
|
||||||
|
adapterCreator: randomlyPickedVoiceChannel!.guild.voiceAdapterCreator,
|
||||||
});
|
});
|
||||||
|
|
||||||
await entersState(connection, VoiceConnectionStatus.Ready, 30e3);
|
await entersState(voiceChannelConnection, VoiceConnectionStatus.Ready, 30e3);
|
||||||
const soundFiles = fileSystem.readdirSync(soundsDir).filter(file => file.endsWith('.mp3'));
|
const soundFilePath = getRandomSoundFilePath();
|
||||||
const soundFile = soundsDir + soundFiles[Math.floor(Math.random() * soundFiles.length)];
|
|
||||||
|
|
||||||
if(!soundFile) {
|
if(!soundFilePath) {
|
||||||
console.log(LoggerColors.Red, 'No sound files found');
|
console.log(LoggerColors.Red, 'No sound files found');
|
||||||
scheduleNextJoin();
|
scheduleNextJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = createAudioResource(soundFile);
|
await playSoundFile(
|
||||||
const player = createAudioPlayer();
|
soundFilePath,
|
||||||
|
randomlyPickedVoiceChannel,
|
||||||
console.log(LoggerColors.Teal, `Playing ${soundFile} in ${voiceChannel?.name}...`);
|
voiceChannelConnection);
|
||||||
|
|
||||||
player.play(resource);
|
|
||||||
connection.subscribe(player);
|
|
||||||
|
|
||||||
await entersState(player, AudioPlayerStatus.Idle, 300000);
|
|
||||||
connection.destroy();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@@ -116,31 +119,78 @@ async function joinRandomChannel(retries = 12) {
|
|||||||
scheduleNextJoin();
|
scheduleNextJoin();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNextJoin(){
|
/**
|
||||||
const randomInterval = Math.floor(Math.random() * (maxTime - minTime + 1)) + minTime;
|
* Plays a sound file in a voice channel.
|
||||||
|
* @param soundFilePath - The path to the sound file to play.
|
||||||
|
* @param randomlyPickedVoiceChannel - The voice channel to play the sound file in.
|
||||||
|
* @param voiceChannelConnection - The voice channel connection.
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
async function playSoundFile(
|
||||||
|
soundFilePath: string,
|
||||||
|
randomlyPickedVoiceChannel: GuildBasedChannel | undefined,
|
||||||
|
voiceChannelConnection: VoiceConnection
|
||||||
|
): Promise<void> {
|
||||||
|
const audioResource = createAudioResource(soundFilePath);
|
||||||
|
const audioPlayer = createAudioPlayer();
|
||||||
|
|
||||||
|
console.log(LoggerColors.Teal, `Playing ${soundFilePath} in ${randomlyPickedVoiceChannel?.name}...`);
|
||||||
|
|
||||||
|
audioPlayer.play(audioResource);
|
||||||
|
voiceChannelConnection.subscribe(audioPlayer);
|
||||||
|
|
||||||
|
await entersState(audioPlayer, AudioPlayerStatus.Idle, 300000);
|
||||||
|
voiceChannelConnection.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random sound file from the sounds directory.
|
||||||
|
* @returns string - The path to a random sound file.
|
||||||
|
*/
|
||||||
|
function getRandomSoundFilePath(): string {
|
||||||
|
const allSoundFilesAsArray = fileSystem.readdirSync(soundsDirectory).filter(file => file.endsWith('.mp3'));
|
||||||
|
return soundsDirectory + allSoundFilesAsArray[Math.floor(Math.random() * allSoundFilesAsArray.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules the next join to a random channel. Using a random interval between minTime and maxTime.
|
||||||
|
* It clears the previous schedule before scheduling the next join, to avoid multiple schedules.
|
||||||
|
* @see minTimeInterval - time in minutes
|
||||||
|
* @see maxTimeInterval - time in hours
|
||||||
|
* @see schedule - node-schedule instance
|
||||||
|
*/
|
||||||
|
function scheduleNextJoin(): void {
|
||||||
|
const randomInterval = Math.floor(Math.random() * (maxTimeInterval - minTimeInterval + 1)) + minTimeInterval;
|
||||||
const minutes = randomInterval % 60;
|
const minutes = randomInterval % 60;
|
||||||
const hours = Math.floor(randomInterval / 60);
|
const hours = Math.floor(randomInterval / 60);
|
||||||
|
|
||||||
schedule.gracefulShutdown().finally(() => {
|
schedule.gracefulShutdown().finally(() => {
|
||||||
console.log(`${Math.floor(minutes)} ${Math.floor(hours) == 0 ? '*' : Math.floor(hours) } * * *`)
|
const jobName = schedule.scheduleJob(`${Math.floor(minutes)} ${Math.floor(hours) == 0 ? '*' : Math.floor(hours) } * * *`, function(){
|
||||||
let jobname = schedule.scheduleJob(`${Math.floor(minutes)} ${Math.floor(hours) == 0 ? '*' : Math.floor(hours) } * * *`, function(){
|
|
||||||
joinRandomChannel();
|
joinRandomChannel();
|
||||||
}).name;
|
}).name;
|
||||||
|
|
||||||
console.log(schedule.scheduledJobs[jobname].nextInvocation().toLocaleString())
|
let nextPlaybackDate = schedule.scheduledJobs[jobName].nextInvocation();
|
||||||
|
|
||||||
let nextPlaybackDate = schedule.scheduledJobs[jobname].nextInvocation();
|
|
||||||
|
|
||||||
nextPlayBackTime = dateToString(nextPlaybackDate) ?? '';
|
nextPlayBackTime = dateToString(nextPlaybackDate) ?? '';
|
||||||
log(nextPlaybackDate, hours, minutes);
|
log(nextPlaybackDate, hours, minutes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertHoursToMinutes(hours: number){
|
function convertHoursToMinutes(hours: number): number {
|
||||||
return hours * 60;
|
return hours * 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(waitTime: Date, preHour: number, preMinute: number){
|
/**
|
||||||
|
* Logs the wait time, current time, next join time, and cron schedule in the console.
|
||||||
|
* @param waitTime - The time to wait until the next join.
|
||||||
|
* @param hour - The hour of the cron schedule.
|
||||||
|
* @param minute - The minute of the cron schedule.
|
||||||
|
*/
|
||||||
|
function log(
|
||||||
|
waitTime: Date,
|
||||||
|
hour: number,
|
||||||
|
minute: number
|
||||||
|
){
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -148,10 +198,10 @@ function log(waitTime: Date, preHour: number, preMinute: number){
|
|||||||
Wait time: ${(waitTime.getTime() - currentTime.getTime()) / 60000} minutes,
|
Wait time: ${(waitTime.getTime() - currentTime.getTime()) / 60000} minutes,
|
||||||
Current time: ${dateToString(currentTime)},
|
Current time: ${dateToString(currentTime)},
|
||||||
Next join time: ${dateToString(waitTime)},
|
Next join time: ${dateToString(waitTime)},
|
||||||
Cron: ${Math.floor(preMinute)} ${Math.floor(preHour) == 0 ? '*' : Math.floor(preHour) } * * *`
|
Cron: ${Math.floor(minute)} ${Math.floor(hour) == 0 ? '*' : Math.floor(hour) } * * *`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateToString(date: Date){
|
function dateToString(date: Date): string {
|
||||||
return date.toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' });
|
return date.toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' });
|
||||||
}
|
}
|
||||||
@@ -32,14 +32,27 @@ const upload = multer({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index.html file.
|
||||||
|
* @returns index.html - The index.html file.
|
||||||
|
*/
|
||||||
app.get('/', (_req, res) => {
|
app.get('/', (_req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'web/index.html'));
|
res.sendFile(path.join(__dirname, 'web/index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to the sounds folder.
|
||||||
|
* @Body myFile - The file to upload.
|
||||||
|
*/
|
||||||
app.post('/upload', upload.single('myFile'), async (req, res) => {
|
app.post('/upload', upload.single('myFile'), async (req, res) => {
|
||||||
res.send('File uploaded successfully.');
|
res.send('File uploaded successfully.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a YouTube video as an mp3 file to the sounds folder.
|
||||||
|
* The video must be shorter than 10 seconds.
|
||||||
|
* @Body url - The YouTube video url.
|
||||||
|
*/
|
||||||
app.post('/upload-youtube', async (req, res) => {
|
app.post('/upload-youtube', async (req, res) => {
|
||||||
const url = req.body.url;
|
const url = req.body.url;
|
||||||
|
|
||||||
@@ -89,9 +102,17 @@ app.post('/upload-youtube', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// create a enpoint to return a file from the sounds folder (use the file name) with as little code as possible
|
/**
|
||||||
|
* Returns a file from the sounds folder by filename
|
||||||
|
* @param filename - The name of the file to return.
|
||||||
|
* @returns mp3 - The requested file.
|
||||||
|
*/
|
||||||
app.use('/sounds', express.static(path.join(__dirname, '../sounds')));
|
app.use('/sounds', express.static(path.join(__dirname, '../sounds')));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all sound files in the sounds directory.
|
||||||
|
* @returns string[] - An array of all sound files in the sounds directory.
|
||||||
|
*/
|
||||||
app.get('/sounds', (_req, res) => {
|
app.get('/sounds', (_req, res) => {
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const directoryPath = path.join(__dirname, '../sounds');
|
const directoryPath = path.join(__dirname, '../sounds');
|
||||||
@@ -103,10 +124,17 @@ app.get('/sounds', (_req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next playback time.
|
||||||
|
* @returns string - The next playback time.
|
||||||
|
*/
|
||||||
app.get('/nextplaybacktime', (_req, res) => {
|
app.get('/nextplaybacktime', (_req, res) => {
|
||||||
res.send(nextPlayBackTime);
|
res.send(nextPlayBackTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a file from the sounds folder by filename
|
||||||
|
*/
|
||||||
app.delete('/sounds/:filename', (req, res) => {
|
app.delete('/sounds/:filename', (req, res) => {
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const directoryPath = path.join(__dirname, '../sounds');
|
const directoryPath = path.join(__dirname, '../sounds');
|
||||||
@@ -121,6 +149,10 @@ app.delete('/sounds/:filename', (req, res) => {
|
|||||||
|
|
||||||
app.use(express.static(path.join(__dirname, "web")));
|
app.use(express.static(path.join(__dirname, "web")));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the web server on either http or https protocol based on the availability of SSL certificates.
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
export function startServer() {
|
export function startServer() {
|
||||||
let port: 80 | 443 = 80;
|
let port: 80 | 443 = 80;
|
||||||
let server;
|
let server;
|
||||||
|
|||||||
Reference in New Issue
Block a user