diff --git a/.env b/.env index 64f6d58..f365365 100644 --- a/.env +++ b/.env @@ -1,10 +1,11 @@ BOT_VERBOSE_LOGGING=false BOT_FFMPEG_LOGGING=false -BOT_COMMAND_PREFIX=// +BOT_COMMAND_PREFIX=$ BOT_LANGUAGE=en BOT_MAX_SONGS_IN_QUEUE=500 +BOT_MAX_SONGS_HISTORY_SIZE=60 BOT_DISCORD_TOKEN=undefined BOT_DISCORD_CLIENT_ID=undefined diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4a6923c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +msvs_version=2022 diff --git a/package.json b/package.json index eb3ddce..3d0676a 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "aicbot", - "version": "3.5.2", + "version": "3.6.0", "description": "Discord Bot for playing music", "main": "build/main.js", "scripts": { "build": "tsc", - "development": "tsc&& cross-env NODE_ENV=development node build/main.js", + "development": "tsc&& cross-env NODE_ENV=development node --trace-deprecation build/main.js ", "production": "cross-env NODE_ENV=production node build/main.js", "cookies_grabbing": "cross-env NODE_ENV=development node build/Script_getCookie.js", "test": "tsc&& cross-env NODE_ENV=development node --test", @@ -21,47 +21,48 @@ "node": ">=20" }, "dependencies": { - "@discordjs/rest": "^2.3.0", - "@discordjs/voice": "^0.17.0", - "@distube/direct-link": "^1.0.1", - "@distube/file": "^1.0.1", - "@distube/soundcloud": "^2.0.3", - "@distube/spotify": "^2.0.2", - "@distube/youtube": "^1.0.4", - "@distube/yt-dlp": "^2.0.1", - "@distube/ytdl-core": "^4.14.3", - "@distube/ytsr": "^2.0.4", - "@eslint/js": "^9.8.0", + "@discordjs/rest": "2.3.0", + "@discordjs/voice": "0.17.0", + "@distube/direct-link": "1.0.1", + "@distube/file": "1.0.1", + "@distube/soundcloud": "2.0.3", + "@distube/spotify": "2.0.2", + "@distube/youtube": "1.0.4", + "@distube/yt-dlp": "2.0.1", + "@distube/ytdl-core": "4.14.4", + "@distube/ytsr": "2.0.4", "cross-env": "7.0.3", - "discord.js": "^14.15.3", - "distube": "^5.0.2", - "distube-apple-music": "^0.1.0", - "distube-yandex-music-plugin": "^1.0.5", - "dotenv": "^16.4.5", - "genius-lyrics": "^4.4.7", - "i18next": "^23.12.2", - "i18next-fs-backend": "^2.3.2", - "mongoose": "^8.5.2", - "node-cron": "^3.0.3", - "node-os-utils": "^1.3.7", - "opusscript": "^0.1.1", - "prism-media": "^1.3.5", + "discord.js": "14.15.3", + "distube": "5.0.2", + "distube-apple-music": "0.1.0", + "distube-yandex-music-plugin": "1.0.5", + "dotenv": "16.4.5", + "genius-lyrics": "4.4.7", + "i18next": "23.12.2", + "i18next-fs-backend": "2.3.2", + "mongoose": "8.5.2", + "node-cron": "3.0.3", + "node-os-utils": "1.3.7", + "opusscript": "0.1.1", + "prism-media": "1.3.5", "puppeteer": "22.13.1", - "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-stealth": "^2.11.2", - "sodium-native": "^4.1.1", - "typescript-eslint": "^7.18.0", - "uuid": "^10.0.0", - "zod": "^3.23.8" + "puppeteer-extra": "3.3.6", + "puppeteer-extra-plugin-stealth": "2.11.2", + "sodium-native": "4.1.1", + "soundcloud.ts": "0.5.5", + "uuid": "10.0.0", + "zod": "3.23.8" }, "devDependencies": { - "@types/node": "^20.14.14", + "@types/node": "^22.2.0", "@types/node-cron": "^3.0.11", "@types/node-os-utils": "^1.3.4", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "eslint": "^9.8.0", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", + "@eslint/js": "^9.9.0", + "typescript-eslint": "7.18.0", + "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "globals": "^15.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23d3d70..0188dae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,105 +9,105 @@ importers: .: dependencies: '@discordjs/rest': - specifier: ^2.3.0 + specifier: 2.3.0 version: 2.3.0 '@discordjs/voice': - specifier: ^0.17.0 + specifier: 0.17.0 version: 0.17.0(opusscript@0.1.1) '@distube/direct-link': - specifier: ^1.0.1 + specifier: 1.0.1 version: 1.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/file': - specifier: ^1.0.1 + specifier: 1.0.1 version: 1.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/soundcloud': - specifier: ^2.0.3 + specifier: 2.0.3 version: 2.0.3(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/spotify': - specifier: ^2.0.2 + specifier: 2.0.2 version: 2.0.2(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/youtube': - specifier: ^1.0.4 + specifier: 1.0.4 version: 1.0.4(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/yt-dlp': - specifier: ^2.0.1 + specifier: 2.0.1 version: 2.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/ytdl-core': - specifier: ^4.14.3 - version: 4.14.3 + specifier: 4.14.4 + version: 4.14.4 '@distube/ytsr': - specifier: ^2.0.4 + specifier: 2.0.4 version: 2.0.4 - '@eslint/js': - specifier: ^9.8.0 - version: 9.8.0 cross-env: specifier: 7.0.3 version: 7.0.3 discord.js: - specifier: ^14.15.3 + specifier: 14.15.3 version: 14.15.3 distube: - specifier: ^5.0.2 + specifier: 5.0.2 version: 5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3) distube-apple-music: - specifier: ^0.1.0 + specifier: 0.1.0 version: 0.1.0(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) distube-yandex-music-plugin: - specifier: ^1.0.5 + specifier: 1.0.5 version: 1.0.5(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) dotenv: - specifier: ^16.4.5 + specifier: 16.4.5 version: 16.4.5 genius-lyrics: - specifier: ^4.4.7 + specifier: 4.4.7 version: 4.4.7 i18next: - specifier: ^23.12.2 + specifier: 23.12.2 version: 23.12.2 i18next-fs-backend: - specifier: ^2.3.2 + specifier: 2.3.2 version: 2.3.2 mongoose: - specifier: ^8.5.2 + specifier: 8.5.2 version: 8.5.2(socks@2.8.3) node-cron: - specifier: ^3.0.3 + specifier: 3.0.3 version: 3.0.3 node-os-utils: - specifier: ^1.3.7 + specifier: 1.3.7 version: 1.3.7 opusscript: - specifier: ^0.1.1 + specifier: 0.1.1 version: 0.1.1 prism-media: - specifier: ^1.3.5 + specifier: 1.3.5 version: 1.3.5(opusscript@0.1.1) puppeteer: specifier: 22.13.1 version: 22.13.1(typescript@5.5.4) puppeteer-extra: - specifier: ^3.3.6 + specifier: 3.3.6 version: 3.3.6(@types/puppeteer@7.0.4(typescript@5.5.4))(puppeteer-core@22.13.1)(puppeteer@22.13.1(typescript@5.5.4)) puppeteer-extra-plugin-stealth: - specifier: ^2.11.2 + specifier: 2.11.2 version: 2.11.2(puppeteer-extra@3.3.6(@types/puppeteer@7.0.4(typescript@5.5.4))(puppeteer-core@22.13.1)(puppeteer@22.13.1(typescript@5.5.4))) sodium-native: - specifier: ^4.1.1 + specifier: 4.1.1 version: 4.1.1 - typescript-eslint: - specifier: ^7.18.0 - version: 7.18.0(eslint@9.8.0)(typescript@5.5.4) + soundcloud.ts: + specifier: 0.5.5 + version: 0.5.5 uuid: - specifier: ^10.0.0 + specifier: 10.0.0 version: 10.0.0 zod: - specifier: ^3.23.8 + specifier: 3.23.8 version: 3.23.8 devDependencies: + '@eslint/js': + specifier: ^9.9.0 + version: 9.9.0 '@types/node': - specifier: ^20.14.14 - version: 20.14.14 + specifier: ^22.2.0 + version: 22.2.0 '@types/node-cron': specifier: ^3.0.11 version: 3.0.11 @@ -118,20 +118,20 @@ importers: specifier: ^10.0.0 version: 10.0.0 '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.0.1 + version: 8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@9.8.0)(typescript@5.5.4) + specifier: ^8.0.1 + version: 8.0.1(eslint@9.9.0)(typescript@5.5.4) eslint: - specifier: ^9.8.0 - version: 9.8.0 + specifier: ^9.9.0 + version: 9.9.0 eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.8.0) + version: 9.1.0(eslint@9.9.0) eslint-plugin-prettier: specifier: ^5.2.1 - version: 5.2.1(eslint-config-prettier@9.1.0(eslint@9.8.0))(eslint@9.8.0)(prettier@3.3.3) + version: 5.2.1(eslint-config-prettier@9.1.0(eslint@9.9.0))(eslint@9.9.0)(prettier@3.3.3) globals: specifier: ^15.9.0 version: 15.9.0 @@ -141,6 +141,9 @@ importers: typescript: specifier: ^5.5.4 version: 5.5.4 + typescript-eslint: + specifier: 7.18.0 + version: 7.18.0(eslint@9.9.0)(typescript@5.5.4) packages: @@ -222,8 +225,8 @@ packages: peerDependencies: distube: '5' - '@distube/ytdl-core@4.14.3': - resolution: {integrity: sha512-z6i5EVGEuKhuvuRNyIqafBSs5aRA28HssnWehCRhEtYrxeFgXImfmpKTjUusHYU5vQt1swSVPVb2JCd22P0CPA==} + '@distube/ytdl-core@4.14.4': + resolution: {integrity: sha512-dHb4GW3qATIjRsS6VIhm3Pop7FdUcDFhsnyQlsPeXW7UhTPuNS0BmraKiTpFbpp0Ky+rxBQjJBfPRFsM+dT1fg==} engines: {node: '>=14.0'} '@distube/ytpl@1.2.1': @@ -252,8 +255,8 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.8.0': - resolution: {integrity: sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==} + '@eslint/js@9.9.0': + resolution: {integrity: sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': @@ -323,8 +326,8 @@ packages: '@types/node-os-utils@1.3.4': resolution: {integrity: sha512-BCUYrbdoO4FUbx6MB9atLNFnkxdliFaxdiTJMIPPiecXIApc5zf4NIqV5G1jWv/ReZvtYyHLs40RkBjHX+vykA==} - '@types/node@20.14.14': - resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} + '@types/node@22.2.0': + resolution: {integrity: sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==} '@types/puppeteer@7.0.4': resolution: {integrity: sha512-ja78vquZc8y+GM2al07GZqWDKQskQXygCDiu0e3uO0DMRKqE0MjrFBFmTulfPYzLB6WnL7Kl2tFPy0WXSpPomg==} @@ -356,6 +359,17 @@ packages: typescript: optional: true + '@typescript-eslint/eslint-plugin@8.0.1': + resolution: {integrity: sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/parser@7.18.0': resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -366,10 +380,24 @@ packages: typescript: optional: true + '@typescript-eslint/parser@8.0.1': + resolution: {integrity: sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/scope-manager@7.18.0': resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.0.1': + resolution: {integrity: sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -380,10 +408,23 @@ packages: typescript: optional: true + '@typescript-eslint/type-utils@8.0.1': + resolution: {integrity: sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/types@7.18.0': resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.0.1': + resolution: {integrity: sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -393,16 +434,35 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.0.1': + resolution: {integrity: sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 + '@typescript-eslint/utils@8.0.1': + resolution: {integrity: sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/visitor-keys@7.18.0': resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.0.1': + resolution: {integrity: sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vladfrangu/async_event_emitter@2.4.5': resolution: {integrity: sha512-J7T3gUr3Wz0l7Ni1f9upgBZ7+J22/Q1B7dl0X6fG+fTsD+H+31DIosMHj4Um1dWQwqbcQ3oQf+YS2foYkDc9cQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -750,10 +810,15 @@ packages: resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.8.0: - resolution: {integrity: sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==} + eslint@9.9.0: + resolution: {integrity: sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true espree@10.1.0: resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} @@ -1555,8 +1620,8 @@ packages: sodium-native@4.1.1: resolution: {integrity: sha512-LXkAfRd4FHtkQS4X6g+nRcVaN7mWVNepV06phIsC6+IZFvGh1voW5TNQiQp2twVaMf05gZqQjuS+uWLM6gHhNQ==} - soundcloud.ts@0.5.3: - resolution: {integrity: sha512-ZMH6gG5e7WqJrIYXTv14MNArPhx3WzfrL1Ij/2qBDW8mVbNJc8lxOQOc4kLvrfvDl5TkCdZa7zXOiwD6ESXq+g==} + soundcloud.ts@0.5.5: + resolution: {integrity: sha512-bygjhC1w/w26Nk0Y+4D4cWSEJ1TdxLaE6+w4pCazFzPF+J4mzuB62ggWmFa7BiwnirzNf9lgPbjzrQYGege4Ew==} source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} @@ -1681,8 +1746,8 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.13.0: + resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} @@ -1692,8 +1757,8 @@ packages: resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==} engines: {node: '>=18.0'} - undici@6.19.5: - resolution: {integrity: sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg==} + undici@6.19.7: + resolution: {integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==} engines: {node: '>=18.17'} unfetch@5.0.0: @@ -1885,7 +1950,7 @@ snapshots: '@distube/direct-link@1.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3))': dependencies: distube: 5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3) - undici: 6.19.5 + undici: 6.19.7 '@distube/file@1.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3))': dependencies: @@ -1894,7 +1959,7 @@ snapshots: '@distube/soundcloud@2.0.3(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3))': dependencies: distube: 5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3) - soundcloud.ts: 0.5.3 + soundcloud.ts: 0.5.5 '@distube/spotify@2.0.2(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3))': dependencies: @@ -1902,13 +1967,13 @@ snapshots: spotify-uri: 4.1.0 spotify-url-info: 3.2.16 spotify-web-api-node: 5.0.2 - undici: 6.19.5 + undici: 6.19.7 transitivePeerDependencies: - supports-color '@distube/youtube@1.0.4(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3))': dependencies: - '@distube/ytdl-core': 4.14.3 + '@distube/ytdl-core': 4.14.4 '@distube/ytpl': 1.2.1 '@distube/ytsr': 2.0.4 distube: 5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3) @@ -1919,9 +1984,9 @@ snapshots: dependencies: dargs: 7.0.0 distube: 5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3) - undici: 6.19.5 + undici: 6.19.7 - '@distube/ytdl-core@4.14.3': + '@distube/ytdl-core@4.14.4': dependencies: http-cookie-agent: 6.0.5(tough-cookie@4.1.4)(undici@5.28.4) m3u8stream: 0.8.6 @@ -1938,11 +2003,11 @@ snapshots: '@distube/ytsr@2.0.4': dependencies: - undici: 6.19.5 + undici: 6.19.7 - '@eslint-community/eslint-utils@4.4.0(eslint@9.8.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.9.0)': dependencies: - eslint: 9.8.0 + eslint: 9.9.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.11.0': {} @@ -1969,7 +2034,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.8.0': {} + '@eslint/js@9.9.0': {} '@eslint/object-schema@2.1.4': {} @@ -2031,9 +2096,9 @@ snapshots: '@types/node-os-utils@1.3.4': {} - '@types/node@20.14.14': + '@types/node@22.2.0': dependencies: - undici-types: 5.26.5 + undici-types: 6.13.0 '@types/puppeteer@7.0.4(typescript@5.5.4)': dependencies: @@ -2055,22 +2120,40 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 20.14.14 + '@types/node': 22.2.0 '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.14.14 + '@types/node': 22.2.0 optional: true - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 7.18.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 7.18.0(eslint@9.9.0)(typescript@5.5.4) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/utils': 7.18.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 7.18.0(eslint@9.9.0)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.8.0 + eslint: 9.9.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.0.1 + '@typescript-eslint/type-utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.1 + eslint: 9.9.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 @@ -2080,14 +2163,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/parser@7.18.0(eslint@9.9.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.3.6 - eslint: 9.8.0 + eslint: 9.9.0 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.0.1 + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/typescript-estree': 8.0.1(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.1 + debug: 4.3.6 + eslint: 9.9.0 optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: @@ -2098,20 +2194,39 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/type-utils@7.18.0(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/scope-manager@8.0.1': + dependencies: + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/visitor-keys': 8.0.1 + + '@typescript-eslint/type-utils@7.18.0(eslint@9.9.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) - '@typescript-eslint/utils': 7.18.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/utils': 7.18.0(eslint@9.9.0)(typescript@5.5.4) debug: 4.3.6 - eslint: 9.8.0 + eslint: 9.9.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.0.1(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 8.0.1(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + debug: 4.3.6 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.0.1': {} + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -2127,13 +2242,39 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.8.0)(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.0.1(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/visitor-keys': 8.0.1 + debug: 4.3.6 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) - eslint: 9.8.0 + eslint: 9.9.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.0.1(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) + '@typescript-eslint/scope-manager': 8.0.1 + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/typescript-estree': 8.0.1(typescript@5.5.4) + eslint: 9.9.0 transitivePeerDependencies: - supports-color - typescript @@ -2143,6 +2284,11 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.0.1': + dependencies: + '@typescript-eslint/types': 8.0.1 + eslint-visitor-keys: 3.4.3 + '@vladfrangu/async_event_emitter@2.4.5': {} acorn-jsx@5.3.2(acorn@8.12.1): @@ -2414,7 +2560,7 @@ snapshots: '@discordjs/voice': 0.17.0(opusscript@0.1.1) discord.js: 14.15.3 tiny-typed-emitter: 2.1.0 - undici: 6.19.5 + undici: 6.19.7 dom-serializer@2.0.0: dependencies: @@ -2470,18 +2616,18 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@9.1.0(eslint@9.8.0): + eslint-config-prettier@9.1.0(eslint@9.9.0): dependencies: - eslint: 9.8.0 + eslint: 9.9.0 - eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@9.8.0))(eslint@9.8.0)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@9.9.0))(eslint@9.9.0)(prettier@3.3.3): dependencies: - eslint: 9.8.0 + eslint: 9.9.0 prettier: 3.3.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 optionalDependencies: - eslint-config-prettier: 9.1.0(eslint@9.8.0) + eslint-config-prettier: 9.1.0(eslint@9.9.0) eslint-scope@8.0.2: dependencies: @@ -2492,13 +2638,13 @@ snapshots: eslint-visitor-keys@4.0.0: {} - eslint@9.8.0: + eslint@9.9.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) '@eslint-community/regexpp': 4.11.0 '@eslint/config-array': 0.17.1 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.8.0 + '@eslint/js': 9.9.0 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 @@ -2661,7 +2807,7 @@ snapshots: genius-lyrics@4.4.7: dependencies: node-html-parser: 6.1.13 - undici: 6.19.5 + undici: 6.19.7 get-caller-file@2.0.5: {} @@ -3284,9 +3430,9 @@ snapshots: dependencies: node-gyp-build: 4.8.1 - soundcloud.ts@0.5.3: + soundcloud.ts@0.5.5: dependencies: - undici: 6.19.5 + undici: 6.19.7 source-map@0.6.1: optional: true @@ -3416,12 +3562,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@7.18.0(eslint@9.8.0)(typescript@5.5.4): + typescript-eslint@7.18.0(eslint@9.9.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/parser': 7.18.0(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/utils': 7.18.0(eslint@9.8.0)(typescript@5.5.4) - eslint: 9.8.0 + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/parser': 7.18.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 7.18.0(eslint@9.9.0)(typescript@5.5.4) + eslint: 9.9.0 optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: @@ -3434,7 +3580,7 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@5.26.5: {} + undici-types@6.13.0: {} undici@5.28.4: dependencies: @@ -3442,7 +3588,7 @@ snapshots: undici@6.13.0: {} - undici@6.19.5: {} + undici@6.19.7: {} unfetch@5.0.0: {} diff --git a/src/EnvironmentVariables.ts b/src/EnvironmentVariables.ts index 95dbf4a..3ee6a8f 100644 --- a/src/EnvironmentVariables.ts +++ b/src/EnvironmentVariables.ts @@ -1,3 +1,8 @@ +/* + Read wiki if you want to know what all of this variable do. + Or if you are too smart, you can continue to read the code + */ + import { z } from 'zod'; import * as dotenv from 'dotenv'; import { loggerSend } from './utilities/logger.js'; @@ -51,6 +56,7 @@ const envVariables = z.object({ BOT_COMMAND_PREFIX: z.string().min(1), BOT_MAX_SONGS_IN_QUEUE: z.coerce.number().positive().min(1).optional().default(500), + BOT_MAX_SONGS_HISTORY_SIZE: z.coerce.number().nonnegative().optional().default(60), MONGO_URI: z.string(), MONGO_DATABASE_NAME: z.string(), @@ -79,5 +85,8 @@ export const ENV = envVariables.parse(process.env); if (fs.existsSync(envPath)) { loggerSend(`Environment variables is loaded from ${envPath}`, loggerPrefixEnv); } else { - loggerSend(`Environment variables is loaded from OS environment variables`, loggerPrefixEnv); + loggerSend( + `Environment variables is loaded from OS / Docker environment variables`, + loggerPrefixEnv + ); } diff --git a/src/audioplayer/AudioPlayersManager.ts b/src/audioplayer/AudioPlayersManager.ts index 30b2702..93d4c44 100644 --- a/src/audioplayer/AudioPlayersManager.ts +++ b/src/audioplayer/AudioPlayersManager.ts @@ -15,7 +15,7 @@ import { generateErrorEmbed } from '../utilities/generateErrorEmbed.js'; import i18next from 'i18next'; import { loggerError, loggerSend } from '../utilities/logger.js'; import { ENV } from '../EnvironmentVariables.js'; -import { LoadPlugins } from './LoadPlugins.js'; +import { DistubePlugin } from './LoadPlugins.js'; import { generateAddedPlaylistMessage } from './util/generateAddedPlaylistMessage.js'; import { generateAddedSongMessage } from './util/generateAddedSongMessage.js'; import { @@ -33,16 +33,15 @@ import { joinVoiceChannel } from '@discordjs/voice'; import { generateWarningEmbed } from '../utilities/generateWarningEmbed.js'; import { generateLyricsEmbed } from './Lyrics.js'; import { getGuildOptionLeaveOnEmpty, setGuildOptionLeaveOnEmpty } from '../schemas/SchemaGuild.js'; +import { addSongToGuildSongsHistory } from '../schemas/SchemaSongsHistory.js'; export const loggerPrefixAudioplayer = `Audioplayer`; -const plugins = await LoadPlugins(); - export class AudioPlayersManager { client: Client; playersManager: AudioPlayersStore; distube: DisTube; - constructor(client: Client) { + constructor(client: Client, plugins: Array) { this.client = client; this.client.audioPlayer = this; this.playersManager = new AudioPlayersStore(this.client); @@ -52,7 +51,7 @@ export class AudioPlayersManager { emitAddSongWhenCreatingQueue: true, savePreviousSongs: true, joinNewVoiceChannel: true, - plugins: plugins + plugins }); this.setupEvents(); @@ -372,12 +371,20 @@ export class AudioPlayersManager { await queue.textChannel.send({ embeds: [generateAddedSongMessage(song)] }); } + if (ENV.BOT_MAX_SONGS_HISTORY_SIZE > 0) { + await addSongToGuildSongsHistory(queue.id, song); + } + const player = this.playersManager.get(queue.id); if (player) { await player.update(); } }) .on(DistubeEvents.ADD_LIST, async (queue, playlist) => { + if (ENV.BOT_MAX_SONGS_HISTORY_SIZE > 0) { + await addSongToGuildSongsHistory(queue.id, playlist); + } + if (!queue.textChannel) return; await queue.textChannel.send({ embeds: [generateAddedPlaylistMessage(playlist)] }); diff --git a/src/audioplayer/LoadPlugins.ts b/src/audioplayer/LoadPlugins.ts index 9689d49..0177c6b 100644 --- a/src/audioplayer/LoadPlugins.ts +++ b/src/audioplayer/LoadPlugins.ts @@ -8,7 +8,7 @@ import { DirectLinkPlugin } from '@distube/direct-link'; import { FilePlugin } from '@distube/file'; import { AppleMusicPlugin } from 'distube-apple-music'; import { YandexMusicPlugin } from 'distube-yandex-music-plugin'; -import { SoundCloudPlugin } from '@distube/soundcloud'; +import { SoundCloudPlugin } from './plugins/soundcloud.js'; import { getYoutubeCookie } from '../CookiesAutomation.js'; import Cron from 'node-cron'; diff --git a/src/audioplayer/plugins/soundcloud.ts b/src/audioplayer/plugins/soundcloud.ts new file mode 100644 index 0000000..8b3f783 --- /dev/null +++ b/src/audioplayer/plugins/soundcloud.ts @@ -0,0 +1,224 @@ +import { Soundcloud } from 'soundcloud.ts'; +import { DisTubeError, ExtractorPlugin, Playlist, Song, checkInvalidKey } from 'distube'; +import type { ResolveOptions } from 'distube'; +import type { SoundcloudPlaylistV2, SoundcloudTrackV2 } from 'soundcloud.ts'; + +type Falsy = undefined | null | false | 0 | ''; +const isTruthy = (x: T | Falsy): x is T => Boolean(x); +export enum SearchType { + Track = 'track', + Playlist = 'playlist' +} + +export interface SoundCloudPluginOptions { + clientId?: string; + oauthToken?: string; +} + +export class SoundCloudPlugin extends ExtractorPlugin { + soundcloud: Soundcloud; + constructor(options: SoundCloudPluginOptions = {}) { + super(); + if (typeof options !== 'object' || Array.isArray(options)) { + throw new DisTubeError( + 'INVALID_TYPE', + ['object', 'undefined'], + options, + 'SoundCloudPluginOptions' + ); + } + checkInvalidKey(options, ['clientId', 'oauthToken'], 'SoundCloudPluginOptions'); + if (options.clientId && typeof options.clientId !== 'string') { + throw new DisTubeError('INVALID_TYPE', 'string', options.clientId, 'clientId'); + } + if (options.oauthToken && typeof options.oauthToken !== 'string') { + throw new DisTubeError('INVALID_TYPE', 'string', options.oauthToken, 'oauthToken'); + } + + this.soundcloud = new Soundcloud(options.clientId, options.oauthToken); + } + search( + query: string, + type?: SearchType.Track, + limit?: number, + options?: ResolveOptions + ): Promise[]>; + search( + query: string, + type: SearchType.Playlist, + limit?: number, + options?: ResolveOptions + ): Promise[]>; + search( + query: string, + type?: SearchType, + limit?: number, + options?: ResolveOptions + ): Promise[] | Playlist[]>; + async search( + query: string, + type: SearchType = SearchType.Track, + limit = 10, + options: ResolveOptions = {} + ) { + if (typeof query !== 'string') { + throw new DisTubeError('INVALID_TYPE', 'string', query, 'query'); + } + if (!Object.values(SearchType).includes(type)) { + throw new DisTubeError('INVALID_TYPE', Object.values(SearchType), type, 'type'); + } + if (typeof limit !== 'number' || limit < 1 || !Number.isInteger(limit)) { + throw new DisTubeError('INVALID_TYPE', 'natural number', limit, 'limit'); + } + if (typeof options !== 'object' || Array.isArray(options)) { + throw new DisTubeError('INVALID_TYPE', 'object', options, 'ResolveOptions'); + } + + await this.soundcloud.api.getClientId().catch(() => { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NO_CLIENT_ID', + 'Cannot find SoundCloud client id automatically. Please provide a client id in the constructor.\nGuide: https://github.com/distubejs/soundcloud#documentation' + ); + }); + + switch (type) { + case SearchType.Track: { + const data = await this.soundcloud.tracks.searchV2({ q: query, limit }); + if (!data?.collection?.length) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NO_RESULT', + `Cannot find any "${query}" ${type} on SoundCloud!` + ); + } + return data.collection.map((t: any) => new SoundCloudSong(this, t, options)); + } + case SearchType.Playlist: { + const data = await this.soundcloud.playlists.searchV2({ q: query, limit }); + const playlists = data.collection; + return ( + await Promise.all( + playlists.map( + async (p: any) => + new SoundCloudPlaylist(this, await this.soundcloud.playlists.fetch(p), options) + ) + ) + ).filter(isTruthy); + } + default: + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_UNSUPPORTED_TYPE', + `${type} search is not supported!` + ); + } + } + + validate(url: string) { + return /^https?:\/\/(?:(?:www|m)\.)?soundcloud\.com\/(.*)$/.test(url); + } + + async resolve(url: string, options: ResolveOptions) { + await this.soundcloud.api.getClientId().catch(() => { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NO_CLIENT_ID', + 'Cannot find SoundCloud client id automatically. Please provide a client id in the constructor.\nGuide: https://github.com/distubejs/soundcloud#documentation' + ); + }); + const opt = { ...options, source: 'soundcloud' }; + url = url.replace(/:\/\/(m|www)\./g, '://'); + const data = await this.soundcloud.resolve.getV2(url, true).catch((e: { message: string }) => { + throw new DisTubeError('SOUNDCLOUD_PLUGIN_RESOLVE_ERROR', e.message); + }); + if (!data || !['track', 'playlist'].includes(data.kind)) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NOT_SUPPORTED', + 'Only public tracks and playlists are supported.' + ); + } + + return data.kind === 'playlist' + ? new SoundCloudPlaylist(this, await this.soundcloud.playlists.fetch(data), opt) + : new SoundCloudSong(this, data, opt); + } + + async getRelatedSongs(song: SoundCloudSong) { + if (!song.url) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_INVALID_SONG', + 'Cannot get related songs from invalid song.' + ); + } + const related = await this.soundcloud.tracks.relatedV2(song.url, 10); + return related + .filter((t: { title: any }) => t.title) + .map((t: any) => new SoundCloudSong(this, t)); + } + + async getStreamURL(song: SoundCloudSong) { + if (!song.url) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_INVALID_SONG', + 'Cannot get stream url from invalid song.' + ); + } + const stream = await this.soundcloud.util.streamLink(song.url); + if (!stream) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_RATE_LIMITED', + 'Reached SoundCloud rate limits\nSee more: https://developers.soundcloud.com/docs/api/rate-limits#play-requests' + ); + } + return stream; + } + + async searchSong(query: string, options: ResolveOptions) { + const songs = await this.search(query, SearchType.Track, 1, options); + return songs[0]; + } +} + +class SoundCloudSong extends Song { + constructor(plugin: SoundCloudPlugin, info: SoundcloudTrackV2, options: ResolveOptions = {}) { + super( + { + plugin, + source: 'soundcloud', + playFromSource: true, + id: info.id.toString(), + name: info.title, + url: info.permalink_url, + thumbnail: info.artwork_url, + duration: info.duration / 1000, + views: info.playback_count, + uploader: { + name: info.user?.username, + url: info.user?.permalink_url + }, + likes: info.likes_count, + reposts: info.reposts_count + }, + options + ); + } +} + +class SoundCloudPlaylist extends Playlist { + constructor( + plugin: SoundCloudPlugin, + info: SoundcloudPlaylistV2, + options: ResolveOptions = {} + ) { + super( + { + source: 'soundcloud', + id: info.id.toString(), + name: info.title, + url: info.permalink_url, + thumbnail: info.artwork_url ?? undefined, + songs: info.tracks.map((s: any) => new SoundCloudSong(plugin, s, options)) + }, + options + ); + } +} + +export default SoundCloudPlugin; diff --git a/src/commands/admin/setPrefix.command.ts b/src/commands/admin/setPrefix.command.ts index dad1ba1..b45963c 100644 --- a/src/commands/admin/setPrefix.command.ts +++ b/src/commands/admin/setPrefix.command.ts @@ -52,5 +52,8 @@ async function changePrefixTo(guild: Guild, prefix: string): Promise { return i18next.t('commands:set_prefix_restrict_prefixes', { prefixes: '/ @ #' }) as string; if (prefix.length > 2) return i18next.t('commands:set_prefix_length_error') as string; await setGuildOptionPrefix(guild.id, prefix); - return i18next.t('commands:set_prefix_success_change', { prefix: prefix }) as string; + return i18next.t('commands:set_prefix_success_change', { + prefix, + interpolation: { escapeValue: false } + }) as string; } diff --git a/src/commands/audio/history.command.ts b/src/commands/audio/history.command.ts new file mode 100644 index 0000000..1f80abe --- /dev/null +++ b/src/commands/audio/history.command.ts @@ -0,0 +1,134 @@ +import { ICommand } from '../../CommandTypes.js'; +import i18next from 'i18next'; +import { + CommandInteraction, + Embed, + EmbedBuilder, + Guild, + Message, + PermissionsBitField, + SlashCommandBuilder +} from 'discord.js'; +import { GroupAudio } from './AudioTypes.js'; +import { + getOrCreateGuildSongsHistory, + ISchemaSongsHistory +} from '../../schemas/SchemaSongsHistory.js'; +import { pagination } from '../../utilities/pagination/pagination.js'; +import { ButtonStyles, ButtonTypes } from '../../utilities/pagination/paginationTypes.js'; +import { ENV } from '../../EnvironmentVariables.js'; + +export default function (): ICommand { + return { + disable: ENV.BOT_MAX_SONGS_HISTORY_SIZE === 0, + text_data: { + name: 'history', + description: i18next.t('commands:history_desc'), + execute: async (message) => { + await replyWithSongHistory(message.guild as Guild, undefined, message); + } + }, + slash_data: { + slash_builder: new SlashCommandBuilder() + .setName('history') + .setDescription(i18next.t('commands:history_desc')), + execute: async (interaction) => { + await replyWithSongHistory(interaction.guild as Guild, interaction); + } + }, + guild_data: { + guild_only: true + }, + group: GroupAudio, + bot_permissions: [PermissionsBitField.Flags.SendMessages] + }; +} + +async function replyWithSongHistory( + guild: Guild, + interaction?: CommandInteraction, + message?: Message +): Promise { + const history: ISchemaSongsHistory | null = await getOrCreateGuildSongsHistory(guild.id); + + if (!history) throw Error(`Can't find guild songs history: ${guild.id}`); + + if (history.songsHistory.length === 0) { + await interaction?.reply({ + embeds: [new EmbedBuilder().setTitle(i18next.t('commands:history_embed_no_songs'))] + }); + await message?.reply({ + embeds: [new EmbedBuilder().setTitle(i18next.t('commands:history_embed_no_songs'))] + }); + } + + function buildPage(history: ISchemaSongsHistory, pageNumber: number, entriesPerPage: number) { + let songsList = ''; + + const startingIndex = pageNumber * entriesPerPage; + + for ( + let i = startingIndex; + i < Math.min(startingIndex + entriesPerPage, history.songsHistory.length); + i++ + ) { + const song = history.songsHistory[i]; + + const songDate = song.createdAt + ? `` + : ''; + + songsList += + `${i + 1}. ` + + `[${song.name}](${song.url})` + + ` - <@${song.requester}>` + + ` - ${songDate}` + + '\n'; + } + + return new EmbedBuilder() + .setTitle(`${i18next.t('commands:history_embed_title')} ${guild.name}`) + .setDescription(`${songsList}`.slice(0, 4096)); + } + + const arrayEmbeds: Array = []; + const entriesPerPage = 20; + const pages = Math.ceil(history.songsHistory.length / entriesPerPage); + + for (let i = 0; i < pages; i++) { + arrayEmbeds.push(buildPage(history, i, entriesPerPage)); + } + + await pagination({ + embeds: arrayEmbeds as unknown as Embed[], + // @ts-expect-error I need to provide Interaction or Message for different command systems. + author: interaction?.user ?? message?.author, + message: message, + interaction: interaction, + ephemeral: true, + fastSkip: true, + pageTravel: false, + buttons: [ + { + type: ButtonTypes.first, + emoji: '⬅️', + style: ButtonStyles.Secondary + }, + { + type: ButtonTypes.previous, + emoji: '◀️', + style: ButtonStyles.Secondary + }, + { + type: ButtonTypes.next, + emoji: '▶️', + style: ButtonStyles.Secondary + }, + { + type: ButtonTypes.last, + emoji: '➡️', + style: ButtonStyles.Secondary + } + ] + }); +} diff --git a/src/events/interactionHandlers/slashCommandHandler.ts b/src/events/interactionHandlers/slashCommandHandler.ts index f7da309..7a91790 100644 --- a/src/events/interactionHandlers/slashCommandHandler.ts +++ b/src/events/interactionHandlers/slashCommandHandler.ts @@ -22,7 +22,7 @@ export async function slashCommandHandler(interaction: Interaction) { if (command.guild_data?.guild_only) { if (!interaction.guild) { await interaction.reply({ - embeds: [generateErrorEmbed(i18next.t('commandshandlers:command_only_in_guilds'))], + embeds: [generateErrorEmbed(i18next.t('commandsHandlers:command_only_in_guilds'))], ephemeral: true }); return; @@ -44,7 +44,7 @@ export async function slashCommandHandler(interaction: Interaction) { if (!checkMemberInVoice(interaction.member)) { await interaction.reply({ - embeds: [generateErrorEmbed(i18next.t('commandshandlers:command_only_in_voice'))], + embeds: [generateErrorEmbed(i18next.t('commandsHandlers:command_only_in_voice'))], ephemeral: true }); return; @@ -58,9 +58,9 @@ export async function slashCommandHandler(interaction: Interaction) { await interaction.reply({ embeds: [ generateErrorEmbed( - `:no_entry: ${i18next.t('commandshandlers:bot_not_enough_permissions_1')} :no_entry:.\n` + - `${i18next.t('commandshandlers:bot_not_enough_permissions_2')} \n` + - `${i18next.t('commandshandlers:bot_not_enough_permissions_3')}` + `:no_entry: ${i18next.t('commandsHandlers:bot_not_enough_permissions_1')} :no_entry:.\n` + + `${i18next.t('commandsHandlers:bot_not_enough_permissions_2')} \n` + + `${i18next.t('commandsHandlers:bot_not_enough_permissions_3')}` ) ], ephemeral: true diff --git a/src/events/messageHandlers/textCommandsHandler.ts b/src/events/messageHandlers/textCommandsHandler.ts index 4627dd7..c2ff7fe 100644 --- a/src/events/messageHandlers/textCommandsHandler.ts +++ b/src/events/messageHandlers/textCommandsHandler.ts @@ -64,7 +64,7 @@ export async function textCommandsHandler(client: Client, message: Message) { // If command allowed only in guild, check voice_required and voice_with_bot_only properties if (!message.guild) { await message.reply({ - embeds: [generateErrorEmbed(i18next.t('commandshandlers:command_only_in_guilds'))] + embeds: [generateErrorEmbed(i18next.t('commandsHandlers:command_only_in_guilds'))] }); return; } @@ -82,7 +82,7 @@ export async function textCommandsHandler(client: Client, message: Message) { if (!checkMemberInVoice(message.member!)) { await message.reply({ - embeds: [generateErrorEmbed(i18next.t('commandshandlers:command_only_in_voice'))] + embeds: [generateErrorEmbed(i18next.t('commandsHandlers:command_only_in_voice'))] }); return; } @@ -96,9 +96,9 @@ export async function textCommandsHandler(client: Client, message: Message) { await message.reply({ embeds: [ generateErrorEmbed( - `:no_entry: ${i18next.t('commandshandlers:bot_not_enough_permissions_1')} :no_entry:.\n` + - `${i18next.t('commandshandlers:bot_not_enough_permissions_2')} \n` + - `${i18next.t('commandshandlers:bot_not_enough_permissions_3')}` + `:no_entry: ${i18next.t('commandsHandlers:bot_not_enough_permissions_1')} :no_entry:.\n` + + `${i18next.t('commandsHandlers:bot_not_enough_permissions_2')} \n` + + `${i18next.t('commandsHandlers:bot_not_enough_permissions_3')}` ) ] }); @@ -110,7 +110,7 @@ export async function textCommandsHandler(client: Client, message: Message) { if (member) { if (!CheckMemberPermissions(member, command.user_permissions)) { await message.reply({ - embeds: [generateErrorEmbed(i18next.t('commandshandlers:user_not_enough_permissions'))] + embeds: [generateErrorEmbed(i18next.t('commandsHandlers:user_not_enough_permissions'))] }); return; } @@ -120,6 +120,6 @@ export async function textCommandsHandler(client: Client, message: Message) { await command.text_data.execute(message, args); } catch (e) { if (ENV.BOT_VERBOSE_LOGGING) - loggerError(`commandshandlers:text_command_error: ${e}`, loggerPrefixCommandHandler); + loggerError(`Error when executing text command: ${e}`, loggerPrefixCommandHandler); } } diff --git a/src/events/onGuildCreate.event.ts b/src/events/onGuildCreate.event.ts index 5a4cb95..381387c 100644 --- a/src/events/onGuildCreate.event.ts +++ b/src/events/onGuildCreate.event.ts @@ -1,12 +1,30 @@ import { BotEvent } from '../DiscordTypes.js'; -import { Client, Guild } from 'discord.js'; +import { + Client, + Guild, + GuildBasedChannel, + GuildTextBasedChannel, + PermissionsBitField +} from 'discord.js'; import { getOrCreateGuildSettings } from '../schemas/SchemaGuild.js'; import { Events } from 'discord.js'; +import { CheckBotPermissions } from '../utilities/checkPermissions.js'; +import { generateNewGuildEmbed } from '../utilities/generateNewGuildEmbed.js'; const event: BotEvent = { name: Events.GuildCreate, execute: async (client: Client, guild: Guild) => { await getOrCreateGuildSettings(guild.id); + + // Send a welcome message + const channel: GuildBasedChannel | undefined = guild.channels.cache.find( + (channel) => + // Channel type zero is Text channel + channel.type === 0 && CheckBotPermissions(channel, [PermissionsBitField.Flags.SendMessages]) + ); + + if (!channel) return; + (channel as GuildTextBasedChannel).send({ embeds: [generateNewGuildEmbed()] }); } }; diff --git a/src/handlers/Command.handler.ts b/src/handlers/Command.handler.ts index dc0a7b5..b0bdd92 100644 --- a/src/handlers/Command.handler.ts +++ b/src/handlers/Command.handler.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import '../DiscordTypes.js'; import getDirName from '../utilities/getDirName.js'; import { ENV } from '../EnvironmentVariables.js'; +import * as process from 'node:process'; export const loggerPrefixCommandHandler = 'Commands'; @@ -38,6 +39,11 @@ const handler = async (client: Client) => { const group: ICommandGroup = command.group; + if (commands.has(command.text_data.name)) { + loggerError(`Duplicate command name: ${command.text_data.name}`, loggerPrefixCommandHandler); + process.exit(1); + } + commands.set(command.text_data.name, command); if (!commandsGroups.has(group.name)) { diff --git a/src/handlersLoad.ts b/src/handlersLoad.ts index cf96252..1445096 100644 --- a/src/handlersLoad.ts +++ b/src/handlersLoad.ts @@ -6,7 +6,7 @@ import { ENV } from './EnvironmentVariables.js'; import path from 'path'; import { pathToFileURL } from 'url'; -const loggerPrefixHandlersManager = 'handlers'; +const loggerPrefixHandlersManager = 'Handlers'; export async function handlersLoad(client: Client): Promise { try { const handlersDir = path.join(getDirName(import.meta.url), 'handlers'); diff --git a/src/locales/Locale.ts b/src/locales/Locale.ts index 00af756..512cde5 100644 --- a/src/locales/Locale.ts +++ b/src/locales/Locale.ts @@ -15,12 +15,13 @@ export default async function loadLocale() { fallbackLng: 'en', ns: [ 'mongodb', - 'commandshandlers', + 'commandsHandlers', 'commands', 'commandsGroups', 'general', 'permissions', - 'audioplayer' + 'audioplayer', + 'welcomeMessage' ], backend: { loadPath: join(getDirName(import.meta.url), '../locales/{{lng}}/{{ns}}.json') diff --git a/src/locales/en/commands.json b/src/locales/en/commands.json index 3851883..04d826b 100644 --- a/src/locales/en/commands.json +++ b/src/locales/en/commands.json @@ -74,5 +74,8 @@ "lyrics_embed_lyrics_not_found": "Lyrics not found", "247_desc": "Toggle 24/7 mode. If enabled, bot will stay in a channel when no remain songs to play.", "247_enabled": "Mode 24/7 is activated", - "247_disabled": "Mode 24/7 is disabled" + "247_disabled": "Mode 24/7 is disabled", + "history_desc": "View the history of last played songs/playlists", + "history_embed_no_songs": "No songs have been played on this server yet, be the first who play the song", + "history_embed_title": "Songs history for server" } diff --git a/src/locales/en/commandshandlers.json b/src/locales/en/commandsHandlers.json similarity index 84% rename from src/locales/en/commandshandlers.json rename to src/locales/en/commandsHandlers.json index d41b030..658dfe1 100644 --- a/src/locales/en/commandshandlers.json +++ b/src/locales/en/commandsHandlers.json @@ -1,6 +1,4 @@ { - "text_command_error": "Error when executing text command", - "slash_command_error": "Error when executing slash command", "bot_not_enough_permissions_1": "BOT has not enough permissions on this server or channel", "bot_not_enough_permissions_2": "Write /help (command name), to see the missing permissions", "bot_not_enough_permissions_3": "And also ask the server administration to give them to the bot", diff --git a/src/locales/en/welcomeMessage.json b/src/locales/en/welcomeMessage.json new file mode 100644 index 0000000..60c1f96 --- /dev/null +++ b/src/locales/en/welcomeMessage.json @@ -0,0 +1,7 @@ +{ + "title": "Hello everyone!", + "row_1": "My main purpose is alcotest, to check yourself write /alcotest", + "row_2": "All jokes aside, I'm an advanced music bot that can play music from almost anywhere.", + "row_3": "Write /play (slash command) or {{prefix}}play (text command) to begin **magic**", + "row_4": "If you have any questions, write /help" +} diff --git a/src/locales/ru/commands.json b/src/locales/ru/commands.json index 2ad0b31..c68836c 100644 --- a/src/locales/ru/commands.json +++ b/src/locales/ru/commands.json @@ -74,5 +74,8 @@ "lyrics_embed_lyrics_not_found": "Текст песни не найден", "247_desc": "Переключает 24/7 режим. Если включено, бот остаётся в канале, даже когда закончились песни.", "247_enabled": "Режим 24/7 включён", - "247_disabled": "Режим 24/7 отключён" + "247_disabled": "Режим 24/7 отключён", + "history_desc": "Просмотр истории последних сыгранных песен и плейлистов", + "history_embed_no_songs": "На этом сервере ещё не было отыграно ещё ни одной песни, станьте первым!", + "history_embed_title": "История песен для сервера" } diff --git a/src/locales/ru/commandshandlers.json b/src/locales/ru/commandsHandlers.json similarity index 100% rename from src/locales/ru/commandshandlers.json rename to src/locales/ru/commandsHandlers.json diff --git a/src/locales/ru/welcomeMessage.json b/src/locales/ru/welcomeMessage.json new file mode 100644 index 0000000..6725af0 --- /dev/null +++ b/src/locales/ru/welcomeMessage.json @@ -0,0 +1,7 @@ +{ + "title": "Всем привет!", + "row_1": "Моя основная функция это алкотест, чтобы проверить себя напишите /alcotest", + "row_2": "А если отбросить шутки, то я продвинутый музыкальный бот который умеет играть музыку почти откуда угодно", + "row_3": "Напиши /play (слеш команда) или {{prefix}}play (текстовая команда) чтобы началась **магия**", + "row_4": "Если у тебя есть вопросы, то напиши /help" +} diff --git a/src/main.ts b/src/main.ts index 343d30f..193d235 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,15 @@ import { clientIntents } from './ClientIntents.js'; - -loggerSend(`Starting bot on version ${process.env.npm_package_version}`); - import { Client, Partials } from 'discord.js'; import { loggerError, loggerSend } from './utilities/logger.js'; import { loginBot } from './utilities/loginBot.js'; import { AudioPlayersManager } from './audioplayer/AudioPlayersManager.js'; import loadLocale from './locales/Locale.js'; +import { handlersLoad } from './handlersLoad.js'; +import { LoadPlugins } from './audioplayer/LoadPlugins.js'; -await loadLocale(); +loggerSend(`Starting bot on version ${process.env.npm_package_version}`); -import { handlersLoad } from './handlersLoad.js'; +await loadLocale(); const client = new Client({ intents: clientIntents, @@ -21,7 +20,7 @@ client.rest.on('rateLimited', (args) => { loggerError(`Client encountered a rate limit: ${JSON.stringify(args)}`); }); -new AudioPlayersManager(client); +new AudioPlayersManager(client, await LoadPlugins()); await handlersLoad(client); diff --git a/src/schemas/SchemaGuild.ts b/src/schemas/SchemaGuild.ts index 96f95b2..e128751 100644 --- a/src/schemas/SchemaGuild.ts +++ b/src/schemas/SchemaGuild.ts @@ -1,17 +1,17 @@ -import { Document, model, Schema } from 'mongoose'; +import mongoose, { Document, model, Schema } from 'mongoose'; import { ENV } from '../EnvironmentVariables.js'; -import { ISchemaSongsHistory } from './SchemaSongsHistory.js'; +import { deleteGuildSongsHistory, SongsHistoryListModelClass } from './SchemaSongsHistory.js'; interface GuildOptions { prefix: string; leaveOnEmpty: boolean; voiceStatus: boolean; - songsHistory: ISchemaSongsHistory; } interface ISchemaGuild extends Document { guildID: string; options: GuildOptions; + songsHistory: SongsHistoryListModelClass; } const SchemaGuild = new Schema({ @@ -19,12 +19,13 @@ const SchemaGuild = new Schema({ options: { prefix: { type: String, default: ENV.BOT_COMMAND_PREFIX }, leaveOnEmpty: { type: Boolean, default: true } - } + }, + songsHistory: { type: mongoose.Schema.Types.ObjectId, ref: 'songHistory', required: false } }); const GuildModel = model('guild', SchemaGuild); -class GuildModelClass extends GuildModel {} // This workaround required for better TypeScript support +export class GuildModelClass extends GuildModel {} // This workaround required for better TypeScript support export async function getOrCreateGuildSettings(guildID: string): Promise { const guild = await GuildModelClass.findOne({ guildID }); @@ -38,6 +39,7 @@ export async function getOrCreateGuildSettings(guildID: string): Promise { const guild: GuildModelClass = await getOrCreateGuildSettings(guildID); + await deleteGuildSongsHistory(guildID); await guild?.deleteOne(); } diff --git a/src/schemas/SchemaSongsHistory.ts b/src/schemas/SchemaSongsHistory.ts index 6b889a0..79e2f24 100644 --- a/src/schemas/SchemaSongsHistory.ts +++ b/src/schemas/SchemaSongsHistory.ts @@ -1,24 +1,83 @@ import { model, Schema } from 'mongoose'; +import { getOrCreateGuildSettings, GuildModelClass } from './SchemaGuild.js'; +import { Playlist, Song } from 'distube'; +import { ENV } from '../EnvironmentVariables.js'; -interface ISongHistoryUnit { +interface ISchemaSongHistoryUnit { name: string; - timestamp: Date; requester: string; + url: string; + createdAt?: Date; } -const SchemaSongsHistoryUnit = new Schema({ - name: String, - timestamp: Date, - requester: String -}); +const SchemaSongsHistoryUnit = new Schema( + { + name: { type: String, required: true }, + requester: { type: String, required: true }, + url: { type: String, required: true } + }, + { + timestamps: { + createdAt: true + } + } +); export interface ISchemaSongsHistory { - songsHistory: Array; + songsHistory: Array; } -export const SchemaSongsHistoryList = new Schema({ - songsHistory: { type: [SchemaSongsHistoryUnit], default: [] } -}); +export const SchemaSongsHistoryList = new Schema( + { + songsHistory: { type: [SchemaSongsHistoryUnit], default: [] } + }, + { + timestamps: { + createdAt: true, + updatedAt: true + } + } +); -const SongsHistoryListModel = model('song_history', SchemaSongsHistoryList); -class SongsHistoryListModelClass extends SongsHistoryListModel {} +const SongsHistoryListModel = model('songHistory', SchemaSongsHistoryList); + +export class SongsHistoryListModelClass extends SongsHistoryListModel {} // This workaround required for better TypeScript support + +export async function getOrCreateGuildSongsHistory(guildID: string) { + const guild: GuildModelClass = await getOrCreateGuildSettings(guildID); + if (guild.songsHistory) return SongsHistoryListModel.findOne({ _id: guild.songsHistory }); + const newHistory = new SongsHistoryListModelClass(); + await newHistory.save(); + + guild.set({ songsHistory: newHistory._id }); + await guild.save(); + return newHistory; +} + +export async function deleteGuildSongsHistory(guildID: string) { + const guild: GuildModelClass = await getOrCreateGuildSettings(guildID); + await SongsHistoryListModelClass.deleteOne({ _id: guild.songsHistory }); +} + +export async function addSongToGuildSongsHistory( + guildID: string, + resource: Song | Playlist +): Promise { + const history = await getOrCreateGuildSongsHistory(guildID); + + if (!history) return; + + if (resource.name && resource.member?.id && resource.url) { + history.songsHistory.push({ + name: resource.name ?? 'unknown', + requester: resource.member?.id ?? 'unknown', + url: resource.url + }); + } + + if (history.songsHistory.length > ENV.BOT_MAX_SONGS_HISTORY_SIZE) { + history.songsHistory.shift(); + } + + await history.save(); +} diff --git a/src/utilities/checkMemberInVoiceWithBot.ts b/src/utilities/checkMemberInVoiceWithBot.ts index 7ced171..5e99f10 100644 --- a/src/utilities/checkMemberInVoiceWithBot.ts +++ b/src/utilities/checkMemberInVoiceWithBot.ts @@ -20,7 +20,7 @@ export async function checkMemberInVoiceWithBot( return response; } } else { - response.errorMessage = i18next.t('commandshandlers:voice_join_in_any_channel'); + response.errorMessage = i18next.t('commandsHandlers:voice_join_in_any_channel'); return response; } @@ -29,7 +29,7 @@ export async function checkMemberInVoiceWithBot( .then((channel) => { if (channel) { if (channel instanceof VoiceChannel) { - response.errorMessage = `${i18next.t('commandshandlers:voice_join_in_channel')} ${channel.name}`; + response.errorMessage = `${i18next.t('commandsHandlers:voice_join_in_channel')} ${channel.name}`; } } }); diff --git a/src/utilities/generateNewGuildEmbed.ts b/src/utilities/generateNewGuildEmbed.ts new file mode 100644 index 0000000..93dd695 --- /dev/null +++ b/src/utilities/generateNewGuildEmbed.ts @@ -0,0 +1,24 @@ +import { Colors, EmbedBuilder } from 'discord.js'; +import { ENV } from '../EnvironmentVariables.js'; +import i18next from 'i18next'; + +export function generateNewGuildEmbed(): EmbedBuilder { + return new EmbedBuilder() + .setTitle(i18next.t('welcomeMessage:title')) + .setDescription( + i18next.t('welcomeMessage:row_1') + + '\n\n' + + i18next.t('welcomeMessage:row_2') + + '\n\n' + + i18next.t('welcomeMessage:row_3', { + prefix: ENV.BOT_COMMAND_PREFIX, + interpolation: { escapeValue: false } + }) + + '\n\n' + + i18next.t('welcomeMessage:row_4') + ) + .setColor(Colors.Yellow) + .setImage( + 'https://github.com/AlexInCube/AlCoTest/blob/master/icons/repository-social.png?raw=true' + ); +} diff --git a/wiki/Setup.md b/wiki/Setup.md index 45f07a8..54a4c20 100644 --- a/wiki/Setup.md +++ b/wiki/Setup.md @@ -14,27 +14,28 @@ Also you need retrieve token, client id and enable intents on Discord Developer - (Optional) To get SoundCloud token, follow the [Soundcloud](https://github.com/AlexInCube/AlCoTest/wiki/API-Configure#soundcloud-optional) section. - (Optional) To get Genius token, follow the [Genius](https://github.com/AlexInCube/AlCoTest/wiki/API-Configure#genius-optional) section. -| Name | Example | Description | Required | -|------------------------------|-----------------------|---------------------------------------------------------------------------|----------| -| `BOT_VERBOSE_LOGGING` | false | The bot will give more info to the console, useful for debugging | ❌ | -| `BOT_FFMPEG_LOGGING=false` | false | The bot will give info about FFMPEGto the console, useful for debugging | | -| `BOT_COMMAND_PREFIX` | // | Used only for text commands | ✔️ | -| `BOT_MAX_SONGS_IN_QUEUE` | 500 | Define max songs count per queue | ❌ | -| `BOT_LANGUAGE` | en | Supported values: en ru | ❌ | -| `MONGO_URI` | mongodb://mongo:27017 | The public key for sending notifications | ✔️ | -| `MONGO_DATABASE_NAME` | aicbot | Database name in MongoDB | ✔️ | -| `BOT_DISCORD_TOKEN` | ODEzNzUwMTY1N... | Token from Discord Developer Portal | ✔️ | -| `BOT_DISCORD_CLIENT_ID` | 813750165783... | Application ID from Discord Developer Portal | ✔️ | -| `BOT_DISCORD_OVERPOWERED_ID` | 29016845994426.... | Discord bot owner user ID, required for having more bot control for owner | ✔️ | -| `BOT_GOOGLE_EMAIL` | | Used to automate cookies fetching for YouTube | ❌ | -| `BOT_GOOGLE_PASSWORD` | | Used to automate cookies fetching for YouTube ❌ | | -| `BOT_SPOTIFY_CLIENT_SECRET` | | Used when the Spotify module cannot get the credentials automatically | ❌ | -| `BOT_SPOTIFY_CLIENT_ID` | | Used when the Spotify module get the credentials automatically | ❌ | -| `BOT_YANDEXMUSIC_TOKEN` | | Provide to enable Yandex Music module | ❌ | -| `BOT_YANDEXMUSIC_UID` | | Provide to enable Yandex Music module | ❌ | -| `BOT_SOUNDCLOUD_CLIENT_ID` | | Provide to fetch more data with SoundCloud Go+ account | ❌ | -| `BOT_SOUNDCLOUD_TOKEN` | | Provide to fetch more data with SoundCloud Go+ account | ❌ | -| `BOT_GENIUS_TOKEN` | | Provide to fetch songs lyrics from Genius | | +| Name | Example | Description | Required | +|------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------|----------| +| `BOT_VERBOSE_LOGGING` | false | The bot will give more info to the console, useful for debugging | ❌ | +| `BOT_FFMPEG_LOGGING` | false | The bot will give info about FFMPEGto the console, useful for debugging | ❌ | +| `BOT_COMMAND_PREFIX` | // | Used only for text commands | ✔️ | +| `BOT_MAX_SONGS_IN_QUEUE` | 500 | Define max songs count per queue | ❌ | +| `BOT_MAX_SONGS_HISTORY_SIZE` | 60 | Define max songs history per guild, set to 0 if you want to disable history (this will not delete history in database which already exists) | ❌ | +| `BOT_LANGUAGE` | en | Supported values: en ru | ❌ | +| `MONGO_URI` | mongodb://mongo:27017 | The public key for sending notifications | ✔️ | +| `MONGO_DATABASE_NAME` | aicbot | Database name in MongoDB | ✔️ | +| `BOT_DISCORD_TOKEN` | ODEzNzUwMTY1N... | Token from Discord Developer Portal | ✔️ | +| `BOT_DISCORD_CLIENT_ID` | 813750165783... | Application ID from Discord Developer Portal | ✔️ | +| `BOT_DISCORD_OVERPOWERED_ID` | 29016845994426.... | Discord bot owner user ID, required for having more bot control for owner | ✔️ | +| `BOT_GOOGLE_EMAIL` | | Used to automate cookies fetching for YouTube | ❌ | +| `BOT_GOOGLE_PASSWORD` | | Used to automate cookies fetching for YouTube | ❌ | +| `BOT_SPOTIFY_CLIENT_SECRET` | | Used when the Spotify module cannot get the credentials automatically | ❌ | +| `BOT_SPOTIFY_CLIENT_ID` | | Used when the Spotify module get the credentials automatically | ❌ | +| `BOT_YANDEXMUSIC_TOKEN` | | Provide to enable Yandex Music module | ❌ | +| `BOT_YANDEXMUSIC_UID` | | Provide to enable Yandex Music module | ❌ | +| `BOT_SOUNDCLOUD_CLIENT_ID` | | Provide to fetch more data with SoundCloud Go+ account | ❌ | +| `BOT_SOUNDCLOUD_TOKEN` | | Provide to fetch more data with SoundCloud Go+ account | ❌ | +| `BOT_GENIUS_TOKEN` | | Provide to fetch songs lyrics from Genius | ❌ | # 🐋 Run in Docker (recommended)