From bf177e6548f1d87a85a0aed9980d48cdcb8a59ed Mon Sep 17 00:00:00 2001 From: flifloo Date: Mon, 22 Nov 2021 00:44:41 +0100 Subject: [PATCH] Add music module and some fix and ToDos --- .gitignore | 2 +- package-lock.json | 579 +++++++++++++++++++++++++++++++- package.json | 11 +- src/index.ts | 25 +- src/lib/Command.ts | 9 +- src/modules/Music/Player.ts | 131 ++++++++ src/modules/Music/Track.ts | 79 +++++ src/modules/Music/disconnect.ts | 39 +++ src/modules/Music/flush.ts | 43 +++ src/modules/Music/index.ts | 30 ++ src/modules/Music/pause.ts | 43 +++ src/modules/Music/play.ts | 72 ++++ src/modules/Music/queue.ts | 67 ++++ src/modules/Music/resume.ts | 43 +++ src/modules/Music/skip.ts | 43 +++ src/modules/Music/stop.ts | 42 +++ src/modules/Utils/info.ts | 1 + tsconfig.json | 2 +- 18 files changed, 1244 insertions(+), 17 deletions(-) create mode 100644 src/modules/Music/Player.ts create mode 100644 src/modules/Music/Track.ts create mode 100644 src/modules/Music/disconnect.ts create mode 100644 src/modules/Music/flush.ts create mode 100644 src/modules/Music/index.ts create mode 100644 src/modules/Music/pause.ts create mode 100644 src/modules/Music/play.ts create mode 100644 src/modules/Music/queue.ts create mode 100644 src/modules/Music/resume.ts create mode 100644 src/modules/Music/skip.ts create mode 100644 src/modules/Music/stop.ts diff --git a/.gitignore b/.gitignore index d495d3a..9aa7638 100644 --- a/.gitignore +++ b/.gitignore @@ -107,7 +107,7 @@ dist .idea # TypeScript compiled sources -*.js +build/ # Configuration config.json diff --git a/package-lock.json b/package-lock.json index 8969710..a241a40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,12 @@ "dependencies": { "@discordjs/builders": "^0.6.0", "@discordjs/rest": "^0.1.0-canary.0", + "@discordjs/voice": "^0.7.5", "discord-api-types": "^0.23.1", - "discord.js": "^13.1.0" + "discord.js": "^13.1.0", + "libsodium-wrappers": "^0.7.9", + "youtube-dl-exec": "^2.0.0", + "ytdl-core": "^4.9.1" }, "devDependencies": { "typescript": "^4.5.2" @@ -95,6 +99,59 @@ "node": ">=12" } }, + "node_modules/@discordjs/voice": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.7.5.tgz", + "integrity": "sha512-lUk+CmIXNKslT6DkC9IF9rpsqhzlTiedauUCPBzepjd4XWxwBZiyVIzR6QpbAirxkAwCoAbbje+3Ho71PGLEAw==", + "dependencies": { + "@types/ws": "^8.2.0", + "discord-api-types": "^0.24.0", + "prism-media": "^1.3.2", + "tiny-typed-emitter": "^2.1.0", + "tslib": "^2.3.1", + "ws": "^8.2.3" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/voice/node_modules/@types/ws": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@discordjs/voice/node_modules/discord-api-types": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz", + "integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@discordjs/voice/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "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/@sapphire/async-queue": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz", @@ -173,6 +230,27 @@ "node": ">= 0.8" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "engines": { + "node": ">=8" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -254,6 +332,28 @@ "node": ">=6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -267,6 +367,25 @@ "node": ">= 6" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -275,11 +394,65 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unix": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-unix/-/is-unix-2.0.1.tgz", + "integrity": "sha512-RyKp5JtlRnfOvnKtfBMPLw9ocqDe1NlPQ8Bt+geVzKGfMnLGj8z/Y2HOmk/aMf47P4EbrEt9dN6YGTT1fx4mZA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/libsodium": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz", + "integrity": "sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz", + "integrity": "sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==", + "dependencies": { + "libsodium": "^0.7.0" + } + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "node_modules/m3u8stream": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz", + "integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==", + "dependencies": { + "miniget": "^4.0.0", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "node_modules/mime-db": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", @@ -299,10 +472,37 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniget": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz", + "integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-fetch": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.4.tgz", - "integrity": "sha512-aD1fO+xtLiSCc9vuD+sYMxpIuQyhHscGSkBEo2o5LTV/3bTEAYvdUii29n8LlO5uLCmWdGP7uVUVXFo5SRdkLA==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -310,6 +510,31 @@ "node": "4.x || >=6.0.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ow": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz", @@ -329,6 +554,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/prism-media": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz", + "integrity": "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==", + "peerDependencies": { + "@discordjs/opus": "^0.5.0", + "ffmpeg-static": "^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/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -390,6 +690,20 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/ws": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", @@ -409,6 +723,35 @@ "optional": true } } + }, + "node_modules/youtube-dl-exec": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/youtube-dl-exec/-/youtube-dl-exec-2.0.0.tgz", + "integrity": "sha512-wD5BSkxC1o3rzj+1ktXdWObvkorW3Zvl1E+l56JQqYFTek3d9SR7o1RbjLiQZYQpXpoCPw0zV16SNDXjhVksXQ==", + "hasInstallScript": true, + "dependencies": { + "dargs": "~7.0.0", + "execa": "~5.1.0", + "is-unix": "~2.0.1", + "mkdirp": "~1.0.4", + "node-fetch": "~2.6.5" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ytdl-core": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.9.1.tgz", + "integrity": "sha512-6Jbp5RDhUEozlaJQAR+l8oV8AHsx3WUXxSyPxzE6wOIAaLql7Hjiy0ZM58wZoyj1YEenlEPjEqcJIjKYKxvHtQ==", + "dependencies": { + "m3u8stream": "^0.8.3", + "miniget": "^4.0.0", + "sax": "^1.1.3" + }, + "engines": { + "node": ">=10" + } } }, "dependencies": { @@ -473,6 +816,40 @@ } } }, + "@discordjs/voice": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.7.5.tgz", + "integrity": "sha512-lUk+CmIXNKslT6DkC9IF9rpsqhzlTiedauUCPBzepjd4XWxwBZiyVIzR6QpbAirxkAwCoAbbje+3Ho71PGLEAw==", + "requires": { + "@types/ws": "^8.2.0", + "discord-api-types": "^0.24.0", + "prism-media": "^1.3.2", + "tiny-typed-emitter": "^2.1.0", + "tslib": "^2.3.1", + "ws": "^8.2.3" + }, + "dependencies": { + "@types/ws": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==", + "requires": { + "@types/node": "*" + } + }, + "discord-api-types": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz", + "integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} + } + } + }, "@sapphire/async-queue": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz", @@ -527,6 +904,21 @@ "delayed-stream": "~1.0.0" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -584,6 +976,22 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -594,16 +1002,68 @@ "mime-types": "^2.1.12" } }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-unix": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-unix/-/is-unix-2.0.1.tgz", + "integrity": "sha512-RyKp5JtlRnfOvnKtfBMPLw9ocqDe1NlPQ8Bt+geVzKGfMnLGj8z/Y2HOmk/aMf47P4EbrEt9dN6YGTT1fx4mZA==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "libsodium": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz", + "integrity": "sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==" + }, + "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" + } + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "m3u8stream": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz", + "integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==", + "requires": { + "miniget": "^4.0.0", + "sax": "^1.2.4" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "mime-db": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", @@ -617,14 +1077,45 @@ "mime-db": "1.49.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "miniget": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz", + "integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, "node-fetch": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.4.tgz", - "integrity": "sha512-aD1fO+xtLiSCc9vuD+sYMxpIuQyhHscGSkBEo2o5LTV/3bTEAYvdUii29n8LlO5uLCmWdGP7uVUVXFo5SRdkLA==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "requires": { "whatwg-url": "^5.0.0" } }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, "ow": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz", @@ -638,6 +1129,50 @@ "vali-date": "^1.0.0" } }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "prism-media": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz", + "integrity": "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==", + "requires": {} + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -683,11 +1218,41 @@ "webidl-conversions": "^3.0.0" } }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, "ws": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", "requires": {} + }, + "youtube-dl-exec": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/youtube-dl-exec/-/youtube-dl-exec-2.0.0.tgz", + "integrity": "sha512-wD5BSkxC1o3rzj+1ktXdWObvkorW3Zvl1E+l56JQqYFTek3d9SR7o1RbjLiQZYQpXpoCPw0zV16SNDXjhVksXQ==", + "requires": { + "dargs": "~7.0.0", + "execa": "~5.1.0", + "is-unix": "~2.0.1", + "mkdirp": "~1.0.4", + "node-fetch": "~2.6.5" + } + }, + "ytdl-core": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.9.1.tgz", + "integrity": "sha512-6Jbp5RDhUEozlaJQAR+l8oV8AHsx3WUXxSyPxzE6wOIAaLql7Hjiy0ZM58wZoyj1YEenlEPjEqcJIjKYKxvHtQ==", + "requires": { + "m3u8stream": "^0.8.3", + "miniget": "^4.0.0", + "sax": "^1.1.3" + } } } } diff --git a/package.json b/package.json index 67297b5..997aa97 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "test": "test" }, "scripts": { - "run": "tsc && node src/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "build": "tsc", + "run": " node build/index.js", + "dev": "rm -fr build && tsc && node build/index.js" }, "repository": { "type": "git", @@ -26,8 +27,12 @@ "dependencies": { "@discordjs/builders": "^0.6.0", "@discordjs/rest": "^0.1.0-canary.0", + "@discordjs/voice": "^0.7.5", "discord-api-types": "^0.23.1", - "discord.js": "^13.1.0" + "discord.js": "^13.1.0", + "libsodium-wrappers": "^0.7.9", + "youtube-dl-exec": "^2.0.0", + "ytdl-core": "^4.9.1" }, "devDependencies": { "typescript": "^4.5.2" diff --git a/src/index.ts b/src/index.ts index b5d9229..ff059bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,20 @@ import "fs"; -import { Intents } from "discord.js"; +import {Intents, Message} from "discord.js"; import "./lib/Modules"; import {AdministratorClient} from "./lib/AdministratorClient"; const config = require("../config.json"); -const client = new AdministratorClient({ intents: [Intents.FLAGS.GUILDS] }); +const client = new AdministratorClient({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_VOICE_STATES] }); client.once("ready", async () => { client.application = await client.application?.fetch() ?? null; + if ("DEV" in process.env && process.env["DEV"] == "true") { + console.log("Dev mod enable"); + await client.application?.commands.set([]); + for (const name in await client.guilds.fetch()) + await (await client.guilds.fetch(name)).commands.set([]); + } await client.modules.loadAllModules(); console.log("Started !"); }); @@ -24,7 +30,20 @@ client.on("interactionCreate", async interaction => { await command.execute(interaction); } catch (error) { console.error(error); - await interaction.reply({ content: "There was an error while executing this command !", ephemeral: true }); + const msg = {content: "There was an error while executing this command !", ephemeral: true}; + try { + await interaction.reply(msg); + } catch { + try { + await interaction.followUp(msg); + } catch { + try { + await (await interaction.fetchReply() as Message).reply(msg); + } catch { + console.warn("Cant send error message to the user :/"); + } + } + } } }); diff --git a/src/lib/Command.ts b/src/lib/Command.ts index da340ef..cfb5af9 100644 --- a/src/lib/Command.ts +++ b/src/lib/Command.ts @@ -20,7 +20,12 @@ export abstract class Command { async register() { try { - this.scope = await this.module.modules.client?.application?.commands.create(this.data); + if ("DEV" in process.env && process.env["DEV"] == "true") { + const devGuild = await this.module.modules.client?.guilds.fetch(process.env["DEVGUILD"] as any); + this.scope = await devGuild.commands.create(this.data); // ToDo: use only one call to avoid spamming the api + } else { + this.scope = await this.module.modules.client?.application?.commands.create(this.data); + } console.log("Successfully registered commands " + this.scope?.name); } catch (error) { @@ -30,7 +35,7 @@ export abstract class Command { async isRegister(): Promise { if (this.scope) - return !! await this.module.modules.client?.application?.commands.fetch(this.scope.id); + return !! await this.module.modules.client?.application?.commands.fetch(this.scope.id); // ToDo: use only one call to avoid spamming the api return false; } diff --git a/src/modules/Music/Player.ts b/src/modules/Music/Player.ts new file mode 100644 index 0000000..0f58484 --- /dev/null +++ b/src/modules/Music/Player.ts @@ -0,0 +1,131 @@ +import {VoiceChannel} from "discord.js"; +import { + AudioPlayer, + AudioPlayerState, + AudioPlayerStatus, + AudioResource, + createAudioPlayer, + entersState, + joinVoiceChannel, + VoiceConnection, + VoiceConnectionDisconnectReason, + VoiceConnectionState, + VoiceConnectionStatus +} from '@discordjs/voice'; +import {promisify} from "util"; +import {Track} from "./Track"; + +const wait = promisify(setTimeout); + + +export class Player { + readonly connexion: VoiceConnection; + readonly audio: AudioPlayer; + current: Track | null = null; + queue: Track[] = []; + readyLock: boolean = false; + queueLock: boolean = false; + + constructor(voiceChanel: VoiceChannel) { + this.connexion = joinVoiceChannel({channelId: voiceChanel.id, guildId: voiceChanel.guildId, selfDeaf: true, selfMute: false, adapterCreator: voiceChanel.guild.voiceAdapterCreator as any}); + this.audio = createAudioPlayer(); + + this.connexion.on("error", console.warn); + this.audio.on('error', (error: { resource: any; }) => (error.resource as AudioResource).metadata.onError(error as any)); + this.connexion.on("stateChange", async (_: VoiceConnectionState, newState: VoiceConnectionState) => { + if (newState.status === VoiceConnectionStatus.Disconnected) { + if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) { + try { + await entersState(this.connexion, VoiceConnectionStatus.Connecting, 5_000); + } catch { + this.connexion.destroy(); + } + } else if (this.connexion.rejoinAttempts < 5) { + await wait((this.connexion.rejoinAttempts + 1) * 5_000); + this.connexion.rejoin(); + } else { + this.connexion.destroy(); + } + } else if (newState.status === VoiceConnectionStatus.Destroyed) { + this.stop(); + } else if ( + !this.readyLock && + (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling) + ) { + this.readyLock = true; + try { + await entersState(this.connexion, VoiceConnectionStatus.Ready, 20_000); + } catch (e) { + if (this.connexion.state.status !== VoiceConnectionStatus.Destroyed) this.connexion.destroy(); + } finally { + this.readyLock = false; + } + } + }); + this.audio.on('stateChange', (oldState: AudioPlayerState, newState: AudioPlayerState) => { + if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { + (oldState.resource as AudioResource).metadata.onFinish(); + void this.processQueue(); + } else if (newState.status === AudioPlayerStatus.Playing) { + (newState.resource as AudioResource).metadata.onStart(); + } + }); + + this.connexion.subscribe(this.audio); + } + + public enqueue(track: Track) { + this.queue.push(track); + void this.processQueue(); + } + + public pause() { + this.audio.pause(); + } + + public resume() { + this.audio.unpause(); + } + + public async skip() { + this.audio.stop(true); + await this.processQueue(); + } + + public stop() { + this.queueLock = true; + this.queue = []; + this.audio.stop(true); + } + + public disconnect() { + if (this.audio.state.status != AudioPlayerStatus.Idle) + this.stop(); + this.connexion.disconnect(); + } + + public flush() { + this.queue = []; + } + + private async processQueue(): Promise { + if (this.queueLock || this.audio.state.status !== AudioPlayerStatus.Idle || this.queue.length === 0) { + return; + } + + this.queueLock = true; + + const nextTrack = this.queue.shift()!; + try { + const resource = await nextTrack.createAudioResource(); + this.audio.play(resource); + this.queueLock = false; + this.current = nextTrack; + } catch (error) { + await nextTrack.onError(error as Error); + this.queueLock = false; + this.current = null; + return this.processQueue(); + } + } +} diff --git a/src/modules/Music/Track.ts b/src/modules/Music/Track.ts new file mode 100644 index 0000000..8ca0207 --- /dev/null +++ b/src/modules/Music/Track.ts @@ -0,0 +1,79 @@ +import {getInfo, videoInfo} from "ytdl-core"; +import { AudioResource, createAudioResource, demuxProbe } from "@discordjs/voice"; +import {exec as ytdl} from "youtube-dl-exec"; +import {CommandInteraction, Message} from "discord.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = async () => {}; + + +export class Track { + public readonly info: videoInfo; + private readonly interaction: CommandInteraction; + + private constructor(info: videoInfo, interaction: CommandInteraction) { + this.info = info; + this.interaction = interaction + } + + private async replyInteraction(message: string) { + try { + await this.interaction.followUp(message); + } catch { + await (await this.interaction.fetchReply() as Message).reply(message); + } + } + + async onStart() { + this.onStart = noop; + await this.replyInteraction("Now playing"); + } + + async onFinish() { + this.onStart = noop; + } + + async onError(error: Error) { + this.onStart = noop; + console.error(error); + await this.replyInteraction("Error with this song, sorry :/"); + } + + public createAudioResource(): Promise> { + return new Promise((resolve, reject) => { + const process = ytdl( + this.info.videoDetails.video_url, + { + output: "-", + quiet: true, + format: 'bestaudio[ext=webm+acodec=opus+asr=48000]/bestaudio', + limitRate: '100K' + }, + { stdio: ['ignore', 'pipe', 'ignore'] }, + ); + if (!process.stdout) { + reject(new Error('No stdout')); + return; + } + const stream = process.stdout; + const onError = (error: Error) => { + if (!process.killed) process.kill(); + stream.resume(); + reject(error); + }; + process + .once('spawn', () => { + demuxProbe(stream) + .then((probe: { stream: any; type: any; }) => resolve(createAudioResource(probe.stream, { metadata: this, inputType: probe.type }))) + .catch(onError); + }) + .catch(onError); + }); + } + + public static async from(url: string, interaction: CommandInteraction): Promise { + const info = await getInfo(url); + + return new Track(info, interaction); + } +} diff --git a/src/modules/Music/disconnect.ts b/src/modules/Music/disconnect.ts new file mode 100644 index 0000000..00216ac --- /dev/null +++ b/src/modules/Music/disconnect.ts @@ -0,0 +1,39 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerStatus} from "@discordjs/voice"; + + +export class DisconnectCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "disconnect", + description: "Stop the music" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } + + player.disconnect(); + await interaction.followUp("Bot disconnected"); + } +} diff --git a/src/modules/Music/flush.ts b/src/modules/Music/flush.ts new file mode 100644 index 0000000..1bcb830 --- /dev/null +++ b/src/modules/Music/flush.ts @@ -0,0 +1,43 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerPausedState, AudioPlayerStatus} from "@discordjs/voice"; + + +export class FlushCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "flush", + description: "Flush the music queue" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } else if (!player.queue.length) { + await interaction.editReply("Can't flush queue, there is no music left"); + return; + } + + player.flush(); + await interaction.followUp("Queue flushed"); + } +} diff --git a/src/modules/Music/index.ts b/src/modules/Music/index.ts new file mode 100644 index 0000000..6ff7fdd --- /dev/null +++ b/src/modules/Music/index.ts @@ -0,0 +1,30 @@ +import {Module} from "../../lib/Module"; +import {Modules} from "../../lib/Modules"; +import {PlayCommand} from "./play"; +import {Snowflake} from "discord-api-types"; +import {Player} from "./Player"; +import {StopCommand} from "./stop"; +import {PauseCommand} from "./pause"; +import {SkipCommand} from "./skip"; +import {ResumeCommand} from "./resume"; +import {FlushCommand} from "./flush"; +import {QueueCommand} from "./queue"; +import {DisconnectCommand} from "./disconnect"; + + +export class Music extends Module { + players: Map = new Map(); + + constructor(modules: Modules) { + super(modules); + this.commands.push(new PlayCommand(this)); + this.commands.push(new StopCommand(this)); + this.commands.push(new PauseCommand(this)); + this.commands.push(new ResumeCommand(this)); + this.commands.push(new SkipCommand(this)); + this.commands.push(new FlushCommand(this)); + this.commands.push(new QueueCommand(this)); + this.commands.push(new DisconnectCommand(this)); + // ToDo: stop if nobody in the channel + } +} diff --git a/src/modules/Music/pause.ts b/src/modules/Music/pause.ts new file mode 100644 index 0000000..1f9dcc7 --- /dev/null +++ b/src/modules/Music/pause.ts @@ -0,0 +1,43 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerPausedState, AudioPlayerStatus} from "@discordjs/voice"; + + +export class PauseCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "pause", + description: "Pause the music" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } else if ([AudioPlayerStatus.Playing, AudioPlayerStatus.Buffering].includes(player.audio.state.status)) { + await interaction.editReply(`Can't pause, the music is ${player.audio.state.status}`); + return; + } + + player.pause(); + await interaction.followUp("Music paused"); + } +} diff --git a/src/modules/Music/play.ts b/src/modules/Music/play.ts new file mode 100644 index 0000000..c4fc17f --- /dev/null +++ b/src/modules/Music/play.ts @@ -0,0 +1,72 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember, VoiceChannel} from "discord.js"; +import {Music} from "./index"; +import {Player} from "./Player"; +import {Track} from "./Track"; +import {entersState, VoiceConnectionStatus} from "@discordjs/voice"; +const {Constants: { ApplicationCommandOptionTypes }} = require("discord.js"); + +export class PlayCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "play", + description: "Play a music", + options: [{ + type: ApplicationCommandOptionTypes.STRING, + name: "music", + description: "The music to play", + required: true + }] + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + if (! interaction.member.voice.channel || ! (interaction.member.voice.channel instanceof VoiceChannel)) { + await interaction.editReply("You must be connected into a voice channel !"); + return; + } else if (!interaction.member.voice.channel.joinable) { + await interaction.editReply("The bot doesn't have the permission to join this voice channel :/"); + return; + } + + player = new Player(interaction.member.voice.channel); + this.module.players.set(interaction.guild.id, player); + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } + + try { + await entersState(player.connexion, VoiceConnectionStatus.Ready, 20e3); + } catch (error) { + console.warn("Fail to enter state Ready !"); + await interaction.followUp("Failed to join voice channel within 20 seconds, please try again later !"); + return; + } + + const url = interaction.options.get("music")!.value! as string; + + try { + const track = await Track.from(url, interaction); + player.enqueue(track); + await interaction.followUp(`${track.info.videoDetails.title} added to queue`); + } catch (error) { + console.error(error); + await interaction.followUp("Fail to add to queue") + } + } +} diff --git a/src/modules/Music/queue.ts b/src/modules/Music/queue.ts new file mode 100644 index 0000000..74c1543 --- /dev/null +++ b/src/modules/Music/queue.ts @@ -0,0 +1,67 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerStatus} from "@discordjs/voice"; + + +function millisecondsToTime(milli: number): string { + const seconds = Math.floor((milli / 1000) % 60); + const minutes = Math.floor((milli / (60 * 1000)) % 60); + + return ('0' + minutes).slice(-2) + ":" + ('0' + seconds).slice(-2); +} + +export class QueueCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "queue", + description: "Display the current queue" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } else if (player.audio.state.status == AudioPlayerStatus.Idle) { + await interaction.editReply("There is no queue to display"); + return; + } + + let queue = ""; + + if (player.queue.length) { + queue = player.queue.map((m, n) => `${n}. ${m.info.videoDetails.title}`).join("\n") + "\n"; + } + + let barr = ""; + if ([AudioPlayerStatus.Playing, AudioPlayerStatus.Paused, AudioPlayerStatus.AutoPaused].includes(player.audio.state.status)) { + // @ts-ignore + const duration: number = player.current?.info.videoDetails.lengthSeconds * 1000; + // @ts-ignore + const current: number = player.audio.state.playbackDuration; + + const maxSize = 35; + const progress = Math.ceil((current/duration)*maxSize); + barr = `\n${player.current?.info.videoDetails.title}\n${millisecondsToTime(current)} [${"=".repeat(progress)}${"-".repeat(maxSize-progress)}] ${millisecondsToTime(duration)}\n` + } + + await interaction.followUp(`\`\`\`md\n${queue}${barr}\`\`\``); + } +} diff --git a/src/modules/Music/resume.ts b/src/modules/Music/resume.ts new file mode 100644 index 0000000..341e78e --- /dev/null +++ b/src/modules/Music/resume.ts @@ -0,0 +1,43 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerStatus} from "@discordjs/voice"; + + +export class ResumeCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "resume", + description: "Resume the music" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } else if ([AudioPlayerStatus.Paused, AudioPlayerStatus.AutoPaused].includes(player.audio.state.status)) { + await interaction.editReply(`Can't resume, the music is ${player.audio.state.status}`); + return; + } + + player.resume(); + await interaction.followUp("Music resumed"); + } +} diff --git a/src/modules/Music/skip.ts b/src/modules/Music/skip.ts new file mode 100644 index 0000000..f764cd1 --- /dev/null +++ b/src/modules/Music/skip.ts @@ -0,0 +1,43 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerStatus} from "@discordjs/voice"; + + +export class SkipCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "skip", + description: "Skip the music" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } else if (player.audio.state.status == AudioPlayerStatus.Idle) { + await interaction.editReply("Can't skip, there is no music"); + return; + } + + await player.skip(); + await interaction.followUp("Music skipped"); + } +} diff --git a/src/modules/Music/stop.ts b/src/modules/Music/stop.ts new file mode 100644 index 0000000..d18b327 --- /dev/null +++ b/src/modules/Music/stop.ts @@ -0,0 +1,42 @@ +import {Command} from "../../lib/Command"; +import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js"; +import {Music} from "./index"; +import {AudioPlayerStatus} from "@discordjs/voice"; + + +export class StopCommand extends Command { + data: ChatInputApplicationCommandData = { + name: "stop", + description: "Stop the music" + }; + module: Music; + + constructor(module: Music) { + super(module); + this.module = module; + } + + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + if (!interaction.guild || ! (interaction.member instanceof GuildMember)) { + await interaction.editReply("This command is only usable in a guild :/"); + return; + } + + let player = this.module.players.get(interaction.guild.id); + + if (!player) { + await interaction.editReply("No music currently playing !"); + return; + } else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) { + await interaction.editReply("You must be in the same voice channel !"); + return; + } else if (player.audio.state.status == AudioPlayerStatus.Idle) { + await interaction.editReply("Can't stop, there is no music"); + return; + } + + player.stop(); + await interaction.followUp("Music stopped"); + } +} diff --git a/src/modules/Utils/info.ts b/src/modules/Utils/info.ts index 67faf2c..221654c 100644 --- a/src/modules/Utils/info.ts +++ b/src/modules/Utils/info.ts @@ -8,6 +8,7 @@ import { TextChannel, VoiceChannel } from "discord.js"; + const {Constants: { ApplicationCommandOptionTypes }} = require("discord.js"); diff --git a/tsconfig.json b/tsconfig.json index ceb46b8..510ea79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "lib": ["es2021"], "module": "commonjs", "target": "es2021", - + "outDir": "build", "strict": true, "esModuleInterop": true, "skipLibCheck": true,