diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..029e07a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.env.* +*.log +.DS_Store +Dockerfile +.dockerignore +CLAUDE.md +todo.todo \ No newline at end of file diff --git a/.gitignore b/.gitignore index 75c9369..79244e8 100755 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ logs start.bat tokens.json -config.json +config.*json +!config.example.json +.env *.todo \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8fdd954 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5e27ebc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Discord bot built with discord.js v14 that provides sound effects (both prefix and slash commands), text commands, fun facts, and scheduled events functionality for Discord servers. + +## Development Commands + +### Running the Bot + +```bash +# Install dependencies +pnpm install + +# Direct Node.js execution (requires Node 22 LTS) +pnpm start # Production mode +pnpm dev # Development mode with auto-reload + +# Docker commands +pnpm docker:build # Build Docker image +pnpm docker:run # Run Docker container with auto-restart +``` + +### Docker Management + +```bash +# Stop and remove container +docker stop ghbot && docker rm ghbot + +# View logs +docker logs ghbot -f + +# Rebuild and restart +docker stop ghbot && docker rm ghbot && pnpm docker:build && pnpm docker:run +``` + +## Architecture & Key Components + +### Core Structure + +- **src/index.js**: Main bot entry point with Discord.js v14 client +- **src/config/**: Configuration management + - `config.js`: Config loader and validator + - `intents.js`: Discord gateway intents +- **src/commands/**: + - `prefix/`: Traditional prefix commands (!sfx, !funfact, etc.) + - `slash/`: Slash commands (/sfx with autocomplete) +- **src/services/**: + - `voiceService.js`: Voice connections using @discordjs/voice + - `commandLoader.js`: Static/Ankhbot command loader + - `sfxManager.js`: Sound effect file management + - `schedulerService.js`: Scheduled events handler +- **src/utils/**: Helper functions +- **config.json**: Bot configuration (copy from config.json.example) + +### Command System + +Commands are handled in priority order: +1. Native commands (defined in index.js commands object) +2. Static text commands (from conf/text_commands) +3. Ankhbot imported commands (from conf/ghbot.abcomg) + +### Sound Effects System + +- Sound files stored in `sfx/` directory as .mp3 or .wav +- Automatically discovered on startup and directory changes +- Requires voice channel connection and ffmpeg (included in Docker image) +- Volume and audio passes configurable per guild + +### Configuration Files + +- **conf/text_commands**: Pipe-delimited text commands with alias support +- **conf/funfacts & conf/hamfacts**: Line-separated fact collections +- **conf/snesgames.json**: SNES game database (purpose unclear from current usage) + +### Scheduled Events + +Configured per guild in config.json with cron-style scheduling using node-schedule. Events can: +- Send messages to specific channels +- Ping specific roles +- Run on cron schedules + +### Docker Setup + +Uses Node 22-alpine base image with ffmpeg for audio processing. The Dockerfile installs all dependencies and runs the bot with `node src/index.js`. + +## Important Implementation Details + +- Discord.js v14 with modern API patterns +- @discordjs/voice for audio playback +- Hybrid command system: prefix commands + slash command for SFX +- /sfx slash command with autocomplete for easy sound discovery +- Hot-reloads configuration files and commands without restart +- Supports multiple guilds with individual configurations +- Admin commands restricted by Discord user ID +- Blacklist system for blocking specific users +- Proper voice connection pooling and cleanup \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6bfd0dd..e574f19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,33 @@ -# Use the official Node.js image as the base image -FROM node:14 +# Build stage +FROM node:22-alpine AS builder -# Set the working directory inside the container WORKDIR /app -# Copy package.json and package-lock.json to the working directory -COPY package*.json ./ +# Copy package files +COPY package*.json pnpm-lock.yaml* ./ -# Install the dependencies -RUN npm install +# Install latest pnpm and build dependencies (including ffmpeg for audio processing) +RUN npm install -g pnpm@latest && \ + apk add --no-cache python3 make g++ ffmpeg opus-dev -# Copy the rest of the application code to the working directory +# Install dependencies (including native modules that need compilation) +RUN pnpm install --force + +# Production stage +FROM node:22-alpine + +WORKDIR /app + +# Install latest pnpm and runtime dependencies +RUN npm install -g pnpm@latest && \ + apk add --no-cache ffmpeg opus + +# Copy package files and installed dependencies from builder +COPY package*.json pnpm-lock.yaml* ./ +COPY --from=builder /app/node_modules ./node_modules + +# Copy application code COPY . . # Start the bot -CMD [ "node", "index.js" ] +CMD [ "node", "src/index.js" ] \ No newline at end of file diff --git a/conf/text_commands b/conf/text_commands index afc2daf..0e74dbe 100755 --- a/conf/text_commands +++ b/conf/text_commands @@ -1,4 +1,2 @@ f|is for :frog: -ladeda|https://www.youtube.com/watch?v=V0HCZ4YGqbw -fine,ez,steve|https://www.youtube.com/watch?v=_c1NJQ0UP_Q -imelly|Didn't know this was a political channel. WTF, you don't have sites where you can vomit that garbage out for the peanut gallery? Not everyone holds your same position. Why do that? Don't assume everyone is a socialist. Knock it off. I didn't join here to listen to your political BS. I am not paying a subscription to listen to you people pontificate about your political hangups with normal people. Unsubscribed, unfollowed. -iMellyGurl \ No newline at end of file +imelly|Didn't know this was a political channel. WTF, you don't have sites where you can vomit that garbage out for the peanut gallery? Not everyone holds your same position. Why do that? Don't assume everyone is a socialist. Knock it off. I didn't join here to listen to your political BS. I am not paying a subscription to listen to you people pontificate about your political hangups with normal people. Unsubscribed, unfollowed. -iMellyGurl diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..74b692c --- /dev/null +++ b/config.example.json @@ -0,0 +1,50 @@ +{ + "botName": "greenhambot", + "discord": { + "clientId": "YOUR DISCORD APP ID", + "token": "YOUR DISCORD APP TOKEN", + "adminUserId": "YOUR DISCORD USER ID", + "guilds": [ + { + "internalName": "GUILD NAME", + "id": "GUILD ID", + "prefix": "!", + "enableSfx": true, + "allowedSfxChannels": "piped|list|of-valid-channels", + "sfxVolume": 0.5, + "passes": 2, + "enableFunFacts": true, + "enableHamFacts": true, + "scheduledEvents": [ + { + "id": "rise-up-and-kick-a-little-ass", + "schedule": { + "hour": 7, + "minute": 30, + "tz": "America/Los_Angeles" + }, + "channelId": "DISCORD CHANNEL ID", + "pingRoleId": "DISCORD ROLE ID", + "message": "I'm gonna rise up, I'm gonna kick a little ass, Gonna kick some ass in the USA, Gonna climb a mountain, Gonna sew a flag, Gonna fly on an Eagle, I'm gonna kick some butt, I'm gonna drive a big truck, I'm gonna rule this world, Gonna kick some ass, Gonna rise up, Kick a little ass" + } + ] + }, + { + "internalName": "SECOND GUILD NAME", + "id": "SECOND GUILD ID", + "prefix": "!", + "enableSfx": true, + "allowedSfxChannels": "piped|list|of-valid-channels", + "sfxVolume": 0.5, + "passes": 2, + "enableFunFacts": true, + "enableHamFacts": true + } + ], + "activities": [ + "that gum you like" + ], + "blacklistedUsers": ["IGNORE COMMANDS FROM THESE DISCORD USER IDS"] + }, + "debug": false +} \ No newline at end of file diff --git a/config.json.example b/config.json.example deleted file mode 100755 index bc460f4..0000000 --- a/config.json.example +++ /dev/null @@ -1,37 +0,0 @@ -{ - "botName": "greenhambot", - "debug": false, - "discord": { - "token": "YOUR DISCORD APP TOKEN", - "adminID": "YOUR DISCORD USER ID", - "master": true, - "guilds": { - "GUILD ID": { - "internalName": "GUILD NAME", - "id": "GUILD ID", - "prefix": "!", - "enableSfx": true, - "allowedSfxChannels": "piped|list|of-valid-channels", - "sfxVolume": 0.5, - "passes": 2, - "enableFunFacts": true, - "enableHamFacts": true - }, - "SECOND GUILD ID": { - "internalName": "SECOND GUILD NAME", - "id": "SECOND GUILD ID", - "prefix": "!", - "enableSfx": true, - "allowedSfxChannels": "piped|list|of-valid-channels", - "sfxVolume": 0.5, - "passes": 2, - "enableFunFacts": true, - "enableHamFacts": true - }, - }, - "activities": [ - "that gum you like" - ], - "blacklistedUsers": [] - } -} \ No newline at end of file diff --git a/index.js b/legacy.js similarity index 96% rename from index.js rename to legacy.js index 6cbff58..688b0cd 100644 --- a/index.js +++ b/legacy.js @@ -319,7 +319,7 @@ function init(config) { } }, reboot: (msg, guildConfig) => { - if (msg.author.id == config.discord.adminID) process.exit(); // Requires a node module like Forever to work. + if (msg.author.id == config.discord.adminUserId) process.exit(); // Requires a node module like Forever to work. }, }; @@ -533,8 +533,6 @@ process.on("unhandledRejection", console.error); init(config); Discord.Client.prototype.setRandomActivity = function () { - if (!config.discord.master) return; - let activity = config.discord.activities.length > 0 ? randElement(config.discord.activities) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 361c967..0000000 --- a/package-lock.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "name": "ghbot", - "version": "1.6.9", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@discordjs/collection": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", - "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" - }, - "@discordjs/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "cron-parser": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", - "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", - "requires": { - "is-nan": "^1.3.2", - "luxon": "^1.26.0" - } - }, - "define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - } - }, - "define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "requires": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "discord.js": { - "version": "12.5.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", - "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", - "requires": { - "@discordjs/collection": "^0.1.6", - "@discordjs/form-data": "^3.0.1", - "abort-controller": "^3.0.0", - "node-fetch": "^2.6.1", - "prism-media": "^1.2.9", - "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.3", - "ws": "^7.4.4" - } - }, - "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "requires": { - "es-define-property": "^1.0.0" - } - }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "libsodium": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", - "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==" - }, - "libsodium-wrappers": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz", - "integrity": "sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==", - "requires": { - "libsodium": "^0.7.0" - } - }, - "long-timeout": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", - "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" - }, - "luxon": { - "version": "1.28.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", - "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-schedule": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.0.0.tgz", - "integrity": "sha512-cHc9KEcfiuXxYDU+HjsBVo2FkWL1jRAUoczFoMIzRBpOA4p/NRHuuLs85AWOLgKsHtSPjN8csvwIxc2SqMv+CQ==", - "requires": { - "cron-parser": "^3.1.0", - "long-timeout": "0.1.1", - "sorted-array-functions": "^1.3.0" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "opusscript": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz", - "integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==" - }, - "prism-media": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", - "integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==" - }, - "set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", - "requires": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "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==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==" - } - } -} diff --git a/package.json b/package.json index f38178f..52b4cd7 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,28 @@ { "name": "ghbot", - "version": "1.6.9", + "version": "2.0.0", "description": "", - "main": "index.js", + "main": "src/index.js", "dependencies": { - "axios": "^0.21.2", - "discord.js": "^12.5.3", - "libsodium-wrappers": "^0.7.9", - "node-schedule": "^2.0.0", - "opusscript": "^0.0.8" + "axios": "^1.11.0", + "discord.js": "^14.21.0", + "@discordjs/voice": "^0.18.0", + "@discordjs/opus": "^0.9.0", + "opusscript": "^0.1.1", + "ffmpeg-static": "^5.2.0", + "node-schedule": "^2.1.1", + "sodium-native": "^4.3.3" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "nodemon": "^3.1.9" }, - "devDependencies": {}, "scripts": { - "build": "docker build -t ghbot:latest .", - "start": "docker run -d --name ghbot ghbot:latest" + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "docker:build": "docker build -t ghbot:${VERSION:-latest} .", + "docker:run": "docker run -d --name ghbot --restart always ghbot:${VERSION:-latest}" }, - "author": "", + "author": "https://github.com/greenham", "license": "MIT" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6402770 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1111 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@discordjs/opus': + specifier: ^0.9.0 + version: 0.9.0 + '@discordjs/voice': + specifier: ^0.18.0 + version: 0.18.0(@discordjs/opus@0.9.0)(ffmpeg-static@5.2.0) + axios: + specifier: ^1.11.0 + version: 1.11.0 + discord.js: + specifier: ^14.21.0 + version: 14.21.0 + ffmpeg-static: + specifier: ^5.2.0 + version: 5.2.0 + node-schedule: + specifier: ^2.1.1 + version: 2.1.1 + sodium-native: + specifier: ^4.3.3 + version: 4.3.3 + +devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.17.2 + nodemon: + specifier: ^3.1.9 + version: 3.1.10 + +packages: + + /@derhuerst/http-basic@8.2.4: + resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} + engines: {node: '>=6.0.0'} + dependencies: + caseless: 0.12.0 + concat-stream: 2.0.0 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + dev: false + + /@discordjs/builders@1.11.3: + resolution: {integrity: sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/formatters': 0.6.1 + '@discordjs/util': 1.1.1 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.20 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + dev: false + + /@discordjs/collection@1.5.3: + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + dev: false + + /@discordjs/collection@2.1.1: + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + dev: false + + /@discordjs/formatters@0.6.1: + resolution: {integrity: sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==} + engines: {node: '>=16.11.0'} + dependencies: + discord-api-types: 0.38.20 + dev: false + + /@discordjs/node-pre-gyp@0.4.5: + resolution: {integrity: sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==} + hasBin: true + dependencies: + detect-libc: 2.0.4 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.2 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@discordjs/opus@0.9.0: + resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@discordjs/node-pre-gyp': 0.4.5 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@discordjs/rest@2.5.1: + resolution: {integrity: sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw==} + engines: {node: '>=18'} + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.3 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.38.20 + magic-bytes.js: 1.12.1 + tslib: 2.8.1 + undici: 6.21.3 + dev: false + + /@discordjs/util@1.1.1: + resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} + engines: {node: '>=18'} + dev: false + + /@discordjs/voice@0.18.0(@discordjs/opus@0.9.0)(ffmpeg-static@5.2.0): + resolution: {integrity: sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==} + engines: {node: '>=18'} + dependencies: + '@types/ws': 8.18.1 + discord-api-types: 0.37.120 + prism-media: 1.3.5(@discordjs/opus@0.9.0)(ffmpeg-static@5.2.0) + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - '@discordjs/opus' + - bufferutil + - ffmpeg-static + - node-opus + - opusscript + - utf-8-validate + dev: false + + /@discordjs/ws@1.2.3: + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.5.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.38.20 + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@sapphire/async-queue@1.5.5: + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@sapphire/shapeshift@4.0.0: + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + dev: false + + /@sapphire/snowflake@3.5.3: + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@types/node@10.17.60: + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + dev: false + + /@types/node@22.17.2: + resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + dependencies: + undici-types: 6.21.0 + + /@types/ws@8.18.1: + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + dependencies: + '@types/node': 22.17.2 + dev: false + + /@vladfrangu/async_event_emitter@2.4.6: + resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + dev: false + + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /bare-addon-resolve@1.9.4(bare-url@2.2.1): + resolution: {integrity: sha512-unn6Vy/Yke6F99vg/7tcrvM2KUvIhTNniaSqDbam4AWkd4NhvDVSrQiRYVlNzUV2P7SPobkCK7JFVxrJk9btCg==} + peerDependencies: + bare-url: '*' + peerDependenciesMeta: + bare-url: + optional: true + dependencies: + bare-module-resolve: 1.11.1(bare-url@2.2.1) + bare-semver: 1.0.1 + bare-url: 2.2.1 + dev: false + + /bare-module-resolve@1.11.1(bare-url@2.2.1): + resolution: {integrity: sha512-DCxeT9i8sTs3vUMA3w321OX/oXtNEu5EjObQOnTmCdNp5RXHBAvAaBDHvAi9ta0q/948QPz+co6SsGi6aQMYRg==} + peerDependencies: + bare-url: '*' + peerDependenciesMeta: + bare-url: + optional: true + dependencies: + bare-semver: 1.0.1 + bare-url: 2.2.1 + dev: false + + /bare-os@3.6.1: + resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} + engines: {bare: '>=1.14.0'} + dev: false + + /bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + dependencies: + bare-os: 3.6.1 + dev: false + + /bare-semver@1.0.1: + resolution: {integrity: sha512-UtggzHLiTrmFOC/ogQ+Hy7VfoKoIwrP1UFcYtTxoCUdLtsIErT8+SWtOC2DH/snT9h+xDrcBEPcwKei1mzemgg==} + dev: false + + /bare-url@2.2.1: + resolution: {integrity: sha512-5Ms88Fgq8eMQqwxet7fqGJoOwFdj8k+NDYCjEkL2OdS/WCeeo7j5TsZCDGkE8VrAd4ss8uQUC1u1d5khpujZEw==} + dependencies: + bare-path: 3.0.0 + dev: false + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: false + + /caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: false + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + dev: false + + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false + + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.7.1 + dev: false + + /debug@4.4.1(supports-color@5.5.0): + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + + /detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + dev: false + + /discord-api-types@0.37.120: + resolution: {integrity: sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==} + dev: false + + /discord-api-types@0.38.20: + resolution: {integrity: sha512-wJSmFFi8eoFL/jIosUQLoXeCv7YK+l7joKmFCsnkx7HWSFt5xScNQdhvILLxC0oU6J5bK0ppR7GZ1d4NJScSNQ==} + dev: false + + /discord.js@14.21.0: + resolution: {integrity: sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ==} + engines: {node: '>=18'} + dependencies: + '@discordjs/builders': 1.11.3 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.1 + '@discordjs/rest': 2.5.1 + '@discordjs/util': 1.1.1 + '@discordjs/ws': 1.2.3 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.38.20 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.12.1 + tslib: 2.8.1 + undici: 6.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: false + + /es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + + /es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: false + + /es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: false + + /ffmpeg-static@5.2.0: + resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==} + engines: {node: '>=16'} + requiresBuild: true + dependencies: + '@derhuerst/http-basic': 8.2.4 + env-paths: 2.2.1 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + dev: false + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: false + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + + /get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: false + + /get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.1.0 + dev: false + + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-response-object@3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + dependencies: + '@types/node': 10.17.60 + dev: false + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + dev: false + + /luxon@3.7.1: + resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} + engines: {node: '>=12'} + dev: false + + /magic-bytes.js@1.12.1: + resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} + dev: false + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: false + + /math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.12 + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + dev: false + + /nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.6.0 + debug: 4.4.1(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.2 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + dev: true + + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + + /parse-cache-control@1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + dev: false + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: false + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /prism-media@1.3.5(@discordjs/opus@0.9.0)(ffmpeg-static@5.2.0): + resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==} + peerDependencies: + '@discordjs/opus': '>=0.8.0 <1.0.0' + ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0 + node-opus: ^0.3.3 + opusscript: ^0.0.8 + peerDependenciesMeta: + '@discordjs/opus': + optional: true + ffmpeg-static: + optional: true + node-opus: + optional: true + opusscript: + optional: true + dependencies: + '@discordjs/opus': 0.9.0 + ffmpeg-static: 5.2.0 + dev: false + + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: false + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /require-addon@1.1.0: + resolution: {integrity: sha512-KbXAD5q2+v1GJnkzd8zzbOxchTkStSyJZ9QwoCq3QwEXAaIlG3wDYRZGzVD357jmwaGY7hr5VaoEAL0BkF0Kvg==} + engines: {bare: '>=1.10.0'} + dependencies: + bare-addon-resolve: 1.9.4(bare-url@2.2.1) + bare-url: 2.2.1 + dev: false + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + dependencies: + glob: 7.2.3 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: false + + /semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.7.2 + dev: true + + /sodium-native@4.3.3: + resolution: {integrity: sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==} + dependencies: + require-addon: 1.1.0 + dev: false + + /sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + dev: false + + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + dev: false + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + /undici@6.21.3: + resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} + engines: {node: '>=18.17'} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + + /ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false diff --git a/src/commands/prefix/dance.js b/src/commands/prefix/dance.js new file mode 100644 index 0000000..726895c --- /dev/null +++ b/src/commands/prefix/dance.js @@ -0,0 +1,10 @@ +module.exports = { + name: 'dance', + description: 'Make the bot dance!', + + async execute(message, args, guildConfig) { + await message.channel.send( + '*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*' + ); + } +}; \ No newline at end of file diff --git a/src/commands/prefix/funfact.js b/src/commands/prefix/funfact.js new file mode 100644 index 0000000..5df761f --- /dev/null +++ b/src/commands/prefix/funfact.js @@ -0,0 +1,79 @@ +const { EmbedBuilder } = require('discord.js'); +const fs = require('fs'); +const path = require('path'); + +class FunFactCommand { + constructor() { + this.funFactsPath = path.join(__dirname, '..', '..', '..', 'conf', 'funfacts'); + this.funFacts = []; + this.loadFunFacts(); + this.watchFile(); + } + + loadFunFacts() { + try { + if (!fs.existsSync(this.funFactsPath)) { + console.log('Fun facts file not found'); + return; + } + + const data = fs.readFileSync(this.funFactsPath, 'utf-8'); + this.funFacts = data.split('\n').filter(line => line.trim().length > 0); + console.log(`Loaded ${this.funFacts.length} fun facts`); + } catch (error) { + console.error('Error loading fun facts:', error); + } + } + + watchFile() { + if (fs.existsSync(this.funFactsPath)) { + fs.watchFile(this.funFactsPath, (curr, prev) => { + if (curr.mtime !== prev.mtime) { + console.log('Fun facts file changed, reloading...'); + this.loadFunFacts(); + } + }); + } + } + + async execute(message, args, guildConfig) { + if (guildConfig.enableFunFacts === false) { + return; + } + + if (this.funFacts.length === 0) { + return message.channel.send('No fun facts found!'); + } + + // Check if a specific fact number was requested + let factIndex; + const requestedNum = parseInt(args[0]); + + if (!isNaN(requestedNum) && requestedNum > 0 && requestedNum <= this.funFacts.length) { + factIndex = requestedNum - 1; + } else { + factIndex = Math.floor(Math.random() * this.funFacts.length); + } + + const displayNum = factIndex + 1; + const funFact = this.funFacts[factIndex]; + + const embed = new EmbedBuilder() + .setTitle(`FunFact #${displayNum}`) + .setColor(0x21c629) + .setDescription(funFact); + + await message.channel.send({ embeds: [embed] }); + } +} + +const funFactCommand = new FunFactCommand(); + +module.exports = { + name: 'funfact', + description: 'Get a random fun fact', + + async execute(message, args, guildConfig) { + await funFactCommand.execute(message, args, guildConfig); + } +}; \ No newline at end of file diff --git a/src/commands/prefix/hamfact.js b/src/commands/prefix/hamfact.js new file mode 100644 index 0000000..a19dccd --- /dev/null +++ b/src/commands/prefix/hamfact.js @@ -0,0 +1,79 @@ +const { EmbedBuilder } = require('discord.js'); +const fs = require('fs'); +const path = require('path'); + +class HamFactCommand { + constructor() { + this.hamFactsPath = path.join(__dirname, '..', '..', '..', 'conf', 'hamfacts'); + this.hamFacts = []; + this.loadHamFacts(); + this.watchFile(); + } + + loadHamFacts() { + try { + if (!fs.existsSync(this.hamFactsPath)) { + console.log('Ham facts file not found'); + return; + } + + const data = fs.readFileSync(this.hamFactsPath, 'utf-8'); + this.hamFacts = data.split('\n').filter(line => line.trim().length > 0); + console.log(`Loaded ${this.hamFacts.length} ham facts`); + } catch (error) { + console.error('Error loading ham facts:', error); + } + } + + watchFile() { + if (fs.existsSync(this.hamFactsPath)) { + fs.watchFile(this.hamFactsPath, (curr, prev) => { + if (curr.mtime !== prev.mtime) { + console.log('Ham facts file changed, reloading...'); + this.loadHamFacts(); + } + }); + } + } + + async execute(message, args, guildConfig) { + if (guildConfig.enableHamFacts === false) { + return; + } + + if (this.hamFacts.length === 0) { + return message.channel.send('No ham facts found!'); + } + + // Check if a specific fact number was requested + let factIndex; + const requestedNum = parseInt(args[0]); + + if (!isNaN(requestedNum) && requestedNum > 0 && requestedNum <= this.hamFacts.length) { + factIndex = requestedNum - 1; + } else { + factIndex = Math.floor(Math.random() * this.hamFacts.length); + } + + const displayNum = factIndex + 1; + const hamFact = this.hamFacts[factIndex]; + + const embed = new EmbedBuilder() + .setTitle(`HamFact #${displayNum}`) + .setColor(0x21c629) + .setDescription(hamFact); + + await message.channel.send({ embeds: [embed] }); + } +} + +const hamFactCommand = new HamFactCommand(); + +module.exports = { + name: 'hamfact', + description: 'Get a random ham fact', + + async execute(message, args, guildConfig) { + await hamFactCommand.execute(message, args, guildConfig); + } +}; \ No newline at end of file diff --git a/src/commands/prefix/index.js b/src/commands/prefix/index.js new file mode 100644 index 0000000..9929082 --- /dev/null +++ b/src/commands/prefix/index.js @@ -0,0 +1,67 @@ +const fs = require('fs'); +const path = require('path'); + +class PrefixCommandHandler { + constructor() { + this.commands = new Map(); + this.loadCommands(); + } + + /** + * Load all prefix command modules + */ + loadCommands() { + const commandFiles = fs.readdirSync(__dirname) + .filter(file => file.endsWith('.js') && file !== 'index.js'); + + for (const file of commandFiles) { + const command = require(path.join(__dirname, file)); + + // Register command and any aliases + if (command.name) { + this.commands.set(command.name, command); + + if (command.aliases && Array.isArray(command.aliases)) { + for (const alias of command.aliases) { + this.commands.set(alias, command); + } + } + } + } + + console.log(`Loaded ${this.commands.size} prefix commands`); + } + + /** + * Check if a command exists + * @param {string} commandName + * @returns {boolean} + */ + has(commandName) { + return this.commands.has(commandName); + } + + /** + * Execute a command + * @param {string} commandName + * @param {Message} message + * @param {Array} args + * @param {Object} guildConfig + */ + async execute(commandName, message, args, guildConfig) { + const command = this.commands.get(commandName); + + if (!command) { + return; + } + + try { + await command.execute(message, args, guildConfig); + } catch (error) { + console.error(`Error executing prefix command ${commandName}:`, error); + throw error; + } + } +} + +module.exports = new PrefixCommandHandler(); \ No newline at end of file diff --git a/src/commands/prefix/join.js b/src/commands/prefix/join.js new file mode 100644 index 0000000..44a087a --- /dev/null +++ b/src/commands/prefix/join.js @@ -0,0 +1,26 @@ +const voiceService = require('../../services/voiceService'); + +module.exports = { + name: 'join', + description: 'Make the bot join your voice channel', + + async execute(message, args, guildConfig) { + // Check if user is in a voice channel + if (!message.member.voice.channel) { + return message.reply('You need to be in a voice channel first!'); + } + + // Check if already connected + if (voiceService.isConnected(message.guild.id)) { + return message.reply("I'm already in a voice channel!"); + } + + try { + await voiceService.join(message.member.voice.channel); + await message.react('✅'); + } catch (error) { + console.error('Error joining voice channel:', error); + await message.reply("I couldn't connect to your voice channel. Make sure I have the proper permissions!"); + } + } +}; \ No newline at end of file diff --git a/src/commands/prefix/leave.js b/src/commands/prefix/leave.js new file mode 100644 index 0000000..164e3f6 --- /dev/null +++ b/src/commands/prefix/leave.js @@ -0,0 +1,16 @@ +const voiceService = require('../../services/voiceService'); + +module.exports = { + name: 'leave', + description: 'Make the bot leave the voice channel', + + async execute(message, args, guildConfig) { + // Check if connected to a voice channel + if (!voiceService.isConnected(message.guild.id)) { + return message.reply("If ya don't eat your meat, ya can't have any pudding!"); + } + + voiceService.leave(message.guild.id); + await message.react('👋'); + } +}; \ No newline at end of file diff --git a/src/commands/prefix/reboot.js b/src/commands/prefix/reboot.js new file mode 100644 index 0000000..a8e020e --- /dev/null +++ b/src/commands/prefix/reboot.js @@ -0,0 +1,19 @@ +const config = require("../../config/config"); + +module.exports = { + name: "reboot", + description: "Reboot the bot (admin only)", + + async execute(message, args, guildConfig) { + // Check if user is the bot admin + if (message.author.id !== config.discord.adminUserId) { + return; + } + + await message.reply("Rebooting..."); + console.log(`Reboot requested by ${message.author.username}`); + + // Exit the process - requires a process manager like PM2 or Docker restart policy + process.exit(0); + }, +}; diff --git a/src/commands/prefix/role.js b/src/commands/prefix/role.js new file mode 100644 index 0000000..7292012 --- /dev/null +++ b/src/commands/prefix/role.js @@ -0,0 +1,74 @@ +module.exports = { + name: 'role', + description: 'Add or remove allowed roles', + + async execute(message, args, guildConfig) { + // Check if there are allowed roles configured + if (!guildConfig.allowedRolesForRequest || guildConfig.allowedRolesForRequest.length === 0) { + return message.reply('No roles are currently allowed to be added/removed by members.'); + } + + // Show usage if no arguments + if (args.length === 0) { + return message.reply( + `Usage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}` + ); + } + + const action = args[0]?.toLowerCase(); + const roleName = args.slice(1).join(' '); + + // Validate action + if (!['add', 'remove'].includes(action)) { + return message.reply( + `You must use add/remove after the role command! *e.g. ${guildConfig.prefix}role add *` + ); + } + + // Validate role name + if (!roleName) { + return message.reply( + `Usage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}` + ); + } + + // Check if role is in the allowed list + const allowedRoles = guildConfig.allowedRolesForRequest.split('|'); + const roleRegex = new RegExp(guildConfig.allowedRolesForRequest, 'i'); + + if (!roleRegex.test(roleName)) { + return message.reply( + `**${roleName}** is not a valid role name! The roles allowed for request are: ${allowedRoles.join(', ')}` + ); + } + + // Find the role in the guild (case-sensitive search) + const role = message.guild.roles.cache.find(r => + r.name.toLowerCase() === roleName.toLowerCase() + ); + + if (!role) { + return message.reply(`${roleName} is not a role on this server!`); + } + + try { + if (action === 'add') { + await message.member.roles.add(role, 'User requested'); + await message.react('👍'); + console.log(`Added role ${role.name} to ${message.author.username}`); + } else if (action === 'remove') { + await message.member.roles.remove(role, 'User requested'); + await message.react('👍'); + console.log(`Removed role ${role.name} from ${message.author.username}`); + } + } catch (error) { + console.error(`Error managing role ${role.name}:`, error); + await message.react('⚠️'); + + // Send error message if we can't react + if (!message.reactions.cache.has('⚠️')) { + await message.reply('I encountered an error managing that role. Make sure I have the proper permissions!'); + } + } + } +}; \ No newline at end of file diff --git a/src/commands/prefix/sfx.js b/src/commands/prefix/sfx.js new file mode 100644 index 0000000..e41dc60 --- /dev/null +++ b/src/commands/prefix/sfx.js @@ -0,0 +1,87 @@ +const axios = require('axios'); +const { chunkSubstr } = require('../../utils/helpers'); +const sfxManager = require('../../services/sfxManager'); +const voiceService = require('../../services/voiceService'); + +module.exports = { + name: 'sfx', + description: 'Play a sound effect', + + async execute(message, args, guildConfig) { + // Check if SFX is allowed in this channel + if (guildConfig.allowedSfxChannels) { + const allowedChannels = new RegExp(guildConfig.allowedSfxChannels); + if (!allowedChannels.test(message.channel.name)) { + return; + } + } + + const sfxName = args[0]; + + // Log the SFX command + if (sfxName) { + console.log( + `SFX '${sfxName}' requested in ${guildConfig.internalName || message.guild.name}#${message.channel.name} from @${message.author.username}` + ); + } + + // If no SFX specified, show the list + if (!sfxName) { + try { + const response = await axios.get('https://rentry.co/ghbotsfx/raw'); + + // Break into chunks if message is too long + let chunks = [response.data]; + if (response.data.length > 2000) { + chunks = chunkSubstr(response.data, Math.ceil(response.data.length / 2)); + } + + for (const chunk of chunks) { + await message.channel.send(chunk); + } + } catch (error) { + console.error('Error fetching SFX list:', error); + await message.reply('Could not fetch the SFX list.'); + } + return; + } + + // Check if SFX exists + if (!sfxManager.hasSFX(sfxName)) { + return message.reply('This sound effect does not exist!'); + } + + // Check if user is in a voice channel + if (!message.member.voice.channel) { + return message.reply('You need to be in a voice channel to use this command!'); + } + + try { + // Join the voice channel + await voiceService.join(message.member.voice.channel); + + // Get the SFX file path + const sfxPath = sfxManager.getSFXPath(sfxName); + + // Play the sound effect + await voiceService.play( + message.guild.id, + sfxPath, + { + volume: guildConfig.sfxVolume || 0.5 + } + ); + + // Leave the voice channel after playing + setTimeout(() => { + voiceService.leave(message.guild.id); + }, 500); + + console.log(`✅ Successfully played SFX '${sfxName}'`); + + } catch (error) { + console.error(`❌ Error playing SFX '${sfxName}':`, error); + await message.reply("I couldn't play that sound effect. Make sure I have permission to join your voice channel!"); + } + } +}; \ No newline at end of file diff --git a/src/commands/slash/index.js b/src/commands/slash/index.js new file mode 100644 index 0000000..21dd55a --- /dev/null +++ b/src/commands/slash/index.js @@ -0,0 +1,77 @@ +const fs = require('fs'); +const path = require('path'); + +class SlashCommandHandler { + constructor() { + this.commands = new Map(); + this.loadCommands(); + } + + /** + * Load all slash command modules + */ + loadCommands() { + const commandFiles = fs.readdirSync(__dirname) + .filter(file => file.endsWith('.js') && file !== 'index.js'); + + for (const file of commandFiles) { + const command = require(path.join(__dirname, file)); + + if (command.data?.name) { + this.commands.set(command.data.name, command); + } + } + + console.log(`Loaded ${this.commands.size} slash commands`); + } + + /** + * Get slash command definitions for registration + * @returns {Array} + */ + getSlashCommandDefinitions() { + return Array.from(this.commands.values()).map(cmd => cmd.data.toJSON()); + } + + /** + * Execute a slash command + * @param {string} commandName + * @param {CommandInteraction} interaction + * @param {Object} guildConfig + */ + async execute(commandName, interaction, guildConfig) { + const command = this.commands.get(commandName); + + if (!command) { + return; + } + + try { + await command.execute(interaction, guildConfig); + } catch (error) { + console.error(`Error executing slash command ${commandName}:`, error); + throw error; + } + } + + /** + * Handle autocomplete interactions + * @param {AutocompleteInteraction} interaction + * @param {Object} guildConfig + */ + async handleAutocomplete(interaction, guildConfig) { + const command = this.commands.get(interaction.commandName); + + if (!command || !command.autocomplete) { + return; + } + + try { + await command.autocomplete(interaction, guildConfig); + } catch (error) { + console.error(`Error handling autocomplete for ${interaction.commandName}:`, error); + } + } +} + +module.exports = new SlashCommandHandler(); \ No newline at end of file diff --git a/src/commands/slash/sfx.js b/src/commands/slash/sfx.js new file mode 100644 index 0000000..23d4430 --- /dev/null +++ b/src/commands/slash/sfx.js @@ -0,0 +1,108 @@ +const { SlashCommandBuilder } = require('discord.js'); +const sfxManager = require('../../services/sfxManager'); +const voiceService = require('../../services/voiceService'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('sfx') + .setDescription('Play a sound effect') + .addStringOption(option => + option.setName('sound') + .setDescription('The sound effect to play') + .setRequired(true) + .setAutocomplete(true) + ), + + async execute(interaction, guildConfig) { + // Check if SFX is allowed in this channel + if (guildConfig.allowedSfxChannels) { + const allowedChannels = new RegExp(guildConfig.allowedSfxChannels); + if (!allowedChannels.test(interaction.channel.name)) { + return interaction.reply({ + content: 'Sound effects are not allowed in this channel!', + ephemeral: true + }); + } + } + + const sfxName = interaction.options.getString('sound'); + + // Log the slash command SFX request + console.log( + `/sfx '${sfxName}' requested in ${guildConfig.internalName || interaction.guild.name}#${interaction.channel.name} from @${interaction.user.username}` + ); + + // Check if SFX exists + if (!sfxManager.hasSFX(sfxName)) { + return interaction.reply({ + content: 'This sound effect does not exist!', + ephemeral: true + }); + } + + // Check if user is in a voice channel + const member = interaction.member; + if (!member.voice.channel) { + return interaction.reply({ + content: 'You need to be in a voice channel to use this command!', + ephemeral: true + }); + } + + // Defer the reply as joining voice might take a moment + await interaction.deferReply(); + + try { + // Join the voice channel + await voiceService.join(member.voice.channel); + + // Get the SFX file path + const sfxPath = sfxManager.getSFXPath(sfxName); + + // Play the sound effect + await voiceService.play( + interaction.guild.id, + sfxPath, + { + volume: guildConfig.sfxVolume || 0.5 + } + ); + + // Update the reply + await interaction.editReply(`Playing sound effect: **${sfxName}**`); + + // Leave the voice channel after playing + setTimeout(() => { + voiceService.leave(interaction.guild.id); + }, 500); + + console.log(`✅ Successfully played /sfx '${sfxName}'`); + + } catch (error) { + console.error(`❌ Error playing /sfx '${sfxName}':`, error); + await interaction.editReply({ + content: "I couldn't play that sound effect. Make sure I have permission to join your voice channel!" + }); + } + }, + + async autocomplete(interaction, guildConfig) { + const focusedValue = interaction.options.getFocused().toLowerCase(); + + // Get all SFX names + const choices = sfxManager.getSFXNames(); + + // Filter based on what the user has typed + const filtered = choices + .filter(choice => choice.toLowerCase().includes(focusedValue)) + .slice(0, 25); // Discord limits autocomplete to 25 choices + + // Respond with the filtered choices + await interaction.respond( + filtered.map(choice => ({ + name: choice, + value: choice + })) + ); + } +}; \ No newline at end of file diff --git a/src/config/config.js b/src/config/config.js new file mode 100644 index 0000000..92ee0d0 --- /dev/null +++ b/src/config/config.js @@ -0,0 +1,26 @@ +const fs = require("fs"); +const path = require("path"); + +// Load config from root directory +const configPath = path.join(__dirname, "..", "..", "config.json"); +const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); + +// Validate required config fields +function validateConfig(config) { + if (!config.discord?.token) { + throw new Error("Discord token is required in config.json"); + } + + if (!config.discord?.guilds || !Array.isArray(config.discord.guilds)) { + throw new Error("Discord guilds configuration is required"); + } + + // Ensure guilds is an array (supporting both old object format and new array format) + if (!Array.isArray(config.discord.guilds)) { + config.discord.guilds = Object.values(config.discord.guilds); + } + + return config; +} + +module.exports = validateConfig(config); diff --git a/src/config/intents.js b/src/config/intents.js new file mode 100644 index 0000000..c8e24af --- /dev/null +++ b/src/config/intents.js @@ -0,0 +1,10 @@ +const { GatewayIntentBits } = require('discord.js'); + +module.exports = [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.MessageContent, // Required for prefix commands + GatewayIntentBits.GuildMembers // Required for role management + // GatewayIntentBits.GuildPresences - Requires special permission in Discord Developer Portal +]; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cff335d --- /dev/null +++ b/src/index.js @@ -0,0 +1,253 @@ +const { + Client, + Events, + EmbedBuilder, + REST, + Routes, + ActivityType, +} = require("discord.js"); +const { generateDependencyReport } = require("@discordjs/voice"); +const intents = require("./config/intents"); +const config = require("./config/config"); +const { randElement } = require("./utils/helpers"); + +// Log audio dependencies status +console.log("Audio Dependencies Status:"); +console.log(generateDependencyReport()); + +// Initialize Discord client +const client = new Client({ intents }); + +// Services +const commandLoader = require("./services/commandLoader"); +const schedulerService = require("./services/schedulerService"); + +// Command handlers +const prefixCommands = require("./commands/prefix"); +const slashCommands = require("./commands/slash"); + +// Activity rotation +let activityInterval; + +/** + * Set a random activity for the bot + */ +function setRandomActivity() { + const activity = + config.discord.activities?.length > 0 + ? randElement(config.discord.activities) + : "DESTROY ALL HUMANS"; + + console.log(`Setting Discord activity to: ${activity}`); + + client.user.setActivity(activity, { + url: "https://twitch.tv/fgfm", + type: ActivityType.Streaming, + }); +} + +/** + * Register slash commands + */ +async function registerSlashCommands() { + const rest = new REST({ version: "10" }).setToken(config.discord.token); + + try { + console.log("Started refreshing application (/) commands."); + + // Get all slash command definitions + const commands = slashCommands.getSlashCommandDefinitions(); + + // Register commands for each guild + for (const guild of config.discord.guilds) { + await rest.put( + Routes.applicationGuildCommands(client.user.id, guild.id), + { body: commands } + ); + console.log( + `Registered slash commands for guild: ${guild.internalName || guild.id}` + ); + } + + console.log("Successfully reloaded application (/) commands."); + } catch (error) { + console.error("Error registering slash commands:", error); + } +} + +// Client ready event +client.once(Events.ClientReady, async () => { + console.log(`✅ ${config.botName} is connected and ready!`); + console.log(`Logged in as ${client.user.tag}`); + console.log(`Serving ${client.guilds.cache.size} guild(s)`); + + // Set initial activity + setRandomActivity(); + + // Rotate activity every hour + activityInterval = setInterval(() => { + setRandomActivity(); + }, 3600 * 1000); + + // Register slash commands + await registerSlashCommands(); + + // Initialize scheduled events + schedulerService.initialize(client, config); +}); + +// Message handler for prefix commands +client.on(Events.MessageCreate, async (message) => { + // Ignore bot messages + if (message.author.bot) return; + + // Ignore DMs if not configured + if (!message.guild) return; + + // Check if guild is configured + const guildConfig = config.discord.guilds.find( + (g) => g.id === message.guild.id + ); + if (!guildConfig) return; + + // Check blacklist + if (config.discord.blacklistedUsers?.includes(message.author.id)) return; + + // Check for command prefix + if (!message.content.startsWith(guildConfig.prefix)) return; + + // Parse command + const args = message.content + .slice(guildConfig.prefix.length) + .trim() + .split(/ +/); + const commandName = args.shift().toLowerCase(); + + console.log( + `Command '${commandName}' received in ${ + guildConfig.internalName || message.guild.name + }#${message.channel.name} from @${message.author.username}` + ); + + try { + // Check for prefix commands + if (prefixCommands.has(commandName)) { + await prefixCommands.execute(commandName, message, args, guildConfig); + return; + } + + // Check for static commands + if (commandLoader.hasStaticCommand(commandName)) { + const response = commandLoader.getStaticCommand(commandName); + const embed = new EmbedBuilder() + .setTitle(commandName) + .setColor(0x21c629) + .setDescription(response); + + await message.channel.send({ embeds: [embed] }); + return; + } + + // Check for Ankhbot commands + if (commandLoader.hasAnkhbotCommand(commandName)) { + const response = commandLoader.getAnkhbotCommand(commandName); + const embed = new EmbedBuilder() + .setTitle(commandName) + .setColor(0x21c629) + .setDescription(response); + + await message.channel.send({ embeds: [embed] }); + return; + } + + // Command not found - ignore silently + } catch (error) { + console.error(`Error executing command ${commandName}:`, error); + message + .reply("There was an error executing that command!") + .catch(console.error); + } +}); + +// Interaction handler for slash commands +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) + return; + + // Get guild config + const guildConfig = config.discord.guilds.find( + (g) => g.id === interaction.guild.id + ); + if (!guildConfig) return; + + try { + if (interaction.isAutocomplete()) { + await slashCommands.handleAutocomplete(interaction, guildConfig); + } else if (interaction.isChatInputCommand()) { + await slashCommands.execute( + interaction.commandName, + interaction, + guildConfig + ); + } + } catch (error) { + console.error("Error handling interaction:", error); + + if (interaction.isChatInputCommand() && !interaction.replied) { + await interaction + .reply({ + content: "There was an error executing this command!", + ephemeral: true, + }) + .catch(console.error); + } + } +}); + +// Handle new guild members +client.on(Events.GuildMemberAdd, (member) => { + // Check if guild is configured + const guildConfig = config.discord.guilds.find( + (g) => g.id === member.guild.id + ); + if (!guildConfig) return; + + console.log( + `A new member has joined '${member.guild.name}': ${member.displayName}` + ); +}); + +// Handle guild becoming unavailable +client.on(Events.GuildUnavailable, (guild) => { + console.log( + `Guild '${guild.name}' is no longer available! Most likely due to server outage.` + ); +}); + +// Debug logging +client.on("debug", (info) => { + if (config.debug === true) { + console.log(`[${new Date().toISOString()}] DEBUG: ${info}`); + } +}); + +// Error handling +client.on("error", console.error); + +// Process error handling +process.on("unhandledRejection", console.error); + +// Graceful shutdown +process.on("SIGTERM", () => { + console.log("SIGTERM signal received, shutting down gracefully..."); + + if (activityInterval) { + clearInterval(activityInterval); + } + + client.destroy(); + process.exit(0); +}); + +// Login to Discord +client.login(config.discord.token); diff --git a/src/services/commandLoader.js b/src/services/commandLoader.js new file mode 100644 index 0000000..0957bca --- /dev/null +++ b/src/services/commandLoader.js @@ -0,0 +1,166 @@ +const fs = require('fs'); +const path = require('path'); + +class CommandLoader { + constructor() { + this.staticCommands = {}; + this.ankhbotCommands = {}; + + // Paths to command files + this.staticCommandsPath = path.join(__dirname, '..', '..', 'conf', 'text_commands'); + this.ankhbotCommandsPath = path.join(__dirname, '..', '..', 'conf', 'ghbot.abcomg'); + + // Load commands initially + this.loadStaticCommands(); + this.loadAnkhbotCommands(); + + // Watch for changes + this.watchFiles(); + } + + /** + * Load static text commands from file + */ + loadStaticCommands() { + try { + if (!fs.existsSync(this.staticCommandsPath)) { + console.log('Static commands file not found, skipping...'); + return; + } + + const data = fs.readFileSync(this.staticCommandsPath, 'utf-8'); + const lines = data.toString().split('\n'); + const commands = {}; + + lines.forEach(line => { + if (line.length > 0 && line.indexOf('|') !== -1) { + const parts = line.split('|'); + // Check for aliases (comma-separated) + const aliases = parts[0].split(','); + aliases.forEach(cmd => { + commands[cmd.trim()] = parts[1]; + }); + } + }); + + this.staticCommands = commands; + console.log(`Loaded ${Object.keys(commands).length} static commands`); + } catch (error) { + console.error('Error loading static commands:', error); + } + } + + /** + * Load Ankhbot commands from file + */ + loadAnkhbotCommands() { + try { + if (!fs.existsSync(this.ankhbotCommandsPath)) { + console.log('Ankhbot commands file not found, skipping...'); + return; + } + + const data = fs.readFileSync(this.ankhbotCommandsPath, 'utf-8'); + + // Try to parse as JSON first, fall back to eval if needed (for legacy format) + let commands; + try { + commands = JSON.parse(data); + } catch { + // Legacy format might use JavaScript object notation + // Create a safer evaluation context + const sandbox = { commands: null }; + const script = `commands = ${data}`; + try { + // Use Function constructor for safer eval + new Function('commands', script).call(sandbox, sandbox); + commands = sandbox.commands; + } catch (e) { + console.error('Failed to parse Ankhbot commands:', e); + return; + } + } + + // Convert to a map for easier lookup + const commandMap = {}; + if (Array.isArray(commands)) { + commands.forEach(cmd => { + if (cmd.Enabled === true && cmd.Command && cmd.Response) { + // Remove prefix from command name for storage + const cmdName = cmd.Command.startsWith('!') ? + cmd.Command.substring(1) : cmd.Command; + commandMap[cmdName] = cmd.Response; + } + }); + } + + this.ankhbotCommands = commandMap; + console.log(`Loaded ${Object.keys(commandMap).length} Ankhbot commands`); + } catch (error) { + console.error('Error loading Ankhbot commands:', error); + } + } + + /** + * Watch command files for changes + */ + watchFiles() { + // Watch static commands file + if (fs.existsSync(this.staticCommandsPath)) { + fs.watchFile(this.staticCommandsPath, (curr, prev) => { + if (curr.mtime !== prev.mtime) { + console.log('Static commands file changed, reloading...'); + this.loadStaticCommands(); + } + }); + } + + // Watch Ankhbot commands file + if (fs.existsSync(this.ankhbotCommandsPath)) { + fs.watchFile(this.ankhbotCommandsPath, (curr, prev) => { + if (curr.mtime !== prev.mtime) { + console.log('Ankhbot commands file changed, reloading...'); + this.loadAnkhbotCommands(); + } + }); + } + } + + /** + * Check if a static command exists + * @param {string} command + * @returns {boolean} + */ + hasStaticCommand(command) { + return this.staticCommands.hasOwnProperty(command); + } + + /** + * Get a static command response + * @param {string} command + * @returns {string|undefined} + */ + getStaticCommand(command) { + return this.staticCommands[command]; + } + + /** + * Check if an Ankhbot command exists + * @param {string} command + * @returns {boolean} + */ + hasAnkhbotCommand(command) { + return this.ankhbotCommands.hasOwnProperty(command); + } + + /** + * Get an Ankhbot command response + * @param {string} command + * @returns {string|undefined} + */ + getAnkhbotCommand(command) { + return this.ankhbotCommands[command]; + } +} + +module.exports = new CommandLoader(); \ No newline at end of file diff --git a/src/services/schedulerService.js b/src/services/schedulerService.js new file mode 100644 index 0000000..4f78058 --- /dev/null +++ b/src/services/schedulerService.js @@ -0,0 +1,157 @@ +const schedule = require('node-schedule'); + +class SchedulerService { + constructor() { + this.jobs = new Map(); + } + + /** + * Initialize scheduled events for all guilds + * @param {Client} client + * @param {Object} config + */ + async initialize(client, config) { + console.log('Initializing scheduled events...'); + + for (const guildConfig of config.discord.guilds) { + try { + const guild = await client.guilds.fetch(guildConfig.id); + if (!guild) { + console.error(`Could not find guild ${guildConfig.id}`); + continue; + } + + if (!guildConfig.scheduledEvents || guildConfig.scheduledEvents.length === 0) { + continue; + } + + for (const event of guildConfig.scheduledEvents) { + await this.scheduleEvent(guild, event, guildConfig); + } + } catch (error) { + console.error(`Error setting up scheduled events for guild ${guildConfig.id}:`, error); + } + } + } + + /** + * Schedule a single event + * @param {Guild} guild + * @param {Object} event + * @param {Object} guildConfig + */ + async scheduleEvent(guild, event, guildConfig) { + try { + // Validate channel + let channel = null; + if (event.channelId) { + channel = await guild.channels.fetch(event.channelId); + if (!channel) { + console.error(`Invalid channel ${event.channelId} for event ${event.id} in guild ${guild.name}`); + return; + } + } + + // Validate role + let pingRole = null; + if (event.pingRoleId) { + pingRole = await guild.roles.fetch(event.pingRoleId); + if (!pingRole) { + console.warn(`Invalid role ${event.pingRoleId} for event ${event.id} in guild ${guild.name}`); + } + } + + console.log(`Scheduling event ${event.id} for ${guild.name}...`); + + // Create the scheduled job + const job = schedule.scheduleJob(event.schedule, () => { + this.executeEvent(channel, event, pingRole); + }); + + if (job) { + // Store job reference + const jobKey = `${guild.id}-${event.id}`; + this.jobs.set(jobKey, job); + + console.log(`Event ${event.id} scheduled. Next invocation: ${job.nextInvocation()}`); + } else { + console.error(`Failed to schedule event ${event.id} - invalid cron expression: ${event.schedule}`); + } + } catch (error) { + console.error(`Error scheduling event ${event.id}:`, error); + } + } + + /** + * Execute a scheduled event + * @param {TextChannel} channel + * @param {Object} event + * @param {Role} pingRole + */ + async executeEvent(channel, event, pingRole) { + try { + const content = []; + + // Add role ping if configured + if (pingRole) { + content.push(pingRole.toString()); + } + + // Add message if configured + if (event.message) { + content.push(event.message); + } + + // Send the message + if (content.length > 0 && channel) { + await channel.send(content.join(' ')); + console.log(`Executed scheduled event ${event.id}`); + } + } catch (error) { + console.error(`Error executing scheduled event ${event.id}:`, error); + } + } + + /** + * Cancel a scheduled job + * @param {string} guildId + * @param {string} eventId + */ + cancelJob(guildId, eventId) { + const jobKey = `${guildId}-${eventId}`; + const job = this.jobs.get(jobKey); + + if (job) { + job.cancel(); + this.jobs.delete(jobKey); + console.log(`Cancelled scheduled event ${eventId} for guild ${guildId}`); + } + } + + /** + * Cancel all jobs for a guild + * @param {string} guildId + */ + cancelGuildJobs(guildId) { + for (const [key, job] of this.jobs) { + if (key.startsWith(`${guildId}-`)) { + job.cancel(); + this.jobs.delete(key); + } + } + console.log(`Cancelled all scheduled events for guild ${guildId}`); + } + + /** + * Cancel all jobs + */ + cancelAllJobs() { + for (const job of this.jobs.values()) { + job.cancel(); + } + this.jobs.clear(); + console.log('Cancelled all scheduled events'); + } +} + +module.exports = new SchedulerService(); \ No newline at end of file diff --git a/src/services/sfxManager.js b/src/services/sfxManager.js new file mode 100644 index 0000000..f2cc462 --- /dev/null +++ b/src/services/sfxManager.js @@ -0,0 +1,113 @@ +const fs = require('fs'); +const path = require('path'); + +class SFXManager { + constructor() { + this.sfxPath = path.join(__dirname, '..', '..', 'sfx'); + this.sfxList = []; + + // Load SFX list initially + this.loadSFXList(); + + // Watch for changes + this.watchSFXDirectory(); + } + + /** + * Load the list of available SFX files + */ + loadSFXList() { + try { + if (!fs.existsSync(this.sfxPath)) { + console.log('SFX directory not found, creating...'); + fs.mkdirSync(this.sfxPath, { recursive: true }); + } + + const files = fs.readdirSync(this.sfxPath); + this.sfxList = files + .filter(file => file.endsWith('.mp3') || file.endsWith('.wav')) + .map(file => { + const ext = path.extname(file); + return { + name: file.replace(ext, ''), + filename: file, + path: path.join(this.sfxPath, file) + }; + }); + + console.log(`Loaded ${this.sfxList.length} sound effects`); + } catch (error) { + console.error('Error loading SFX list:', error); + } + } + + /** + * Watch the SFX directory for changes + */ + watchSFXDirectory() { + fs.watch(this.sfxPath, (eventType, filename) => { + if (eventType === 'rename') { + console.log('SFX directory changed, reloading...'); + this.loadSFXList(); + } + }); + } + + /** + * Get all available SFX + * @returns {Array} List of SFX objects + */ + getAllSFX() { + return this.sfxList; + } + + /** + * Get SFX names for autocomplete + * @returns {Array} List of SFX names + */ + getSFXNames() { + return this.sfxList.map(sfx => sfx.name); + } + + /** + * Find an SFX by name + * @param {string} name + * @returns {Object|undefined} SFX object or undefined + */ + findSFX(name) { + return this.sfxList.find(sfx => sfx.name.toLowerCase() === name.toLowerCase()); + } + + /** + * Check if an SFX exists + * @param {string} name + * @returns {boolean} + */ + hasSFX(name) { + return this.findSFX(name) !== undefined; + } + + /** + * Get the file path for an SFX + * @param {string} name + * @returns {string|null} + */ + getSFXPath(name) { + const sfx = this.findSFX(name); + return sfx ? sfx.path : null; + } + + /** + * Search SFX names (for autocomplete) + * @param {string} query + * @returns {Array} Matching SFX names + */ + searchSFX(query) { + const lowerQuery = query.toLowerCase(); + return this.sfxList + .filter(sfx => sfx.name.toLowerCase().includes(lowerQuery)) + .map(sfx => sfx.name); + } +} + +module.exports = new SFXManager(); \ No newline at end of file diff --git a/src/services/voiceService.js b/src/services/voiceService.js new file mode 100644 index 0000000..f53ec43 --- /dev/null +++ b/src/services/voiceService.js @@ -0,0 +1,171 @@ +const { + createAudioPlayer, + createAudioResource, + joinVoiceChannel, + VoiceConnectionStatus, + AudioPlayerStatus, + entersState, + getVoiceConnection, + generateDependencyReport +} = require('@discordjs/voice'); +const { ChannelType } = require('discord.js'); + +// Try to use ffmpeg-static as fallback if system ffmpeg is not available +try { + const ffmpegPath = require('ffmpeg-static'); + if (ffmpegPath && !process.env.FFMPEG_PATH) { + process.env.FFMPEG_PATH = ffmpegPath; + } +} catch (error) { + // ffmpeg-static not available, rely on system ffmpeg +} + +class VoiceService { + constructor() { + this.connections = new Map(); + this.players = new Map(); + } + + /** + * Join a voice channel + * @param {VoiceChannel} channel + * @returns {VoiceConnection} + */ + async join(channel) { + if (!channel || channel.type !== ChannelType.GuildVoice) { + throw new Error('Invalid voice channel'); + } + + // Check if already connected + let connection = getVoiceConnection(channel.guild.id); + + if (!connection) { + connection = joinVoiceChannel({ + channelId: channel.id, + guildId: channel.guild.id, + adapterCreator: channel.guild.voiceAdapterCreator, + }); + + // Wait for connection to be ready + try { + await entersState(connection, VoiceConnectionStatus.Ready, 10_000); + } catch (error) { + connection.destroy(); + throw error; + } + + // Store connection + this.connections.set(channel.guild.id, connection); + + // Handle disconnection + connection.on(VoiceConnectionStatus.Disconnected, async () => { + try { + // Try to reconnect + await Promise.race([ + entersState(connection, VoiceConnectionStatus.Signalling, 5_000), + entersState(connection, VoiceConnectionStatus.Connecting, 5_000), + ]); + } catch (error) { + // Seems to be a real disconnect, destroy the connection + connection.destroy(); + this.connections.delete(channel.guild.id); + this.players.delete(channel.guild.id); + } + }); + } + + return connection; + } + + /** + * Leave a voice channel + * @param {string} guildId + */ + leave(guildId) { + const connection = this.connections.get(guildId); + if (connection) { + connection.destroy(); + this.connections.delete(guildId); + this.players.delete(guildId); + } + } + + /** + * Play an audio file + * @param {string} guildId + * @param {string} filePath + * @param {Object} options + * @returns {AudioPlayer} + */ + async play(guildId, filePath, options = {}) { + const connection = this.connections.get(guildId); + if (!connection) { + throw new Error('Not connected to voice channel'); + } + + // Create or get player for this guild + let player = this.players.get(guildId); + if (!player) { + player = createAudioPlayer(); + this.players.set(guildId, player); + } + + // Create audio resource with options + const resource = createAudioResource(filePath, { + inlineVolume: options.volume !== undefined, + }); + + if (options.volume !== undefined && resource.volume) { + resource.volume.setVolume(options.volume); + } + + // Subscribe the connection to the player + connection.subscribe(player); + + // Play the resource + player.play(resource); + + // Return a promise that resolves when playback finishes + return new Promise((resolve, reject) => { + player.once(AudioPlayerStatus.Idle, () => { + resolve(); + }); + + player.once('error', (error) => { + console.error('Player error:', error); + reject(error); + }); + }); + } + + /** + * Stop playing audio + * @param {string} guildId + */ + stop(guildId) { + const player = this.players.get(guildId); + if (player) { + player.stop(); + } + } + + /** + * Check if connected to a voice channel + * @param {string} guildId + * @returns {boolean} + */ + isConnected(guildId) { + return this.connections.has(guildId); + } + + /** + * Get the current voice connection + * @param {string} guildId + * @returns {VoiceConnection|undefined} + */ + getConnection(guildId) { + return this.connections.get(guildId); + } +} + +module.exports = new VoiceService(); \ No newline at end of file diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..a53fa62 --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,70 @@ +/** + * Pick a random element from an array + * @param {Array} arr + * @returns {*} Random element from the array + */ +function randElement(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +/** + * Random sort function + * @returns {number} + */ +function randSort() { + return 0.5 - Math.random(); +} + +/** + * Split a string into chunks of specified size + * @param {string} str + * @param {number} size + * @returns {string[]} + */ +function chunkSubstr(str, size) { + const numChunks = Math.ceil(str.length / size); + const chunks = new Array(numChunks); + + for (let i = 0, o = 0; i < numChunks; ++i, o += size) { + chunks[i] = str.substr(o, size); + } + + return chunks; +} + +/** + * Async forEach implementation + * @param {Array} array + * @param {Function} callback + */ +async function asyncForEach(array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +} + +/** + * Converts seconds to human-readable time + * @param {number} seconds + * @returns {string} HH:MM:SS format + */ +function toHHMMSS(seconds) { + const sec_num = parseInt(seconds, 10); + let hours = Math.floor(sec_num / 3600); + let minutes = Math.floor((sec_num - hours * 3600) / 60); + let secs = sec_num - hours * 3600 - minutes * 60; + + if (hours < 10) hours = "0" + hours; + if (minutes < 10) minutes = "0" + minutes; + if (secs < 10) secs = "0" + secs; + + return hours + ":" + minutes + ":" + secs; +} + +module.exports = { + randElement, + randSort, + chunkSubstr, + asyncForEach, + toHHMMSS +}; \ No newline at end of file