diff --git a/bot.ts b/bot.ts index d0f026f..23c16af 100644 --- a/bot.ts +++ b/bot.ts @@ -9,10 +9,15 @@ import { import { ChannelType, Client, GatewayIntentBits } from 'discord.js'; import * as dotenv from 'dotenv'; import * as fileSystem from 'fs'; -import { startServer } from './client/webserver'; +import { startServer as startWebServer } from './client/webserver'; +import * as schedule from 'node-schedule'; dotenv.config(); +const minTime = parseInt(process.env.INTERVALMIN_MINUTES!, 10); // Minimum interval in minutes +const maxTime = convertHoursToMinutes(parseInt(process.env.INTERVALMAX_HOURS!, 10)); // Maximum interval in minutes +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 soundsDir = './sounds/'; const client = new Client({ @@ -29,35 +34,55 @@ export class LoggerColors { public static readonly Green = "\x1b[32m%s\x1b[0m"; public static readonly Yellow = "\x1b[33m%s\x1b[0m"; public static readonly Cyan = "\x1b[36m%s\x1b[0m"; + public static readonly Red = "\x1b[31m%s\x1b[0m"; + public static readonly Teal = "\x1b[35m%s\x1b[0m"; } client.login(token); -client.on('ready', () => { - +client.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`); - startServer(); console.log(`Logged in as ${client.user?.tag}!`); - joinRandomChannel(); + + joinRandomChannel(voiceChannelRetries); + startWebServer(); }); -async function joinRandomChannel() { - if (!client.guilds.cache.size) +/** + * 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. + * @returns void + */ +async function joinRandomChannel(retries = 12) { + if (!client.guilds.cache.size) { + console.log(LoggerColors.Red, 'No guilds found'); + scheduleNextJoin(); return; - + } + const guild = client.guilds.cache.random(); const voiceChannels = guild?.channels.cache.filter(channel => channel.type === ChannelType.GuildVoice && channel.members.size > 0 ); - if (!voiceChannels?.size) + if (!voiceChannels?.size) { + if (retries > 0) { + 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 + } + + if(retries === 0) { + console.log(LoggerColors.Red, 'No voice channels found'); + scheduleNextJoin(); + } return; + } const voiceChannel = voiceChannels.random(); - + try { - const connection = await joinVoiceChannel({ + const connection = joinVoiceChannel({ channelId: voiceChannel!.id, guildId: voiceChannel!.guild.id, adapterCreator: voiceChannel!.guild.voiceAdapterCreator, @@ -69,7 +94,7 @@ async function joinRandomChannel() { const resource = createAudioResource(soundFile); const player = createAudioPlayer(); - console.log(LoggerColors.Yellow, `Playing ${soundFile} in ${voiceChannel?.name}...`); + console.log(LoggerColors.Teal, `Playing ${soundFile} in ${voiceChannel?.name}...`); player.play(resource); connection.subscribe(player); @@ -80,17 +105,30 @@ async function joinRandomChannel() { console.error(error); } - const waitTime = Math.floor(Math.random() * (43200000 - 10000 + 1)) + 10000; - log(waitTime); - setTimeout(joinRandomChannel, waitTime); + scheduleNextJoin(); +} + +function scheduleNextJoin(){ + const randomInterval = Math.floor(Math.random() * (maxTime - minTime + 1)) + minTime; + log(randomInterval); + + schedule.scheduleJob(`*/${randomInterval} * * * *`, function(){ + joinRandomChannel(); + }); +} + +function convertHoursToMinutes(hours: number){ + return hours * 60; } function log(waitTime: number){ + const currentTime = new Date(); + const nextJoinTime = new Date(currentTime.getTime() + waitTime * 60 * 1000); // Convert waitTime from minutes to milliseconds + console.log( LoggerColors.Cyan, ` - Wait time: ${(waitTime / 1000 / 60 > 60) ? (waitTime / 1000 / 60 / 60 + ' hours') : (waitTime / 1000 / 60 + ' minutes')}, - Current time: ${new Date().toLocaleTimeString()}, - Next join time: ${new Date(Date.now() + waitTime) - .toLocaleTimeString()}` + Wait time: ${waitTime} minutes, + Current time: ${currentTime.toLocaleTimeString()}, + Next join time: ${nextJoinTime.toLocaleTimeString()}` ); } \ No newline at end of file diff --git a/client/readme.md b/client/readme.md new file mode 100644 index 0000000..678a4e5 --- /dev/null +++ b/client/readme.md @@ -0,0 +1,13 @@ +### Webserver +A simple webserver for managing sounds played by the bot. + +``I havent paid much attention to this and it can be refactored, rewritten and just more structured. I didn't feel like i bothered too much so i gave it minimal attention and left it at "I just works" stage.`` + + +//Feels weird to write basic js in the browser since it was so long ago.. + + +## How it works, +the bot should give you an address on startup, but if you miss it, it should be http://localhost:8080 if you host it somewhere its the ip-address of the server instead of localhost. + +i tested ``foundation`` because the sake of it for the design, i dont think i will use it again on a project. \ No newline at end of file diff --git a/client/web/client.js b/client/web/client.js index 9e24010..9347f11 100644 --- a/client/web/client.js +++ b/client/web/client.js @@ -1,3 +1,57 @@ +function loadFiles() { + // Fetch the JSON data from the /sounds endpoint + fetch('/sounds') + .then(response => response.json()) + .then(data => { + // Get the fileList element + const fileList = document.getElementById('fileList'); + + // Clear the current list + fileList.innerHTML = ''; + + // Add each file to the list + data.forEach(file => { + // Create a new list item + const li = document.createElement('li'); + li.className = 'grid-x'; + + // Create a div for the file name and add it to the list item + const fileNameDiv = document.createElement('div'); + fileNameDiv.className = 'cell auto'; + fileNameDiv.textContent = file; + li.appendChild(fileNameDiv); + + // Create a div for the trash icon and add it to the list item + const trashIconDiv = document.createElement('div'); + trashIconDiv.className = 'cell shrink'; + trashIconDiv.style.cursor = 'pointer'; + trashIconDiv.textContent = '🗑️'; + li.appendChild(trashIconDiv); + + // Attach a click event listener to the trash icon div + trashIconDiv.addEventListener('click', () => { + // Send a DELETE request to the server to remove the file + fetch('/sounds/' + file, { method: 'DELETE' }) + .then(response => response.text()) + .then(message => { + console.log(message); + + // Remove the list item from the fileList element + fileList.removeChild(li); + }) + .catch(error => console.error('Error:', error)); + }); + + // Add the list item to the fileList element + fileList.appendChild(li); + }); + }) + .catch(error => console.error('Error:', error)); +} + +// Call loadFiles when the script is loaded +loadFiles(); + document.getElementById('uploadForm').addEventListener('submit', function(event) { event.preventDefault(); @@ -37,10 +91,13 @@ document.getElementById('uploadForm').addEventListener('submit', function(event) .then(data => { console.log(data); alert('File uploaded successfully.'); + + // Call loadFiles again to update the file list + loadFiles(); }) .catch(error => { console.error(error); alert('An error occurred while uploading the file.'); }); }); -}); \ No newline at end of file +}); diff --git a/client/web/index.html b/client/web/index.html index e45c034..60a8383 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -23,10 +23,17 @@ + +
+

Uploaded Files

+
+ +
+
- \ No newline at end of file + diff --git a/client/webserver.ts b/client/webserver.ts index 940dc44..7fbc61d 100644 --- a/client/webserver.ts +++ b/client/webserver.ts @@ -30,6 +30,29 @@ app.post('/upload', upload.single('myFile'), async (req, res) => { res.send('File uploaded successfully.'); }); +app.get('/sounds', (_req, res) => { + const fs = require('fs'); + const directoryPath = path.join(__dirname, '../sounds'); + fs.readdir(directoryPath, function (err: any, files: any[]) { + if (err) { + return console.log(LoggerColors.Red, 'Unable to scan directory: ' + err); + } + res.send(files); + }); +}); + +app.delete('/sounds/:filename', (req, res) => { + const fs = require('fs'); + const directoryPath = path.join(__dirname, '../sounds'); + const filePath = directoryPath + '/' + req.params.filename; + fs.unlink(filePath, (err: any) => { + if (err) { + return console.log(LoggerColors.Red, 'Unable to delete file: ' + err); + } + res.send('File deleted successfully.'); + }); +}); + app.use(express.static(path.join(__dirname, "web"))); export function startServer() { @@ -37,4 +60,4 @@ export function startServer() { app.listen(port, () => { console.log(LoggerColors.Cyan,`Add sounds at http://localhost:${port}, or drop in the sounds folder.`); }); -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e8ff9c..015e4d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,14 @@ "dotenv": "~16.3.1", "express": "~4.18.2", "multer": "~1.4.5-lts.1", + "node-schedule": "^2.1.1", "sodium": "~3.0.2", - "ts-dotenv": "^0.9.1", "ts-node": "~10.9.1", "typescript": "~5.2.2" }, "devDependencies": { - "@types/express": "~4.17.18" + "@types/express": "~4.17.18", + "@types/node-schedule": "^2.1.1" } }, "node_modules/@cspotcode/source-map-support": { @@ -268,6 +269,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" }, + "node_modules/@types/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-FaqkbBizA+DinA0XWtAhdbEXykUkkqzBWT4BSnhn71z9C+vvcDgNcHvTP59nBhMg3o39E/ZY8zB/AQ6/HGuRag==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -475,6 +485,17 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -756,6 +777,19 @@ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, + "node_modules/luxon": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-bytes.js": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.5.0.tgz", @@ -871,6 +905,19 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" }, + "node_modules/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "dependencies": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1101,6 +1148,11 @@ "node-addon-api": "*" } }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1138,11 +1190,6 @@ "node": ">=0.6" } }, - "node_modules/ts-dotenv": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/ts-dotenv/-/ts-dotenv-0.9.1.tgz", - "integrity": "sha512-oTTXmjSMkCIK8d+uKbQnHh7JS9zEYGn3pq7nE0qHVERq+6Mx9wZKcXVAT+7X0qhOWpRz10dqrJCFFcKwunYz5w==" - }, "node_modules/ts-mixer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", diff --git a/package.json b/package.json index a717ca4..cdc5bbc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Discord bot randomly plays sounds", "scripts": { "start": "ts-node bot.ts", - "create": "echo TOKEN=MY-API-TOKEN > .env", + "create": "node -e \"require('fs').writeFileSync('.env', 'TOKEN=MY-API-TOKEN\\nINTERVALMIN_MINUTES=10\\nINTERVALMAX_HOURS=6\\nVOICECHANNELRETRIES=12')\"", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "SocksOnHead", @@ -17,11 +17,14 @@ "dotenv": "~16.3.1", "express": "~4.18.2", "multer": "~1.4.5-lts.1", + "node-schedule": "~2.1.1", + "libsodium-wrappers": "~0.7.13", "sodium": "~3.0.2", "ts-node": "~10.9.1", "typescript": "~5.2.2" }, "devDependencies": { - "@types/express": "~4.17.18" + "@types/express": "~4.17.18", + "@types/node-schedule": "^2.1.1" } }