diff options
| author | Aleksa Vuckovic <aleksa@vuckovic.cc> | 2025-11-11 14:14:48 +0100 |
|---|---|---|
| committer | Your Name <you@example.com> | 2025-11-13 19:07:45 +0100 |
| commit | f10e48a8d8d0cdca589c9d73791b9a46e896425d (patch) | |
| tree | 97055c143dffa352cec1a3e122a9061584a1e324 | |
Initial commit
| -rw-r--r-- | .env.example | 3 | ||||
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | index.js | 1 | ||||
| -rw-r--r-- | package-lock.json | 1125 | ||||
| -rw-r--r-- | package.json | 21 | ||||
| -rw-r--r-- | pajser-bot.service | 19 | ||||
| -rw-r--r-- | src/bot.js | 113 | ||||
| -rw-r--r-- | src/commands/definitions.js | 89 | ||||
| -rw-r--r-- | src/handlers/controlCommands.js | 97 | ||||
| -rw-r--r-- | src/handlers/playCommand.js | 271 | ||||
| -rw-r--r-- | src/handlers/queueCommand.js | 38 | ||||
| -rw-r--r-- | src/handlers/seekCommand.js | 51 | ||||
| -rw-r--r-- | src/handlers/volumeCommand.js | 28 | ||||
| -rw-r--r-- | src/utils/commandRegistry.js | 16 | ||||
| -rw-r--r-- | src/utils/helpers.js | 24 | ||||
| -rw-r--r-- | src/utils/player.js | 516 |
16 files changed, 2416 insertions, 0 deletions
diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6ccac30 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DISCORD_TOKEN=your_bot_token_here +CLIENT_ID=your_client_id_here +COOKIES_FILE=/path/to/cookies.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..728ef1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +*.log +cookies.txt diff --git a/index.js b/index.js new file mode 100644 index 0000000..7f64e9b --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +require('./src/bot'); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8c0896d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1125 @@ +{ + "name": "pajser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pajser", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@discordjs/opus": "^0.10.0", + "@discordjs/voice": "^0.18.0", + "discord.js": "^14.24.2", + "dotenv": "^17.2.3", + "opusscript": "^0.0.8", + "sodium-native": "^5.0.9" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/formatters/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/node-pre-gyp": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@discordjs/node-pre-gyp/-/node-pre-gyp-0.4.5.tgz", + "integrity": "sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@discordjs/opus": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/opus/-/opus-0.10.0.tgz", + "integrity": "sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discordjs/node-pre-gyp": "^0.4.5", + "node-addon-api": "^8.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/voice": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz", + "integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==", + "license": "Apache-2.0", + "dependencies": { + "@types/ws": "^8.5.12", + "discord-api-types": "^0.37.103", + "prism-media": "^1.3.5", + "tslib": "^2.6.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-addon-resolve": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.6.tgz", + "integrity": "sha512-hvOQY1zDK6u0rSr27T6QlULoVLwi8J2k8HHHJlxSfT7XQdQ/7bsS+AnjYkHtu/TkL+gm3aMXAKucJkJAbrDG/g==", + "license": "Apache-2.0", + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.0.tgz", + "integrity": "sha512-JrzrqlC3Tds0iKRwQs8xIIJ+FRieKA9ll0jaqpotDLZtjJPVevzRoeuUYZ5GIo1t1z7/pIRdk85Q3i/2xQLfEQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-semver": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", + "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.120", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", + "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opusscript": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz", + "integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==", + "license": "MIT", + "peer": true + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prism-media": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", + "integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==", + "license": "Apache-2.0", + "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 + } + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-addon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", + "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", + "license": "Apache-2.0", + "dependencies": { + "bare-addon-resolve": "^1.3.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sodium-native": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.9.tgz", + "integrity": "sha512-6fpu3d6zdrRpLhuV3CDIBO5g90KkgaeR+c3xvDDz0ZnDkAlqbbPhFW7zhMJfsskfZ9SuC3SvBbqvxcECkXRyKw==", + "license": "MIT", + "dependencies": { + "require-addon": "^1.1.0", + "which-runtime": "^1.2.1" + }, + "engines": { + "bare": ">=1.16.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/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==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-runtime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz", + "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", + "license": "Apache-2.0" + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "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 + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..16f5cba --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "pajser", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@discordjs/opus": "^0.10.0", + "@discordjs/voice": "^0.18.0", + "discord.js": "^14.24.2", + "dotenv": "^17.2.3", + "opusscript": "^0.0.8", + "sodium-native": "^5.0.9" + } +} diff --git a/pajser-bot.service b/pajser-bot.service new file mode 100644 index 0000000..fd6d1ae --- /dev/null +++ b/pajser-bot.service @@ -0,0 +1,19 @@ +[Unit] +Description=Discord Music Bot (Pajser) +After=network.target + +[Service] +Type=simple +User=pajser +WorkingDirectory=/home/pajser/pajser +ExecStart=/usr/bin/node index.js +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=pajser-bot + +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..cd7c2db --- /dev/null +++ b/src/bot.js @@ -0,0 +1,113 @@ +const { Client, GatewayIntentBits } = require('discord.js'); +require('dotenv').config(); + +const commands = require('./commands/definitions'); +const { registerCommands } = require('./utils/commandRegistry'); +const { handlePlay } = require('./handlers/playCommand'); +const { handlePause, handleResume, handleSkip, handleClear, handleLoop, handleQuit } = require('./handlers/controlCommands'); +const { handleQueue } = require('./handlers/queueCommand'); +const { handleVolume } = require('./handlers/volumeCommand'); +const { handleSeek } = require('./handlers/seekCommand'); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildVoiceStates, + ], +}); + +const queues = new Map(); + +client.once('ready', async () => { + console.log(`Bot ready: ${client.user.tag}`); + console.log(`Ready at: ${new Date().toISOString()}`); + await registerCommands(commands, process.env.DISCORD_TOKEN, process.env.CLIENT_ID); + + setInterval(() => { + const activeQueues = queues.size; + console.log(`[HEARTBEAT] ${new Date().toISOString()} - Active queues: ${activeQueues}`); + }, 5 * 60 * 1000); +}); + +function handleError(error, context = 'Unknown') { + const timestamp = new Date().toISOString(); + console.error(`[ERROR] ${timestamp} - Context: ${context}`); + console.error(error); + + if (error.stack) { + console.error('Stack trace:', error.stack); + } +} + +async function handleInteractionError(interaction, error, commandName) { + handleError(error, `Command: ${commandName}`); + + try { + const reply = interaction.deferred || interaction.replied ? 'editReply' : 'reply'; + await interaction[reply]({ + content: 'An error occurred while executing this command!', + ephemeral: true + }).catch(() => {}); + } catch (replyError) { + console.error('Failed to send error message to user:', replyError); + } +} + +client.on('interactionCreate', async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + try { + switch (interaction.commandName) { + case 'play': + await handlePlay(interaction, queues); + break; + case 'pause': + handlePause(interaction, queues); + break; + case 'resume': + handleResume(interaction, queues); + break; + case 'skip': + handleSkip(interaction, queues); + break; + case 'clear': + handleClear(interaction, queues); + break; + case 'loop': + handleLoop(interaction, queues); + break; + case 'quit': + handleQuit(interaction, queues); + break; + case 'queue': + handleQueue(interaction, queues); + break; + case 'volume': + handleVolume(interaction, queues); + break; + case 'seek': + handleSeek(interaction, queues); + break; + } + } catch (error) { + await handleInteractionError(interaction, error, interaction.commandName); + } +}); + +process.on('unhandledRejection', (error) => { + handleError(error, 'Unhandled Promise Rejection'); +}); + +process.on('uncaughtException', (error) => { + handleError(error, 'Uncaught Exception'); +}); + +client.on('error', (error) => { + handleError(error, 'Discord Client Error'); +}); + +client.on('warn', (info) => { + console.warn(`[WARN] ${new Date().toISOString()} - ${info}`); +}); + +client.login(process.env.DISCORD_TOKEN);
\ No newline at end of file diff --git a/src/commands/definitions.js b/src/commands/definitions.js new file mode 100644 index 0000000..626fcc6 --- /dev/null +++ b/src/commands/definitions.js @@ -0,0 +1,89 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = [ + new SlashCommandBuilder() + .setName('play') + .setDescription('Play music from YouTube') + .addStringOption(option => + option.setName('query') + .setDescription('YouTube URL or search query') + .setRequired(true) + ), + new SlashCommandBuilder() + .setName('pause') + .setDescription('Pause the current song'), + new SlashCommandBuilder() + .setName('resume') + .setDescription('Resume the paused song'), + new SlashCommandBuilder() + .setName('skip') + .setDescription('Skip the current song'), + new SlashCommandBuilder() + .setName('clear') + .setDescription('Clear the entire queue'), + new SlashCommandBuilder() + .setName('loop') + .setDescription('Toggle loop mode') + .addStringOption(option => + option.setName('mode') + .setDescription('Loop mode') + .setRequired(false) + .addChoices( + { name: 'Off', value: 'off' }, + { name: 'Song', value: 'song' }, + { name: 'Queue', value: 'queue' } + ) + ), + new SlashCommandBuilder() + .setName('quit') + .setDescription('Leave the voice channel'), + new SlashCommandBuilder() + .setName('queue') + .setDescription('Show the music queue'), + new SlashCommandBuilder() + .setName('volume') + .setDescription('Adjust volume') + .addSubcommand(subcommand => + subcommand + .setName('set') + .setDescription('Set volume to a specific value') + .addIntegerOption(option => + option.setName('value') + .setDescription('Volume (0-100)') + .setRequired(true) + .setMinValue(0) + .setMaxValue(100) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('inc') + .setDescription('Increase volume') + .addIntegerOption(option => + option.setName('amount') + .setDescription('Amount to increase (default: 10)') + .setMinValue(1) + .setMaxValue(100) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('dec') + .setDescription('Decrease volume') + .addIntegerOption(option => + option.setName('amount') + .setDescription('Amount to decrease (default: 10)') + .setMinValue(1) + .setMaxValue(100) + ) + ), + new SlashCommandBuilder() + .setName('seek') + .setDescription('Skip forward or backward in the current song') + .addIntegerOption(option => + option.setName('seconds') + .setDescription('Seconds to skip (use negative to go back)') + .setRequired(true) + ), + +]; diff --git a/src/handlers/controlCommands.js b/src/handlers/controlCommands.js new file mode 100644 index 0000000..14c73f7 --- /dev/null +++ b/src/handlers/controlCommands.js @@ -0,0 +1,97 @@ +const { playSong, safeCleanup, clearSongBuffer } = require('../utils/player'); +const { getQueueOrReply } = require('../utils/helpers'); + +function handlePause(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Nothing is playing!'); + if (!queue) return; + + queue.player.pause(); + interaction.reply('Paused!'); +} + +function handleResume(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Nothing is paused!'); + if (!queue) return; + + queue.player.unpause(); + interaction.reply('Resumed!'); +} + +function handleSkip(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Nothing to skip!'); + if (!queue || queue.songs.length === 0) { + if (queue) interaction.reply('Nothing to skip!'); + return; + } + + const playerState = queue.player.state.status; + console.log(`[SKIP] Guild: ${interaction.guild.id}, Skipping: ${queue.songs[0]?.title}, Player state: ${playerState}, Queue length: ${queue.songs.length}`); + + clearSongBuffer(queue.songs[0], 'skipped'); + safeCleanup(queue, 'Skip'); + + queue.player.stop(); + interaction.reply(`Skipped! (${queue.songs.length - 1} songs remaining)`); +} + +function handleClear(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Queue is already empty!'); + if (!queue || queue.songs.length === 0) { + if (queue) interaction.reply('Queue is already empty!'); + return; + } + + const currentSong = queue.songs[0]; + const clearedCount = queue.songs.length - 1; + + for (let i = 1; i < queue.songs.length; i++) { + clearSongBuffer(queue.songs[i], 'queue cleared'); + } + + queue.songs = [currentSong]; + console.log(`[QUEUE] Cleared ${clearedCount} songs from queue`); + interaction.reply(`Cleared queue! Only current song remains: ${currentSong.title}`); +} + +function handleLoop(interaction, queues) { + const queue = getQueueOrReply(interaction, queues); + if (!queue) return; + + const mode = interaction.options.getString('mode'); + + if (mode) { + queue.loopMode = mode; + } else { + const modes = ['off', 'song', 'queue']; + const currentIndex = modes.indexOf(queue.loopMode || 'off'); + queue.loopMode = modes[(currentIndex + 1) % modes.length]; + } + + const modeEmojis = { + off: 'Loop disabled', + song: 'Looping current song', + queue: 'Looping queue' + }; + + interaction.reply(modeEmojis[queue.loopMode]); +} + +function handleQuit(interaction, queues) { + const queue = getQueueOrReply(interaction, queues); + if (!queue) return; + + try { queue.player.stop(); } catch (e) {} + safeCleanup(queue, 'Quit'); + queue.connection.destroy(); + queues.delete(interaction.guild.id); + interaction.reply('Left voice channel!'); +} + +module.exports = { + handlePause, + handleResume, + handleSkip, + handleClear, + handleLoop, + handleQuit, +}; diff --git a/src/handlers/playCommand.js b/src/handlers/playCommand.js new file mode 100644 index 0000000..5551d28 --- /dev/null +++ b/src/handlers/playCommand.js @@ -0,0 +1,271 @@ +const { EmbedBuilder } = require('discord.js'); +const { + createAudioPlayer, + joinVoiceChannel, + AudioPlayerStatus, + VoiceConnectionStatus, + entersState, +} = require('@discordjs/voice'); +const { getVideoInfo, playSong, preloadSong, formatDuration, safeCleanup, clearSongBuffer } = require('../utils/player'); +const { requireVoiceChannel } = require('../utils/helpers'); + +function createSongsFromVideos(videos, requestedBy) { + return videos.map(v => ({ + title: v.title, + url: v.url, + duration: v.duration, + thumbnail: v.thumbnail, + requestedBy: requestedBy, + })); +} + +function onPlayerIdle(interaction, queue) { + console.log(`[PLAYER IDLE] Guild: ${interaction.guild.id}, Loop mode: ${queue.loopMode}, Is seeking: ${queue.isSeeking || false}`); + + safeCleanup(queue, 'Player Idle'); + + if (queue.isSeeking) { + console.log(`[IDLE SKIP] Skipping queue shift because seek is in progress`); + return; + } + + if (queue.loopMode === 'song') { + if (queue.songs[0]) { + queue.songs[0].retryCount = 0; + } + playSong(interaction.guild.id, queue); + } else if (queue.loopMode === 'queue') { + const finishedSong = queue.songs.shift(); + queue.songs.push(finishedSong); + if (queue.songs.length > 0) { + playSong(interaction.guild.id, queue); + } + } else { + const finishedSong = queue.songs.shift(); + clearSongBuffer(finishedSong, 'finished playing'); + if (queue.songs.length > 0) { + playSong(interaction.guild.id, queue); + } else { + console.log(`[QUEUE EMPTY] Guild: ${interaction.guild.id}`); + } + } +} + +function onPlayerError(error, interaction, queue) { + console.error('Audio player error:', error); + safeCleanup(queue, 'Player Error'); + + if (queue.isSeeking) { + console.log(`[ERROR DURING SEEK] Not shifting queue, seek in progress`); + queue.isSeeking = false; + return; + } + + queue.songs.shift(); + if (queue.songs.length > 0) { + playSong(interaction.guild.id, queue); + } +} + +function onPlayerAutoPaused(player) { + console.warn('Player auto-paused, attempting to resume...'); + try { + player.unpause(); + } catch (err) { + console.error('Failed to unpause:', err); + } +} + +function onConnectionError(error, queue) { + console.error('Voice connection error:', error); + safeCleanup(queue, 'Connection Error'); +} + +async function onConnectionDisconnected(connection, queues, guildId) { + try { + await Promise.race([ + entersState(connection, VoiceConnectionStatus.Signalling, 5000), + entersState(connection, VoiceConnectionStatus.Connecting, 5000), + ]); + } catch (error) { + connection.destroy(); + queues.delete(guildId); + } +} + +function onConnectionDestroyed(queues, guildId) { + queues.delete(guildId); +} + +function setupPlayerEventListeners(player, interaction, queue) { + player.on(AudioPlayerStatus.Idle, () => onPlayerIdle(interaction, queue)); + player.on('error', (error) => onPlayerError(error, interaction, queue)); + player.on(AudioPlayerStatus.AutoPaused, () => onPlayerAutoPaused(player)); +} + +function setupConnectionEventListeners(connection, queues, guildId, queue) { + connection.on('error', (error) => onConnectionError(error, queue)); + connection.on(VoiceConnectionStatus.Disconnected, () => + onConnectionDisconnected(connection, queues, guildId) + ); + connection.on(VoiceConnectionStatus.Destroyed, () => + onConnectionDestroyed(queues, guildId) + ); +} + +async function createVoiceConnection(voiceChannel, interaction) { + const connection = joinVoiceChannel({ + channelId: voiceChannel.id, + guildId: interaction.guild.id, + adapterCreator: voiceChannel.guild.voiceAdapterCreator, + }); + + try { + await entersState(connection, VoiceConnectionStatus.Ready, 30000); + return connection; + } catch (error) { + console.error('Failed to join voice channel:', error); + connection.destroy(); + throw error; + } +} + +async function initializeQueue(voiceChannel, interaction, queues, songs) { + const player = createAudioPlayer(); + let connection; + + try { + connection = await createVoiceConnection(voiceChannel, interaction); + } catch (error) { + return null; + } + + connection.subscribe(player); + + const queue = { + voiceChannel, + connection, + player, + songs: songs, + volume: 50, + isPlaying: false, + loopMode: 'off', + }; + + queues.set(interaction.guild.id, queue); + + setupPlayerEventListeners(player, interaction, queue); + setupConnectionEventListeners(connection, queues, interaction.guild.id, queue); + + return queue; +} + +function addSongsToQueue(queue, songs, interaction) { + const oldLength = queue.songs.length; + const wasIdle = queue.player.state.status === AudioPlayerStatus.Idle; + + songs.forEach(song => queue.songs.push(song)); + const newLength = queue.songs.length; + + console.log(`[QUEUE ADD] Guild: ${interaction.guild.id}, Added ${songs.length} songs, Queue: ${oldLength} -> ${newLength}, Player state: ${queue.player.state.status}, Was idle: ${wasIdle}`); + + if (wasIdle && oldLength === 0 && queue.songs.length > 0) { + if (queue.isSeeking) { + console.log(`[AUTO-PLAY BLOCKED] Seek in progress, not starting playback`); + } else { + console.log(`[AUTO-PLAY] Starting playback, songs in queue: ${queue.songs.length}`); + playSong(interaction.guild.id, queue); + } + } + + if (queue.songs.length >= 2) { + const nextSong = queue.songs[1]; + if (!nextSong.audioBuffer && !nextSong.isPreloading) { + console.log(`[PRELOAD TRIGGER] Preloading next song: ${nextSong.title}`); + preloadSong(nextSong).catch(() => {}); + } + } + + return wasIdle; +} + +function createNewPlaybackEmbed(firstSong, isPlaylist, songs) { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle(isPlaylist ? 'Playlist Added' : 'Now Playing') + .setDescription(`**${firstSong.title}**`) + .addFields( + { name: 'Duration', value: formatDuration(firstSong.duration), inline: true }, + { name: 'Requested by', value: firstSong.requestedBy, inline: true } + ) + .setThumbnail(firstSong.thumbnail); + + if (isPlaylist) { + embed.addFields({ name: 'Playlist', value: `${songs.length} songs added to queue` }); + } + + return embed; +} + +function createAddToQueueEmbed(firstSong, isPlaylist, songs, queuePosition) { + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle(isPlaylist ? 'Playlist Added to Queue' : 'Added to Queue') + .setDescription(`**${firstSong.title}**`) + .addFields( + { name: 'Position', value: `${queuePosition}`, inline: true }, + { name: 'Requested by', value: firstSong.requestedBy, inline: true } + ) + .setThumbnail(firstSong.thumbnail); + + if (isPlaylist) { + embed.addFields({ name: 'Playlist', value: `${songs.length} songs added to queue` }); + } + + return embed; +} + +async function handlePlay(interaction, queues) { + await interaction.deferReply(); + + const query = interaction.options.getString('query'); + const voiceChannel = requireVoiceChannel(interaction); + if (!voiceChannel) return; + + const videoInfo = await getVideoInfo(query); + if (!videoInfo) { + return interaction.editReply('Could not find that video!'); + } + + const isPlaylist = videoInfo.isPlaylist; + const videos = videoInfo.videos; + + if (!videos || videos.length === 0) { + return interaction.editReply('Could not find any videos!'); + } + + const songs = createSongsFromVideos(videos, interaction.user.tag); + const firstSong = songs[0]; + + let queue = queues.get(interaction.guild.id); + + if (!queue) { + queue = await initializeQueue(voiceChannel, interaction, queues, songs); + + if (!queue) { + return interaction.editReply('Failed to join voice channel! Try again.'); + } + + const embed = createNewPlaybackEmbed(firstSong, isPlaylist, songs); + interaction.editReply({ embeds: [embed] }); + playSong(interaction.guild.id, queue); + } else { + const queuePosition = queue.songs.length - songs.length + 1; + addSongsToQueue(queue, songs, interaction); + + const embed = createAddToQueueEmbed(firstSong, isPlaylist, songs, queuePosition); + interaction.editReply({ embeds: [embed] }); + } +} + +module.exports = { handlePlay }; diff --git a/src/handlers/queueCommand.js b/src/handlers/queueCommand.js new file mode 100644 index 0000000..e0fbee1 --- /dev/null +++ b/src/handlers/queueCommand.js @@ -0,0 +1,38 @@ +const { EmbedBuilder } = require('discord.js'); +const { formatDuration } = require('../utils/player'); +const { getQueueOrReply } = require('../utils/helpers'); + +function handleQueue(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Queue is empty!'); + if (!queue || queue.songs.length === 0) { + if (queue) interaction.reply('Queue is empty!'); + return; + } + + const nowPlaying = queue.songs[0]; + const upcoming = queue.songs.slice(1, 10); + + const embed = new EmbedBuilder() + .setColor(0x0099ff) + .setTitle('Queue') + .setDescription( + `**Now Playing:**\n${nowPlaying.title}\n` + + `Duration: ${formatDuration(nowPlaying.duration)} | Requested by: ${nowPlaying.requestedBy}` + ) + .setThumbnail(nowPlaying.thumbnail); + + if (upcoming.length > 0) { + const upcomingText = upcoming + .map((song, i) => `${i + 1}. ${song.title} - ${formatDuration(song.duration)}`) + .join('\n'); + embed.addFields({ name: 'Up Next', value: upcomingText }); + } + + if (queue.songs.length > 10) { + embed.setFooter({ text: `...and ${queue.songs.length - 10} more` }); + } + + interaction.reply({ embeds: [embed] }); +} + +module.exports = { handleQueue }; diff --git a/src/handlers/seekCommand.js b/src/handlers/seekCommand.js new file mode 100644 index 0000000..e48f603 --- /dev/null +++ b/src/handlers/seekCommand.js @@ -0,0 +1,51 @@ +const { EmbedBuilder } = require('discord.js'); +const { playSong, getCurrentProgress, formatDuration, safeCleanup } = require('../utils/player'); +const { getQueueOrReply } = require('../utils/helpers'); + +function handleSeek(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Nothing is playing!'); + if (!queue || queue.songs.length === 0) { + if (queue) interaction.reply('Nothing is playing!'); + return; + } + + if (queue.isSeeking) { + return interaction.reply('Already seeking, please wait!'); + } + + const seconds = interaction.options.getInteger('seconds'); + const progress = getCurrentProgress(queue); + + if (!progress) { + return interaction.reply('Cannot seek right now!'); + } + + let newPosition = progress.elapsed + seconds; + const song = queue.songs[0]; + + if (newPosition < 0) { + newPosition = 0; + } + + if (newPosition >= song.duration) { + queue.player.stop(); + return interaction.reply('Skipped to next song!'); + } + + queue.isSeeking = true; + safeCleanup(queue, 'Seek'); + + queue.player.stop(); + + console.log(`[SEEK] Guild: ${interaction.guild.id}, From: ${progress.elapsed}s, To: ${newPosition}s`); + + playSong(interaction.guild.id, queue, newPosition); + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setDescription(`⏩ Seeking to ${formatDuration(newPosition)} / ${formatDuration(song.duration)}`); + + interaction.reply({ embeds: [embed] }); +} + +module.exports = { handleSeek }; diff --git a/src/handlers/volumeCommand.js b/src/handlers/volumeCommand.js new file mode 100644 index 0000000..dbd99de --- /dev/null +++ b/src/handlers/volumeCommand.js @@ -0,0 +1,28 @@ +const { setQueueVolume } = require('../utils/player'); +const { getQueueOrReply } = require('../utils/helpers'); + +function handleVolume(interaction, queues) { + const queue = getQueueOrReply(interaction, queues, 'Nothing is playing!'); + if (!queue) return; + + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'set') { + const value = interaction.options.getInteger('value'); + if (value < 0 || value > 100) { + return interaction.reply('Volume must be between 0 and 100!'); + } + setQueueVolume(queue, value); + interaction.reply(`Volume set to ${value}%`); + } else if (subcommand === 'inc') { + const amount = interaction.options.getInteger('amount') || 10; + const newVolume = setQueueVolume(queue, queue.volume + amount); + interaction.reply(`Volume increased to ${newVolume}%`); + } else if (subcommand === 'dec') { + const amount = interaction.options.getInteger('amount') || 10; + const newVolume = setQueueVolume(queue, queue.volume - amount); + interaction.reply(`Volume decreased to ${newVolume}%`); + } +} + +module.exports = { handleVolume }; diff --git a/src/utils/commandRegistry.js b/src/utils/commandRegistry.js new file mode 100644 index 0000000..918e7b3 --- /dev/null +++ b/src/utils/commandRegistry.js @@ -0,0 +1,16 @@ +const { REST, Routes } = require('discord.js'); + +async function registerCommands(commands, token, clientId) { + const rest = new REST({ version: '10' }).setToken(token); + + try { + await rest.put( + Routes.applicationCommands(clientId), + { body: commands.map(cmd => cmd.toJSON()) } + ); + } catch (error) { + console.error('Error registering commands:', error); + } +} + +module.exports = { registerCommands }; diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..f18408f --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,24 @@ +function getQueueOrReply(interaction, queues, message = 'Not in a voice channel!') { + const queue = queues.get(interaction.guild.id); + + if (!queue) { + interaction.reply(message); + return null; + } + + return queue; +} + +function requireVoiceChannel(interaction) { + if (!interaction.member?.voice?.channel) { + interaction.editReply('You need to be in a voice channel!'); + return null; + } + + return interaction.member.voice.channel; +} + +module.exports = { + getQueueOrReply, + requireVoiceChannel, +}; diff --git a/src/utils/player.js b/src/utils/player.js new file mode 100644 index 0000000..44510d1 --- /dev/null +++ b/src/utils/player.js @@ -0,0 +1,516 @@ +const { spawn } = require('child_process'); +const { createAudioResource, StreamType } = require('@discordjs/voice'); + +const COOKIES_FILE = process.env.COOKIES_FILE || 'cookies.txt'; + +class Mutex { + constructor() { + this.locked = false; + this.queue = []; + } + + async lock() { + if (!this.locked) { + this.locked = true; + return Promise.resolve(); + } + return new Promise(resolve => this.queue.push(resolve)); + } + + unlock() { + if (this.queue.length > 0) { + this.queue.shift()(); + } else { + this.locked = false; + } + } +} + +const guildLocks = new Map(); + +async function downloadWithYtDlp(song, options = {}) { + const { logPrefix = 'DOWNLOAD' } = options; + + const ytdlp = spawn('yt-dlp', [ + '--cookies', COOKIES_FILE, + '--format', 'bestaudio/best', + '--output', '-', + song.url, + ]); + + const chunks = []; + let downloadComplete = false; + + ytdlp.stdout.on('data', (chunk) => chunks.push(chunk)); + + ytdlp.stderr.on('data', (data) => { + console.error('[yt-dlp stderr]:', data.toString().trim()); + }); + + ytdlp.on('error', (err) => { + console.error('yt-dlp process error:', err); + }); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (!downloadComplete) { + ytdlp.kill('SIGKILL'); + reject(new Error('Download timeout after 60s')); + } + }, 60000); + + ytdlp.on('exit', (code) => { + clearTimeout(timeout); + downloadComplete = true; + + if (code !== 0) { + console.error(`yt-dlp exited with code ${code}`); + return reject(new Error(`yt-dlp failed with code ${code}`)); + } + + song.audioBuffer = Buffer.concat(chunks); + console.log(`[${logPrefix} COMPLETE] ${song.title}: ${(song.audioBuffer.length / 1024 / 1024).toFixed(2)} MB`); + resolve(); + }); + }); +} + +async function preloadSong(song) { + if (song.audioBuffer || song.isPreloading) return; + + song.isPreloading = true; + console.log(`[PRELOAD] Starting background download: ${song.title}`); + + try { + await downloadWithYtDlp(song, { logPrefix: 'PRELOAD' }); + } catch (error) { + console.log(`[PRELOAD FAILED] ${song.title}: ${error.message}`); + } finally { + song.isPreloading = false; + } +} + +async function getVideoInfo(query) { + return new Promise((resolve, reject) => { + const isUrl = query.startsWith('http://') || query.startsWith('https://'); + const isPlaylist = isUrl && query.includes('list='); + + const args = [ + '--cookies', COOKIES_FILE, + '--dump-json', + ]; + + if (isPlaylist) { + args.push('--flat-playlist', '--yes-playlist'); + } else { + args.push('--no-playlist'); + } + + if (!isUrl) { + args.push(`ytsearch1:${query}`); + } else { + args.push(query); + } + + const ytdlp = spawn('yt-dlp', args); + let output = ''; + let error = ''; + + ytdlp.stdout.on('data', (data) => { + output += data.toString(); + }); + + ytdlp.stderr.on('data', (data) => { + error += data.toString(); + }); + + ytdlp.on('close', (code) => { + if (code !== 0) { + console.error('yt-dlp error:', error); + return resolve(null); + } + + try { + const lines = output.trim().split('\n').filter(l => l.trim()); + + if (isPlaylist && lines.length > 1) { + const videos = lines.map(line => { + try { + const info = JSON.parse(line); + return { + title: info.title, + url: info.url || `https://www.youtube.com/watch?v=${info.id}`, + duration: info.duration || 0, + thumbnail: info.thumbnail || info.thumbnails?.[0]?.url, + }; + } catch (e) { + return null; + } + }).filter(v => v !== null); + + return resolve({ isPlaylist: true, videos }); + } else { + const info = JSON.parse(lines[0]); + resolve({ + isPlaylist: false, + videos: [{ + title: info.title, + url: info.webpage_url || info.url, + duration: info.duration || 0, + thumbnail: info.thumbnail, + }] + }); + } + } catch (err) { + console.error('Parse error:', err); + resolve(null); + } + }); + }); +} + +async function downloadAudio(song) { + if (song.audioBuffer) { + console.log(`[USING CACHE] Audio already downloaded for: ${song.title}`); + return; + } + + console.log(`[DOWNLOAD] Downloading full audio for: ${song.title}`); + await downloadWithYtDlp(song, { logPrefix: 'DOWNLOAD' }); +} + +function createFFmpegProcess(seekSeconds) { + const ffmpegArgs = [ + '-i', 'pipe:0', + '-analyzeduration', '0', + '-loglevel', 'error', + ]; + + if (seekSeconds > 0) { + ffmpegArgs.push('-ss', seekSeconds.toString()); + } + + ffmpegArgs.push( + '-f', 's16le', + '-ar', '48000', + '-ac', '2', + 'pipe:1' + ); + + return spawn('ffmpeg', ffmpegArgs); +} + +function setupFFmpegMonitoring(ffmpeg, song, guildId, queue, seekSeconds, onRetry) { + let processesKilled = false; + let streamStarted = false; + let ffmpegErrors = ''; + let lastDataTime = Date.now(); + + const cleanup = () => { + if (!processesKilled) { + processesKilled = true; + try { + ffmpeg.kill('SIGKILL'); + } catch (e) {} + } + }; + + const handleRetry = () => { + if (queue.isSeeking) { + console.log(`[RETRY DURING SEEK] Resetting seek flag`); + queue.isSeeking = false; + } + + if (song.retryCount < 2) { + song.retryCount++; + console.log(`[RETRY] Attempting retry ${song.retryCount}/2 for: ${song.title}`); + onRetry(guildId, queue, seekSeconds); + } else { + console.error(`[SKIP] Max retries reached for: ${song.title}`); + delete song.audioBuffer; + queue.songs.shift(); + if (queue.songs.length > 0) { + onRetry(guildId, queue); + } + } + }; + + const startTimeout = setTimeout(() => { + if (!streamStarted && !processesKilled) { + console.error(`[TIMEOUT] FFmpeg failed to start in 10s: ${song.title}`); + cleanup(); + handleRetry(); + } + }, 10000); + + const watchdogInterval = setInterval(() => { + const timeSinceLastData = Date.now() - lastDataTime; + if (timeSinceLastData > 10000 && !processesKilled && streamStarted) { + console.error(`[WATCHDOG] No data for 10s, killing ffmpeg for: ${song.title}`); + cleanup(); + clearTimeout(startTimeout); + clearInterval(watchdogInterval); + handleRetry(); + } + }, 5000); + + ffmpeg.stderr.on('data', (data) => { + ffmpegErrors += data.toString(); + console.error('[ffmpeg stderr]:', data.toString().trim()); + }); + + ffmpeg.stdout.on('data', () => { + lastDataTime = Date.now(); + if (!streamStarted) { + streamStarted = true; + clearTimeout(startTimeout); + console.log(`[STREAM STARTED] Guild: ${guildId}, Song: ${song.title}`); + } + }); + + ffmpeg.on('exit', (code, signal) => { + clearTimeout(startTimeout); + clearInterval(watchdogInterval); + if (code !== 0 && code !== null && !processesKilled) { + console.error(`ffmpeg exited with code ${code}, signal ${signal}`); + console.error('ffmpeg errors:', ffmpegErrors); + } + }); + + ffmpeg.on('error', (err) => { + console.error('ffmpeg process error:', err); + clearTimeout(startTimeout); + clearInterval(watchdogInterval); + cleanup(); + }); + + ffmpeg.stdin.on('error', (err) => { + if (err.code !== 'EPIPE') { + console.error('ffmpeg stdin error:', err); + clearTimeout(startTimeout); + clearInterval(watchdogInterval); + cleanup(); + } + }); + + ffmpeg.stdout.on('error', (err) => { + if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') { + console.error('ffmpeg stdout error:', err); + clearTimeout(startTimeout); + clearInterval(watchdogInterval); + cleanup(); + } + }); + + return { + cleanup: () => { + clearTimeout(startTimeout); + clearInterval(watchdogInterval); + cleanup(); + } + }; +} + +function setupResourceHandlers(resource, guildId, queue, cleanupHandlers, onRetry) { + if (resource.playStream && typeof resource.playStream.on === 'function') { + resource.playStream.on('error', (err) => { + console.error('Resource playStream error:', err); + cleanupHandlers.cleanup(); + + if (queue.isSeeking) { + console.log(`[PLAYSTREAM ERROR DURING SEEK] Resetting seek flag`); + queue.isSeeking = false; + return; + } + + queue.songs.shift(); + if (queue.songs.length > 0) { + onRetry(guildId, queue); + } + }); + + resource.playStream.on('close', () => { + if (queue.isPlaying) { + console.warn('Stream closed unexpectedly'); + } + cleanupHandlers.cleanup(); + }); + } else { + console.warn('playStream not available on resource'); + } +} + +function setResourceVolume(resource, queue) { + try { + if (resource.volume) { + resource.volume.setVolume(queue.volume / 100); + } else { + console.warn('Volume control not available on resource'); + } + } catch (err) { + console.error('Failed to set volume:', err); + } +} + +function handlePlaybackComplete(guildId, queue, cleanupHandlers) { + queue.seekOffset = 0; + queue.isPlaying = true; + + if (queue.isSeeking) { + queue.isSeeking = false; + console.log(`[SEEK COMPLETE] Guild: ${guildId}, Seek flag cleared`); + } + + queue.cleanup = cleanupHandlers.cleanup; + console.log(`[PLAY SUCCESS] Guild: ${guildId}, Now playing: ${queue.songs[0].title}`); + + if (queue.songs.length > 1) { + const nextSong = queue.songs[1]; + preloadSong(nextSong).catch(() => {}); + } +} + +async function playSong(guildId, queue, seekSeconds = 0) { + if (!guildLocks.has(guildId)) { + guildLocks.set(guildId, new Mutex()); + } + const lock = guildLocks.get(guildId); + + await lock.lock(); + try { + return await _playSongInternal(guildId, queue, seekSeconds); + } finally { + lock.unlock(); + } +} + +async function _playSongInternal(guildId, queue, seekSeconds = 0) { + const song = queue.songs[0]; + if (!song) return; + + if (!song.retryCount) { + song.retryCount = 0; + } + + console.log(`[PLAY START] Guild: ${guildId}, Song: ${song.title}, URL: ${song.url}, Seek: ${seekSeconds}s, Retry: ${song.retryCount}`); + + try { + await downloadAudio(song); + + const ffmpeg = createFFmpegProcess(seekSeconds); + const cleanupHandlers = setupFFmpegMonitoring(ffmpeg, song, guildId, queue, seekSeconds, playSong); + + ffmpeg.stdin.write(song.audioBuffer); + ffmpeg.stdin.end(); + + const resource = createAudioResource(ffmpeg.stdout, { + inputType: StreamType.Raw, + inlineVolume: true, + }); + + setupResourceHandlers(resource, guildId, queue, cleanupHandlers, playSong); + setResourceVolume(resource, queue); + + queue.resource = resource; + queue.seekOffset = seekSeconds; + queue.player.play(resource); + + handlePlaybackComplete(guildId, queue, cleanupHandlers); + + } catch (error) { + console.error('Play error:', error); + if (queue.cleanup) { + try { + queue.cleanup(); + } catch (e) { + console.error('Cleanup error:', e); + } + } + + if (queue.isSeeking) { + console.log(`[CATCH ERROR DURING SEEK] Resetting seek flag`); + queue.isSeeking = false; + return; + } + + queue.songs.shift(); + if (queue.songs.length > 0) { + playSong(guildId, queue); + } + } +} + +function formatDuration(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +function safeCleanup(queue, context = 'Unknown') { + if (queue.cleanup) { + try { + queue.cleanup(); + queue.cleanup = null; + } catch (e) { + console.error(`Cleanup error (${context}):`, e); + } + } +} + +function clearSongBuffer(song, reason = 'cleanup') { + if (song && song.audioBuffer) { + delete song.audioBuffer; + console.log(`[MEMORY] Cleared buffer for: ${song.title} (${reason})`); + } +} + +function setQueueVolume(queue, volume) { + if (!queue) return false; + + const clampedVolume = Math.max(0, Math.min(100, volume)); + queue.volume = clampedVolume; + + if (queue.resource?.volume) { + queue.resource.volume.setVolume(clampedVolume / 100); + } + + return clampedVolume; +} + +function getCurrentProgress(queue) { + if (!queue || !queue.songs[0]) { + return null; + } + + let elapsed = 0; + + if (queue.resource && queue.resource.playbackDuration !== undefined) { + elapsed = Math.floor(queue.resource.playbackDuration / 1000) + (queue.seekOffset || 0); + } else { + return null; + } + + const duration = queue.songs[0].duration; + + return { + elapsed, + duration, + percentage: duration > 0 ? Math.min(100, (elapsed / duration) * 100) : 0, + }; +} + +module.exports = { + getVideoInfo, + playSong, + preloadSong, + formatDuration, + getCurrentProgress, + safeCleanup, + clearSongBuffer, + setQueueVolume, +}; |
