Merge branch 'Development' into dependabot/npm_and_yarn/multi-fb6576e9f1

This commit is contained in:
Amane Serenetia 2025-02-11 12:59:12 +07:00 committed by GitHub
commit 67b237fd88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1702 additions and 978 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
node_modules
docs
.env.asli
config.js
# Logs
logs

View File

@ -49,32 +49,24 @@ module.exports = {
MIN_BEG_AMOUNT: 1000, // minimum coins to be received when beg command is used
MAX_BEG_AMOUNT: 25000, // maximum coins to be received when beg command is used
},
MUSIC: {
ENABLED: true,
IDLE_TIME: 120, // Time in seconds before the bot disconnects from an idle voice channel
DEFAULT_VOLUME: 60, // Default player volume 1-100
MAX_SEARCH_RESULTS: 100,
DEFAULT_VOLUME: 100, // Default volume for the music player (0-100)
DEFAULT_SOURCE: "ytsearch", // ytsearch = Youtube, ytmsearch = Youtube Music, spsearch = Spotify, scsearch = SoundCloud
// Lavalink Websocket configuration
LAVALINK_WS: {
clientName: "Tinasha-Bot", // The name of the lavalink client.
resuming: true, // Whether Lavalink should attempt to resume existing sessions when reconnecting.
reconnecting: {
tries: Infinity, // Number of times to attempt reconnecting.
delay: 20000 // Delay
}
},
DEFAULT_SOURCE: "ytsearch", // ytsearch = Youtube, ytmsearch = Youtube Music, scsearch = SoundCloud, spsearch = Spotify
// Add any number of lavalink nodes here
// Refer to https://github.com/lavalink-devs/Lavalink to host your own lavalink server
LAVALINK_NODES: [
{
info: {
host: "localhost",
port: 2333,
auth: "youshallnotpass",
secure: false,
},
identifier: "Lavalink",
id: "Local Node",
host: "localhost",
port: 2333,
authorization: "youshallnotpass",
secure: false,
retryAmount: 20, // Number of reconnection attempts
retryDelay: 30000 // Delay (in ms) between reconnection attempts
},
],
},

View File

@ -12,5 +12,6 @@
"@structures/*": ["./src/structures/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "**/node_modules/*"]
}

198
package-lock.json generated
View File

@ -1,18 +1,16 @@
{
"name": "TinashaBot",
"version": "5.6.0",
"version": "5.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "TinashaBot",
"version": "5.6.0",
"version": "5.8.0",
"license": "ISC",
"dependencies": {
"@google/generative-ai": "^0.14.0",
"@lavaclient/plugin-queue": "^0.0.1",
"@lavaclient/queue": "^2.1.1",
"@lavaclient/spotify": "^3.1.0",
"@lavaclient/types": "^2.1.1",
"@vitalets/google-translate-api": "^9.2.0",
"common-tags": "^1.8.2",
@ -24,14 +22,14 @@
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"enhanced-ms": "^2.3.0",
"express": "^4.21.1",
"express": "^4.21.2",
"express-session": "^1.18.1",
"fixedsize-map": "^1.0.1",
"iso-639-1": "^3.1.0",
"lavaclient": "^5.0.0-rc.3",
"lavalink-client": "^2.4.0",
"module-alias": "^2.2.3",
"moment": "^2.30.1",
"mongoose": "^8.1.1",
"mongoose": "^8.9.5",
"nekos.life": "^3.0.0",
"pino": "^8.18.0",
"pino-pretty": "^10.3.1",
@ -192,13 +190,15 @@
"version": "0.17.6",
"resolved": "https://registry.npmjs.org/@effect/data/-/data-0.17.6.tgz",
"integrity": "sha512-/vwz7Jh05eS0qY8kczR/YyJd18d0C+PMtUkAealh4f6gwvhABLGCnktNJTcq/+UHxY0Cbv18r5uaJ4+7PPC+WQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@effect/schema": {
"version": "0.49.4",
"resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.49.4.tgz",
"integrity": "sha512-Em5qFV7kXfHpt6n89B2Zwd0ccGgfFpZbBAfQuGPdw/zY18k01Tl3ufKfBA6fFphKQiWrU6JS9btTlq1+/WRRIg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"effect": "2.0.0-next.56",
"fast-check": "^3.13.2"
@ -343,30 +343,6 @@
"lavalink-protocol": "1.0.1"
}
},
"node_modules/@lavaclient/queue": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@lavaclient/queue/-/queue-2.1.1.tgz",
"integrity": "sha512-2I+T+4xpb6I4xh4GjCCY/F2zazoHw/RectbKWlBZYSEMGoIGbHfOu81yi4fModigSVG0+4br8NnwHLiQX+XNcg==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.4.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@lavaclient/spotify": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@lavaclient/spotify/-/spotify-3.1.0.tgz",
"integrity": "sha512-B9AwZVyxScjJnJWJa4zMylF2i2/UOvDKL7lHWMxcezBMvOqjM+rZMr4ZTJj179qdpOaQ7zc8mBfRYSXGWJIWmA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.4.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@lavaclient/types": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@lavaclient/types/-/types-2.1.1.tgz",
@ -374,10 +350,9 @@
"license": "MIT"
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz",
"integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==",
"license": "MIT",
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
@ -821,10 +796,9 @@
}
},
"node_modules/bson": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz",
"integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==",
"license": "Apache-2.0",
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz",
"integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==",
"engines": {
"node": ">=16.20.1"
}
@ -1082,11 +1056,10 @@
"license": "ISC"
},
"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==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -1598,9 +1571,10 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -1621,7 +1595,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@ -1636,6 +1610,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-session": {
@ -1685,6 +1663,15 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/express/node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -2475,6 +2462,7 @@
"resolved": "https://registry.npmjs.org/lavaclient/-/lavaclient-5.0.0-rc.3.tgz",
"integrity": "sha512-zSB0wK9SEhXwWd918ije4UjjqMeDlP7Nry8mo5UFvQlD2YMFKxJJuKTJKc9VwgBhBjRVB0cCujAlru7NsTRdDg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@effect/schema": "^0.49.4",
"lavalink-api-client": "1.0.1",
@ -2492,6 +2480,7 @@
"resolved": "https://registry.npmjs.org/lavalink-api-client/-/lavalink-api-client-1.0.1.tgz",
"integrity": "sha512-ur4rrOf68mEKQQ6SwDdzG/8W+ew6zpguLPxny14DjEGxDz/mXvDtILovXpFRR5S4d8Ko9xZygIJ9YqEQfgKC/w==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.6.0"
},
@ -2507,6 +2496,7 @@
"resolved": "https://registry.npmjs.org/lavalink-protocol/-/lavalink-protocol-1.0.2.tgz",
"integrity": "sha512-OLd4ZDrYeT35UxJY1K/Pu6PjqPnHN0X5HutcQ7E/UNz+3YBvieaki9z0gk08gPwsxkBhYjRKDNIVNTAT0sH0fA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@effect/data": "0.17.6",
"@effect/schema": "^0.49.4",
@ -2521,6 +2511,7 @@
"resolved": "https://registry.npmjs.org/lavalink-ws-client/-/lavalink-ws-client-1.0.1.tgz",
"integrity": "sha512-jXntoMJe/lk4vuO3RVfSVQPNx28CwammqP4I3+pi5uIF/WHBAJxzwyIzDCmUPIIXcuEveUH53bs2aE8uCq9Sig==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.6.0",
"typed-emitter": "^2.1.0",
@ -2534,6 +2525,20 @@
"lavalink-protocol": "1.0.2"
}
},
"node_modules/lavalink-client": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/lavalink-client/-/lavalink-client-2.4.1.tgz",
"integrity": "sha512-wrRkbzILdjRzadAbdGM9O3bgLEabAebGci2YOUCcQoCumEjAMPz+rj7Xepa+y0oydnodyYZvBrB9ZeSdy7rzuw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.7.0",
"ws": "^8.18.0"
},
"engines": {
"bun": ">=1.0.0",
"node": ">=18.0.0"
}
},
"node_modules/lavalink-protocol": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lavalink-protocol/-/lavalink-protocol-1.0.1.tgz",
@ -2621,8 +2626,7 @@
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"license": "MIT"
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
@ -2716,14 +2720,12 @@
}
},
"node_modules/mongodb": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz",
"integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==",
"license": "Apache-2.0",
"peer": true,
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0",
"@mongodb-js/saslprep": "^1.1.9",
"bson": "^6.10.1",
"mongodb-connection-string-url": "^3.0.0"
},
"engines": {
@ -2731,7 +2733,7 @@
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0",
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
@ -2773,14 +2775,13 @@
}
},
"node_modules/mongoose": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.4.1.tgz",
"integrity": "sha512-odQ2WEWGL3hb0Qex+QMN4eH6D34WdMEw7F1If2MGABApSDmG9cMmqv/G1H6WsXmuaH9mkuuadW/WbLE5+tHJwA==",
"license": "MIT",
"version": "8.9.5",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz",
"integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==",
"dependencies": {
"bson": "^6.7.0",
"bson": "^6.10.1",
"kareem": "2.6.3",
"mongodb": "6.6.2",
"mongodb": "~6.12.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
@ -2794,52 +2795,6 @@
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose/node_modules/mongodb": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz",
"integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0",
"mongodb-connection-string-url": "^3.0.0"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongoose/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3203,9 +3158,10 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/phin": {
"version": "3.7.1",
@ -3603,6 +3559,7 @@
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@ -3927,7 +3884,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
@ -4156,9 +4112,9 @@
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/twemoji-parser": {
@ -4210,6 +4166,7 @@
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
"license": "MIT",
"peer": true,
"optionalDependencies": {
"rxjs": "*"
}
@ -4362,9 +4319,10 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "TinashaBot",
"version": "5.6.0",
"version": "5.8.0",
"description": "multipurpose discord bot built using discord-js",
"main": "bot.js",
"author": "Amane",
@ -24,8 +24,6 @@
"dependencies": {
"@google/generative-ai": "^0.14.0",
"@lavaclient/plugin-queue": "^0.0.1",
"@lavaclient/queue": "^2.1.1",
"@lavaclient/spotify": "^3.1.0",
"@lavaclient/types": "^2.1.1",
"@vitalets/google-translate-api": "^9.2.0",
"common-tags": "^1.8.2",
@ -37,14 +35,14 @@
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"enhanced-ms": "^2.3.0",
"express": "^4.21.1",
"express": "^4.21.2",
"express-session": "^1.18.1",
"fixedsize-map": "^1.0.1",
"iso-639-1": "^3.1.0",
"lavaclient": "^5.0.0-rc.3",
"lavalink-client": "^2.4.0",
"module-alias": "^2.2.3",
"moment": "^2.30.1",
"mongoose": "^8.1.1",
"mongoose": "^8.9.5",
"nekos.life": "^3.0.0",
"pino": "^8.18.0",
"pino-pretty": "^10.3.1",

View File

@ -3,6 +3,7 @@ const { EMBED_COLORS, SUPPORT_SERVER, DASHBOARD } = require("@root/config");
const { timeformat } = require("@helpers/Utils");
const os = require("os");
const { stripIndent } = require("common-tags");
const packageJson = require("@root/package.json");
/**
* @param {import('@structures/BotClient')} client
@ -29,7 +30,7 @@ module.exports = (client) => {
const overallUsage = `${Math.floor(((os.totalmem() - os.freemem()) / os.totalmem()) * 100)}%`;
let desc = "";
desc += `❒ Bot Version: 5.6.0\n`;
desc += `❒ Bot Version: ${packageJson.version}\n`;
desc += `❒ Bot Maker: <@334307216926703616>\n`;
desc += `❒ Bot Manager: Tinasha Bot Development Team\n`;
desc += `❒ Total guilds: ${guilds}\n`;

View File

@ -0,0 +1,51 @@
const { musicValidations } = require("@helpers/BotUtils");
const { autoplayFunction } = require("@handlers/player");
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "autoplay",
description: "Toggle autoplay feature for music player",
category: "MUSIC",
validations: musicValidations,
command: {
enabled: true,
aliases: ["ap"],
usage: "",
},
slashCommand: {
enabled: true,
options: [],
},
async messageRun(message, args) {
const response = await toggleAutoplay(message);
await message.safeReply(response);
},
async interactionRun(interaction) {
const response = await toggleAutoplay(interaction);
await interaction.followUp(response);
},
};
async function toggleAutoplay({ client, guildId }) {
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "🚫 No song is currently playing";
}
const autoplayState = player.get("autoplay");
if (autoplayState) {
player.set("autoplay", false);
return "Autoplay deactivated";
}
player.set("autoplay", true);
autoplayFunction(client, player.queue.current, player);
return "Autoplay activated!";
}

View File

@ -1,23 +1,18 @@
const { musicValidations } = require("@helpers/BotUtils");
const { ApplicationCommandOptionType } = require("discord.js");
const levels = {
none: 0.0,
low: 0.1,
medium: 0.15,
high: 0.25,
};
const { EQList } = require("lavalink-client");
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "bassboost",
description: "set bassboost level",
description: "Set bassboost level",
category: "MUSIC",
validations: musicValidations,
command: {
enabled: true,
aliases: ["bb"],
minArgsCount: 1,
usage: "<none|low|medium|high>",
},
@ -30,48 +25,64 @@ module.exports = {
type: ApplicationCommandOptionType.String,
required: true,
choices: [
{
name: "none",
value: "none",
},
{
name: "low",
value: "low",
},
{
name: "medium",
value: "medium",
},
{
name: "high",
value: "high",
},
{ name: "High", value: "high" },
{ name: "Medium", value: "medium" },
{ name: "Low", value: "low" },
{ name: "Off", value: "off" },
],
},
],
},
async messageRun(message, args) {
let level = "none";
if (args.length && args[0].toLowerCase() in levels) level = args[0].toLowerCase();
const response = setBassBoost(message, level);
let level = "off";
if (args.length) {
const input = args[0].toLowerCase();
if (["high", "medium", "low", "off"].includes(input)) {
level = input;
}
}
const response = await setBassBoost(message, level);
await message.safeReply(response);
},
async interactionRun(interaction) {
let level = interaction.options.getString("level");
const response = setBassBoost(interaction, level);
const response = await setBassBoost(interaction, level);
await interaction.followUp(response);
},
};
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
* @param {number} level
* @param {string} level
*/
function setBassBoost({ client, guildId }, level) {
const player = client.musicManager.players.resolve(guildId);
const bands = new Array(3).fill(null).map((_, i) => ({ band: i, gain: levels[level] }));
player.setFilters(...bands);
async function setBassBoost({ client, guildId }, level) {
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "🚫 No song is currently playing";
}
// Clear any existing EQ
await player.filterManager.clearEQ();
switch (level) {
case "high":
await player.filterManager.setEQ(EQList.BassboostHigh);
break;
case "medium":
await player.filterManager.setEQ(EQList.BassboostMedium);
break;
case "low":
await player.filterManager.setEQ(EQList.BassboostLow);
break;
case "off":
await player.filterManager.clearEQ();
break;
default:
return "Invalid bassboost level";
}
return `> Set the bassboost level to \`${level}\``;
}

View File

@ -0,0 +1,61 @@
const { EmbedBuilder } = require("discord.js");
const Queue = require("@src/database/schemas/Queue");
const { MUSIC } = require("@root/config");
module.exports = {
name: "join",
description: "Join the voice channel and resume playback if there is a saved queue.",
category: "MUSIC",
validations: [], // Add any validations if necessary
command: {
enabled: true,
},
slashCommand: {
enabled: true,
},
async messageRun(message, args) {
const response = await executeJoinCommand(message);
await message.safeReply(response);
},
async interactionRun(interaction) {
const response = await executeJoinCommand(interaction);
await interaction.followUp(response);
},
};
async function executeJoinCommand(interaction) {
const { guild, member } = interaction;
const voiceChannel = member.voice.channel;
if (!voiceChannel) {
return "You need to be in a voice channel to use this command.";
}
// Get or create the player for the guild
let player = guild.client.musicManager.getPlayer(guild.id);
if (!player) {
player = await guild.client.musicManager.createPlayer({
guildId: guild.id,
voiceChannelId: voiceChannel.id,
textChannelId: interaction.channel.id,
selfMute: false,
selfDeaf: true,
volume: MUSIC.DEFAULT_VOLUME,
});
}
if (!player.connected) await player.connect()
// Check for saved queue in the database
const restore = await Queue.restore(player)
if (restore) {
await player.play({ paused: false });
return `Joined ${voiceChannel.name} and resumed playback of **${player.queue.current.info.title}**.`;
} else {
return `Joined ${voiceChannel.name}.`
}
}

View File

@ -0,0 +1,41 @@
const { musicValidations } = require("@helpers/BotUtils");
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "leave",
description: "Disconnects the bot from the voice channel",
category: "MUSIC",
validations: musicValidations,
command: {
enabled: true,
aliases: ["dc"],
minArgsCount: 0,
usage: "",
},
slashCommand: {
enabled: true,
options: [],
},
async messageRun(message, args) {
const response = await leave(message);
await message.safeReply(response);
},
async interactionRun(interaction) {
const response = await leave(interaction);
await interaction.followUp(response);
},
};
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
async function leave({ client, guildId, member }) {
const player = client.musicManager.getPlayer(guildId);
player.destroy();
return "👋 Disconnected from the voice channel";
}

View File

@ -1,5 +1,4 @@
const { musicValidations } = require("@helpers/BotUtils");
const { LoopType } = require("@lavaclient/plugin-queue");
const { ApplicationCommandOptionType } = require("discord.js");
/**
@ -12,8 +11,9 @@ module.exports = {
validations: musicValidations,
command: {
enabled: true,
aliases: ["lp"],
minArgsCount: 1,
usage: "<queue|track>",
usage: "<queue|track|off>",
},
slashCommand: {
enabled: true,
@ -21,17 +21,12 @@ module.exports = {
{
name: "type",
type: ApplicationCommandOptionType.String,
description: "The entity you want to loop",
description: "Select loop type",
required: false,
choices: [
{
name: "queue",
value: "queue",
},
{
name: "track",
value: "track",
},
{ name: "Track", value: "track" },
{ name: "Queue", value: "queue" },
{ name: "Off", value: "off" },
],
},
],
@ -39,34 +34,47 @@ module.exports = {
async messageRun(message, args) {
const input = args[0].toLowerCase();
const type = input === "queue" ? "queue" : "track";
const response = toggleLoop(message, type);
const type = input === "queue" ? "queue" : input === "track" ? "track" : "off";
const response = await toggleLoop(message, type);
await message.safeReply(response);
},
async interactionRun(interaction) {
const type = interaction.options.getString("type") || "track";
const response = toggleLoop(interaction, type);
const response = await toggleLoop(interaction, type);
await interaction.followUp(response);
},
};
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
* @param {"queue"|"track"} type
* @param {"queue"|"track"|"off"} type
*/
function toggleLoop({ client, guildId }, type) {
const player = client.musicManager.players.resolve(guildId);
async function toggleLoop({ client, guildId }, type) {
const player = client.musicManager.getPlayer(guildId);
// track
if (type === "track") {
player.queue.setLoop(LoopType.Song);
return "Loop mode is set to `track`";
if (!player || !player.queue.current) {
return "🚫 No song is currently playing";
}
// queue
else if (type === "queue") {
player.queue.setLoop(LoopType.Queue);
return "Loop mode is set to `queue`";
switch (type) {
case "track":
player.setRepeatMode("track");
return "Loop mode is set to `track`";
case "queue":
if (player.queue.tracks.length > 1) {
player.setRepeatMode("queue");
return "Loop mode is set to `queue`";
} else {
return "🚫 Queue is too short to be looped";
}
case "off":
player.setRepeatMode("off");
return "Loop mode is disabled";
default:
return "Invalid loop type";
}
}

View File

@ -1,5 +1,5 @@
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { MESSAGES, EMBED_COLORS } = require("@root/config");
const { EMBED_COLORS } = require("@root/config");
/**
* @type {import("@structures/Command")}
@ -38,116 +38,94 @@ module.exports = {
},
};
/**
* @param {import('discord.js').Message} param0
* @param {string} query
*/
async function getLyric({ client, guild, member }, query) {
/** @type {Response} */
let res;
/** @type {import('../../handlers/manager')} */
const manager = client.musicManager
const player = manager.getPlayer(guild.id)
let node = player?.node
if (!node) [node] = manager.nodeManager.nodes.values()
let track;
if (!query) {
// query not specified - use currently playing music
track = player?.queue.current;
if (!track) {
return "🚫 No music is currently playing!";
}
} else {
// query specified -- search
const result = await node.search({ query }, member.user);
if (!result || result.loadType === "error" || result.loadType === "empty") {
return "Failed to find any tracks for the query.";
}
track = result.tracks[0];
}
// lavalyrics
try {
const lyrics = await node.request(`/lyrics?track=${encodeURIComponent(track.encoded)}&skipTrackSource=${true}`)
if (lyrics && 'text' in lyrics) return createLyricsEmbed(lyrics, member, track)
else console.log(`Failed search LavaLyrics track ${track.info.title}`, lyrics)
} catch (e) {
console.error(`Error search LavaLyrics track ${track.info.title}`, e)
}
// javatimed
if (node.info.plugins.includes('java-timed-lyrics')) {
// java-timed (current)
if (!query) {
/** @type { import('lavaclient').Player } */
const player = client.musicManager.players.resolve(guild.id);
if (!player?.track) return "🚫 There's no active music player in this server.";
// Fetch lyrics for the current track
const api = player.node.api;
res = await api.client.execute({ path: `/v4/sessions/${player.api.session.id}/players/${guild.id}/lyrics`, method: 'GET' })
} else {
/** @type {import('lavaclient').ClusterNode} */
const [[, node]] = client.musicManager.nodes
const api = node.api
// Search for lyrics
res = await api.client.execute({ path: `/v4/lyrics/search?query=${encodeURIComponent(query)}&source=genius`, method: 'GET' })
try {
const lyrics = await getCurrentLyricJavaTimed(node, guild.id)
if (lyrics && 'text' in lyrics) return createLyricsEmbed(lyrics, member, track)
else console.log(`Failed search JavaTimed (current) track ${track.info.title}`, lyrics)
} catch (e) {
console.error(`Error search JavaTimed (current) track ${track.info.title}`, e)
}
}
// throws error anyway
if (res.status === 404) return "Lyric not found"
if (!res.ok) throw new Error(`Failed to search lyrics: ${res.status} ${res.statusText}`);
const lyricData = await res.json()
return createLyricsEmbed(lyricData, member);
} catch (error) {
client.logger.error("Lyric Command Error:", error);
return "An error occurred while fetching the lyrics. Please try again later.";
// java-timed (search)
try {
const lyrics = await searchLyricJavaTimed(node, guild.id)
if (lyrics.lines) {
for (const line of lyrics.lines) {
line.timestamp = line.range.start
line.duration = line.range.end - line.range.start
}
}
if (lyrics && 'text' in lyrics) return createLyricsEmbed(lyrics, member, track)
else console.log(`Failed search JavaTimed (search) track ${track.info.title}`, lyrics)
} catch (e) {
console.error(`Error search JavaTimed (search) track ${track.info.title}`, e)
}
}
return "No lyrics found"
}
/**
* @param {LyricResult} lyrics
* @param {import('discord.js').GuildMember} member
* @returns
*/
function createLyricsEmbed(lyrics, member) {
if (!lyrics) {
return "No lyrics found for this song.";
}
const track = lyrics.track
function createLyricsEmbed(lyrics, member, track) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setTitle(`${track.author} - ${track.title}`)
.setThumbnail(track.albumArt[0].url)
.setFooter({ text: `Requested by: ${member.user.displayName} | Source: ${lyrics.source || 'Unknown'}` });
.setTitle(`${track.info.author} - ${track.info.title}`)
.setThumbnail(track.info.artworkUrl)
.setFooter({ text: `Requested by: ${member.user.displayName} | Source: ${lyrics.provider || lyrics.source || lyrics.sourceName || '-'}` });
const ltext = lyrics.type === 'text' ? lyrics.text : lyrics.lines.map(v => v.line).join('\n')
embed.setDescription(ltext.length > 4096 ? ltext.slice(0, 4093) + "..." : ltext);
const lines = !lyrics.lines ? lyrics.text ?? 'No lyrics found' : lyrics.lines
.filter(Boolean)
.map(line => line.line)
.join("\n");
const truncatedLyrics = lines.length > 4096 ? `${lines.slice(0, 4093)}...` : lines;
embed.setDescription(truncatedLyrics);
return { embeds: [embed] };
}
/**
* @typedef LyricResult
* @type {LyricResultBase & (LyricResultText | LyricResultTimed)}
*/
function getCurrentLyricJavaTimed(node, guildId) {
return node.request(`/sessions/${node.sessionId}/players/${guildId}/lyrics`)
}
/**
* @typedef LyricResultBase
* @type {object}
* @property {LyricResultTrack} track
* @property {string} source
*/
/**
* @typedef LyricResultText
* @type {object}
* @property {'text'} type
* @property {string} text
*/
/**
* @typedef LyricResultTimed
* @type {object}
* @property {'time'} type
* @property {LyricResultLinePart[]} lines
*/
/**
* @typedef LyricResultLinePart
* @type {object}
* @property {string} line
* @property {number} start
* @property {number} end
*/
/**
* @typedef LyricResultTrack
* @type {object}
* @property {string} title
* @property {string} author
* @property {string | null} album
* @property {LyricResultTrackAlbumArt[]} albumArt
*/
/**
* @typedef LyricResultTrackAlbumArt
* @type {object}
* @property {string} url
* @property {number} width
* @property {number} height
*/
function searchLyricJavaTimed(node, track, source = "genius") {
return node.request(`/lyrics/search?query=${encodeURIComponent(track.info.title + " " + track.info.author)}&source=${source}`)
}

View File

@ -1,13 +1,13 @@
const { EMBED_COLORS } = require("@root/config");
const { EmbedBuilder } = require("discord.js");
const { formatTime } = require("@helpers/Utils");
const { splitBar } = require("string-progressbar");
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "np",
description: "show's what track is currently being played",
description: "Shows what track is currently being played",
category: "MUSIC",
botPermissions: ["EmbedLinks"],
command: {
@ -18,7 +18,7 @@ module.exports = {
enabled: true,
},
async messageRun(message) {
async messageRun(message, args) {
const response = nowPlaying(message);
await message.safeReply(response);
},
@ -32,35 +32,38 @@ module.exports = {
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
function nowPlaying({ client, guildId, member }) {
const player = client.musicManager.players.resolve(guildId);
if (!player || !player.queue.current) return "🚫 No music is being played!";
function nowPlaying({ client, guildId }) {
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "🚫 No music is being played!";
}
const track = player.queue.current;
const totalLength = track.info.length;
const position = player.position;
const progress = Math.round((position / totalLength) * 15);
const progressBar = `${formatTime(position)} [${"▬".repeat(
progress)}🔘${"▬".repeat(15 - progress)}] ${formatTime(totalLength)}`;
const end = track.info.isStream ? "Live" : client.utils.formatTime(track.info.duration);
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setAuthor({ name: "Now playing" })
.setAuthor({ name: "Now Playing" })
.setDescription(`[${track.info.title}](${track.info.uri})`)
.addFields(
{
name: "Song Duration",
value: `\`${formatTime(track.info.length)}\``,
value: client.utils.formatTime(track.info.duration),
inline: true,
},
{
name: "Requested By",
value: track.requesterId ? track.requesterId : member.user.displayName,
value: track.requester.displayName || "Unknown",
inline: true,
},
{
name: "\u200b",
value: progressBar,
value:
client.utils.formatTime(player.position) +
" [" +
splitBar(track.info.isStream ? player.position : track.info.duration, player.position, 15)[0] +
"] " +
end,
inline: false,
}
);

View File

@ -5,7 +5,7 @@ const { musicValidations } = require("@helpers/BotUtils");
*/
module.exports = {
name: "pause",
description: "pause the music player",
description: "Pause the music player",
category: "MUSIC",
validations: musicValidations,
command: {
@ -16,12 +16,12 @@ module.exports = {
},
async messageRun(message, args) {
const response = pause(message);
const response = await pause(message);
await message.safeReply(response);
},
async interactionRun(interaction) {
const response = pause(interaction);
const response = await pause(interaction);
await interaction.followUp(response);
},
};
@ -29,10 +29,17 @@ module.exports = {
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
function pause({ client, guildId }) {
const player = client.musicManager.players.resolve(guildId);
if (player.paused) return "The player is already paused.";
async function pause({ client, guildId }) {
const player = client.musicManager.getPlayer(guildId);
player.pause(true);
return "⏸️ Paused the music player.";
if (!player || !player.queue.current) {
return "🚫 No song is currently playing";
}
if (player.paused) {
return "The player is already paused";
}
player.pause();
return "⏸️ Paused the music player";
}

View File

@ -1,24 +1,52 @@
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { formatTime } = require("@helpers/Utils");
require("@lavaclient/plugin-queue")
const { EMBED_COLORS, MUSIC } = require("@root/config");
const Queue = require("@src/database/schemas/Queue");
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "play",
description: "play a song",
description: "Play or queue your favorite song!",
category: "MUSIC",
botPermissions: ["EmbedLinks"],
command: {
enabled: true,
usage: "<song-name>",
aliases: ["p"],
usage: "[source:Default/Youtube Music/Soundcloud/Spotify/Deezer] <song-name>",
minArgsCount: 1,
},
slashCommand: {
enabled: true,
options: [
{
name: "source",
description: "select the source to search from",
type: ApplicationCommandOptionType.String,
required: true,
choices: [
{
name: "Default",
value: "ytsearch",
},
{
name: "Youtube Music",
value: "ytmsearch",
},
{
name: "Soundcloud",
value: "scsearch",
},
{
name: "Spotify",
value: "spsearch",
},
{
name: "Deezer",
value: "dzsearch",
},
],
},
{
name: "query",
description: "song name or url",
@ -29,14 +57,23 @@ module.exports = {
},
async messageRun(message, args) {
const query = args.join(" ");
const response = await play(message, query);
let source = MUSIC.DEFAULT_SOURCE;
// Check if the first argument is a valid source
const validSources = ["ytsearch", "ytmsearch", "scsearch", "spsearch", "dzsearch"];
if (validSources.includes(args[0].toLowerCase())) {
source = args.shift().toLowerCase();
}
const searchQuery = args.join(" ");
const response = await play(message, searchQuery, source);
await message.safeReply(response);
},
async interactionRun(interaction) {
const query = interaction.options.getString("query");
const response = await play(interaction, query);
const source = interaction.options.getString("source") || MUSIC.DEFAULT_SOURCE;
const searchQuery = interaction.options.getString("query");
const response = await play(interaction, searchQuery, source);
await interaction.followUp(response);
},
};
@ -45,134 +82,110 @@ module.exports = {
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
* @param {string} query
*/
async function play({ member, guild, channel }, query) {
async function play({ member, guild, channel }, searchQuery, source) {
if (!member.voice.channel) return "🚫 You need to join a voice channel first";
let player = guild.client.musicManager.players.resolve(guild.id);
if (player && !guild.members.me.voice.channel) {
player.voice.disconnect();
await guild.client.musicManager.players.destroy(guild.id);
}
let player = guild.client.musicManager.getPlayer(guild.id);
if (player && member.voice.channel !== guild.members.me.voice.channel) {
return "🚫 You must be in the same voice channel as mine";
}
let embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED);
let tracks;
let description = "";
let thumbnail;
if (!player) {
player = await guild.client.musicManager.createPlayer({
guildId: guild.id,
voiceChannelId: member.voice.channel.id,
textChannelId: channel.id,
selfMute: false,
selfDeaf: true,
volume: MUSIC.DEFAULT_VOLUME,
});
}
if (!player.connected) await player.connect();
try {
const res = await guild.client.musicManager.api.loadTracks(
/^https?:\/\//.test(query) ? query : `${MUSIC.DEFAULT_SOURCE}:${query}`
);
const res = await player.search({ query: searchQuery, source: source }, member.user);
let track;
if (!res || res.loadType === "empty") {
return `🚫 No results found for "${searchQuery}"`;
}
switch (res.loadType) {
switch (res?.loadType) {
case "error":
guild.client.logger.error("Search Exception", res.data);
guild.client.logger.error("Search Exception", res.exception);
return "🚫 There was an error while searching";
case "empty":
return `No results found matching ${query}`;
case "playlist": {
player.queue.add(res.tracks);
case "playlist":
tracks = res.data.tracks;
description = res.data.info.name;
thumbnail = res.data.pluginInfo.artworkUrl;
break;
await Queue.save(player);
const playlistEmbed = new EmbedBuilder()
.setAuthor({ name: "Added Playlist to queue" })
.setThumbnail(res.playlist.thumbnail)
.setColor(EMBED_COLORS.BOT_EMBED)
.setDescription(`[${res.playlist.name}](${res.playlist.uri})`)
.addFields(
{ name: "Enqueued", value: `${res.tracks.length} songs`, inline: true },
{
name: "Playlist duration",
value:
"`" +
guild.client.utils.formatTime(res.tracks.map((t) => t.info.duration).reduce((a, b) => a + b, 0)) +
"`",
inline: true,
}
)
.setFooter({ text: `Requested By: ${member.user.displayName}` });
if (!player.playing && player.queue.tracks.length > 0) {
await player.play({ paused: false });
}
return { embeds: [playlistEmbed] };
}
case "track":
track = res.data;
tracks = [track];
break;
case "search": {
const track = res.tracks[0];
player.queue.add(track);
case "search":
track = res.data[0];
tracks = [track];
break;
await Queue.save(player);
const trackEmbed = new EmbedBuilder()
.setAuthor({ name: "Added Track to queue" })
.setColor(EMBED_COLORS.BOT_EMBED)
.setDescription(`[${track.info.title}](${track.info.uri})`)
.setThumbnail(track.info.artworkUrl)
.addFields({
name: "Song Duration",
value: "`" + guild.client.utils.formatTime(track.info.duration) + "`",
inline: true,
})
.setFooter({ text: `Requested By: ${track.requester.displayName}` });
if (player.queue?.tracks?.length > 1) {
trackEmbed.addFields({
name: "Position in Queue",
value: player.queue.tracks.length.toString(),
inline: true,
});
}
if (!player.playing && player.queue.tracks.length > 0) {
await player.play({ paused: false });
}
return { embeds: [trackEmbed] };
}
default:
guild.client.logger.debug("Unknown loadType", res);
return "🚫 An error occurred while searching for the song";
}
if (!tracks) guild.client.logger.debug({ query, res });
} catch (error) {
guild.client.logger.error("Search Exception", typeof error === "object" ? JSON.stringify(error) : error);
guild.client.logger.error("Search Exception", JSON.stringify(error));
return "🚫 An error occurred while searching for the song";
}
if (!tracks) return "🚫 An error occurred while searching for the song";
if (tracks.length === 1) {
const track = tracks[0];
if (!player?.playing && !player?.paused && !player?.queue.tracks.length) {
embed.setAuthor({ name: "Added Track to queue" });
} else {
const fields = [];
embed
.setAuthor({ name: "Added Track to queue" })
.setDescription(`[${track.info.title}](${track.info.uri})`)
.setThumbnail(track.info.artworkUrl)
.setFooter({ text: `Requested By: ${member.user.displayName}` });
fields.push({
name: "Song Duration",
value: "`" + formatTime(track.info.length) + "`",
inline: true,
});
if (player?.queue?.tracks?.length > 0) {
fields.push({
name: "Position in Queue",
value: (player.queue.tracks.length + 1).toString(),
inline: true,
});
}
embed.addFields(fields);
}
} else {
embed
.setAuthor({ name: "Added Playlist to queue" })
.setThumbnail(thumbnail)
.setDescription(description)
.addFields(
{
name: "Enqueued",
value: `${tracks.length} songs`,
inline: true,
},
{
name: "Playlist duration",
value:
"`" +
formatTime(
tracks.map((t) => t.info.length).reduce((a, b) => a + b, 0),
) +
"`",
inline: true,
}
)
.setFooter({ text: `Requested By: ${member.user.displayName}` });
}
// create a player and/or join the member's vc
if (!player?.connected) {
player = guild.client.musicManager.players.create(guild.id);
player.queue.data.channel = channel;
player.voice.connect(member.voice.channel.id, { deafened: true });
player.setVolume(MUSIC.DEFAULT_VOLUME);
}
// do queue things
const started = player.playing || player.paused;
player.queue.add(tracks, { requester: member.user.displayName, next: false });
if (!started) {
await player.queue.start();
}
return { embeds: [embed] };
}
}

View File

@ -1,6 +1,5 @@
const { EMBED_COLORS } = require("@root/config");
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { formatTime } = require("@helpers/Utils");
/**
* @type {import("@structures/Command")}
@ -12,6 +11,7 @@ module.exports = {
botPermissions: ["EmbedLinks"],
command: {
enabled: true,
aliases: ["q"],
usage: "[page]",
},
slashCommand: {
@ -44,46 +44,39 @@ module.exports = {
* @param {number} pgNo
*/
async function getQueue({ client, guild }, pgNo) {
const player = client.musicManager.players.resolve(guild.id);
if (!player) return "🚫 There is no music playing in this guild.";
const queue = player.queue;
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setAuthor({ name: `Queue for ${guild.name}` });
// change for the amount of tracks per page
const multiple = 10;
const page = pgNo || 1;
const end = page * multiple;
const start = end - multiple;
const tracks = queue.tracks.slice(start, end);
if (queue.current) {
const currentTrack = queue.current;
embed.addFields({
name: "Current",
value: `[${currentTrack.info.title}](${currentTrack.info.uri}) \`[${formatTime(currentTrack.info.length)}]\``
});
const player = client.musicManager.getPlayer(guild.id);
if (!player || !player.queue.current) {
return "🚫 No song is currently playing";
}
const queueList = tracks.map((track, index) => {
const title = track.info.title;
const uri = track.info.uri;
const duration = formatTime(track.info.length);
return `${start + index + 1}. [${title}](${uri}) \`[${duration}]\``;
});
const embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED).setAuthor({ name: `Queue for ${guild.name}` });
if (!queueList.length) {
embed.setDescription(`No tracks in ${page > 1 ? `page ${page}` : "the queue"}.`);
} else {
embed.setDescription(queueList.join("\n"));
const end = (pgNo || 1) * 10;
const start = end - 10;
const tracks = player.queue.tracks.slice(start, end);
if (player.queue.current) {
const current = player.queue.current;
embed
.addFields({
name: "Current",
value: `[${current.info.title}](${current.info.uri}) \`[${client.utils.formatTime(current.info.duration)}]\``,
})
.setThumbnail(current.info.artworkUrl);
}
const maxPages = Math.ceil(queue.tracks.length / multiple);
embed.setFooter({ text: `Page ${page > maxPages ? maxPages : page} of ${maxPages}` });
const queueList = tracks.map(
(track, index) =>
`${start + index + 1}. [${track.info.title}](${track.info.uri}) \`[${client.utils.formatTime(track.info.duration)}]\``
);
embed.setDescription(
queueList.length ? queueList.join("\n") : `No tracks in ${pgNo > 1 ? `page ${pgNo}` : "the queue"}.`
);
const maxPages = Math.ceil(player.queue.tracks.length / 10);
embed.setFooter({ text: `Page ${pgNo > maxPages ? maxPages : pgNo} of ${maxPages}` });
return { embeds: [embed] };
}

View File

@ -5,7 +5,7 @@ const { musicValidations } = require("@helpers/BotUtils");
*/
module.exports = {
name: "resume",
description: "resumes the music player",
description: "Resumes the music player",
category: "MUSIC",
validations: musicValidations,
command: {
@ -16,12 +16,12 @@ module.exports = {
},
async messageRun(message, args) {
const response = resumePlayer(message);
const response = await resumePlayer(message);
await message.safeReply(response);
},
async interactionRun(interaction) {
const response = resumePlayer(interaction);
const response = await resumePlayer(interaction);
await interaction.followUp(response);
},
};
@ -29,9 +29,15 @@ module.exports = {
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
function resumePlayer({ client, guildId }) {
const player = client.musicManager.players.resolve(guildId);
async function resumePlayer({ client, guildId }) {
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "🚫 No song is currently playing";
}
if (!player.paused) return "The player is already resumed";
player.resume();
return "▶️ Resumed the music player";
}

View File

@ -5,8 +5,8 @@ const {
ApplicationCommandOptionType,
ComponentType,
} = require("discord.js");
const { formatTime } = require("@helpers/Utils");
const { EMBED_COLORS, MUSIC } = require("@root/config");
const Queue = require("@src/database/schemas/Queue");
/**
* @type {import("@structures/Command")}
@ -18,12 +18,41 @@ module.exports = {
botPermissions: ["EmbedLinks"],
command: {
enabled: true,
usage: "<song-name>",
aliases: ["sc"],
usage: "[source:Default/Youtube Muisc/SoundCloud/Spotify/Deezer] <song-name>",
minArgsCount: 1,
},
slashCommand: {
enabled: true,
options: [
{
name: "source",
description: "select the source to search from",
type: ApplicationCommandOptionType.String,
required: true,
choices: [
{
name: "Default",
value: "ytsearch",
},
{
name: "Youtube Music",
value: "ytmsearch",
},
{
name: "Soundcloud",
value: "scsearch",
},
{
name: "Spotify",
value: "spsearch",
},
{
name: "Deezer",
value: "dzsearch",
},
],
},
{
name: "query",
description: "song to search",
@ -34,185 +63,127 @@ module.exports = {
},
async messageRun(message, args) {
let source = MUSIC.DEFAULT_SOURCE;
// Check if the first argument is a valid source
const validSources = ["ytsearch", "ytmsearch", "scsearch", "spsearch", "dzsearch"];
if (validSources.includes(args[0].toLowerCase())) {
source = args.shift().toLowerCase();
}
const query = args.join(" ");
const response = await search(message, query);
const response = await search(message, query, source);
if (response) await message.safeReply(response);
},
async interactionRun(interaction) {
const source = interaction.options.getString("source") || MUSIC.DEFAULT_SOURCE;
const query = interaction.options.getString("query");
const response = await search(interaction, query);
const response = await search(interaction, query, source);
if (response) await interaction.followUp(response);
else interaction.deleteReply();
},
};
/**
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} interaction
* @param {string} query
* @param {string} source
*/
async function search({ member, guild, channel }, query) {
async function search({ member, guild, channel }, query, source) {
if (!member.voice.channel) return "🚫 You need to join a voice channel first";
let player = guild.client.musicManager.players.resolve(guild.id);
if (player && !guild.members.me.voice.channel) {
player.voice.disconnect();
await guild.client.musicManager.players.destroy(guild.id);
}
let player = guild.client.musicManager.getPlayer(guild.id);
if (player && member.voice.channel !== guild.members.me.voice.channel) {
return "🚫 You must be in the same voice channel as mine";
return "🚫 You must be in the same voice channel as me.";
}
let res;
if (!player) {
player = await guild.client.musicManager.createPlayer({
guildId: guild.id,
voiceChannelId: member.voice.channel.id,
textChannelId: channel.id,
selfMute: false,
selfDeaf: true,
volume: MUSIC.DEFAULT_VOLUME,
});
}
if (!player.connected) await player.connect();
const res = await player.search({ query, source }, member.user);
if (!res || !res.tracks?.length) {
return {
embeds: [new EmbedBuilder().setColor(EMBED_COLORS.ERROR).setDescription(`No results found for \`${query}\``)],
};
}
let maxResults = MUSIC.MAX_SEARCH_RESULTS;
if (res.tracks.length < maxResults) maxResults = res.tracks.length;
const results = res.tracks.slice(0, maxResults);
const options = results.map((track, index) => ({
label: track.info.title.slice(0, 100),
value: index.toString(),
}));
const menuRow = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId("search-results")
.setPlaceholder("Choose Search Results")
.setMaxValues(1)
.addOptions(options)
);
const searchEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setAuthor({ name: "Search Results" })
.setDescription(`Select the song you wish to add to the queue`);
const searchMessage = await channel.send({
embeds: [searchEmbed],
components: [menuRow],
});
try {
res = await guild.client.musicManager.api.loadTracks(
/^https?:\/\//.test(query) ? query : `${MUSIC.DEFAULT_SOURCE}:${query}`
);
} catch (err) {
return "🚫 There was an error while searching";
}
const response = await channel.awaitMessageComponent({
filter: (i) => i.user.id === member.id && i.message.id === searchMessage.id,
componentType: ComponentType.StringSelect,
idle: 30 * 1000,
});
let embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED);
let tracks;
if (response.customId !== "search-results") return;
switch (res.loadType) {
case "error":
guild.client.logger.error("Search Exception", res.data);
return "🚫 There was an error while searching";
await searchMessage.delete();
if (!response) return "🚫 You took too long to select the songs";
case "empty":
return `No results found matching ${query}`;
const selectedTrack = results[response.values[0]];
player.queue.add(selectedTrack);
case "track": {
const [track] = res.data[0];
tracks = [track];
if (!player?.playing && !player?.paused && !player?.queue.tracks.length) {
embed.setAuthor({ name: "Added Song to queue" });
break;
}
// Save the current state to the database
await Queue.save(player);
const fields = [];
embed
.setAuthor({ name: "Added Song to queue" })
.setDescription(`[${track.info.title}](${track.info.uri})`)
.setFooter({ text: `Requested By: ${member.user.displayName}` });
fields.push({
const trackEmbed = new EmbedBuilder()
.setAuthor({ name: "Added Track to queue" })
.setDescription(`[${selectedTrack.info.title}](${selectedTrack.info.uri})`)
.setThumbnail(selectedTrack.info.artworkUrl)
.addFields({
name: "Song Duration",
value: "`" + formatTime(track.info.length) + "`",
value: "`" + guild.client.utils.formatTime(selectedTrack.info.duration) + "`",
inline: true,
});
})
.setFooter({ text: `Requested By: ${member.user.displayName}` });
if (player?.queue?.tracks?.length > 0) {
fields.push({
name: "Position in Queue",
value: (player.queue.tracks.length + 1).toString(),
inline: true,
});
}
embed.addFields(fields);
break;
if (!player.playing && player.queue.tracks.length > 0) {
await player.play({ paused: false });
}
case "playlist":
tracks = res.data.tracks;
embed
.setAuthor({ name: "Added Playlist to queue" })
.setDescription(res.data.info.name)
.addFields(
{
name: "Enqueued",
value: `${res.data.tracks.length} songs`,
inline: true,
},
{
name: "Playlist duration",
value:
"`" +
formatTime(
res.data.tracks.map((t) => t.info.length).reduce((a, b) => a + b, 0),
) +
"`",
inline: true,
}
)
.setFooter({ text: `Requested By: ${member.user.displayName}` });
break;
case "search": {
let max = guild.client.config.MUSIC.MAX_SEARCH_RESULTS;
if (res.data.length < max) max = res.data.length;
const results = res.data.slice(0, max);
const options = results.map((result, index) => ({
label: result.info.title,
value: index.toString(),
}));
const menuRow = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId("search-results")
.setPlaceholder("Choose Search Results")
.setMaxValues(max)
.addOptions(options)
);
const tempEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setAuthor({ name: "Search Results" })
.setDescription(`Please select the songs you wish to add to queue`);
const sentMsg = await channel.send({
embeds: [tempEmbed],
components: [menuRow],
});
try {
const response = await channel.awaitMessageComponent({
filter: (reactor) => reactor.message.id === sentMsg.id && reactor.user.id === member.id,
idle: 30 * 1000,
componentType: ComponentType.StringSelect,
});
await sentMsg.delete();
if (!response) return "🚫 You took too long to select the songs";
if (response.customId !== "search-results") return;
const toAdd = [];
response.values.forEach((v) => toAdd.push(results[v]));
// Only 1 song is selected
if (toAdd.length === 1) {
tracks = [toAdd[0]];
embed.setAuthor({ name: "Added Song to queue" });
} else {
tracks = toAdd;
embed
.setDescription(`🎶 Added ${toAdd.length} songs to queue`)
.setFooter({ text: `Requested By: ${member.user.displayName}` });
}
} catch (err) {
console.log(err);
await sentMsg.delete();
return "🚫 Failed to register your response";
}
break;
}
return { embeds: [trackEmbed] };
} catch (err) {
console.error("Error handling response:", err);
await searchMessage.delete();
return "🚫 Failed to register your response";
}
// create a player and/or join the member's vc
if (!player?.connected) {
player = guild.client.musicManager.players.create(guild.id);
player.queue.data.channel = channel;
player.voice.connect(member.voice.channel.id, { deafened: true });
}
// do queue things
const started = player.playing || player.paused;
player.queue.add(tracks, { requester: member.user.username, next: false });
if (!started) {
await player.queue.start();
}
return { embeds: [embed] };
}

View File

@ -1,6 +1,4 @@
const { musicValidations } = require("@helpers/BotUtils");
const prettyMs = require("pretty-ms");
const { durationToMillis } = require("@helpers/Utils");
const { ApplicationCommandOptionType } = require("discord.js");
/**
@ -8,7 +6,7 @@ const { ApplicationCommandOptionType } = require("discord.js");
*/
module.exports = {
name: "seek",
description: "sets the playing track's position to the specified position",
description: "Sets the position of the current track",
category: "MUSIC",
validations: musicValidations,
command: {
@ -20,7 +18,7 @@ module.exports = {
options: [
{
name: "time",
description: "The time you want to seek to.",
description: "The time you want to seek to",
type: ApplicationCommandOptionType.String,
required: true,
},
@ -28,14 +26,20 @@ module.exports = {
},
async messageRun(message, args) {
const time = args.join(" ");
const response = seekTo(message, time);
const time = message.client.utils.parseTime(args.join(" "));
if (!time) {
return await message.safeReply("Invalid time format. Use 10s, 1m 50s, 1h");
}
const response = await seekTo(message, time);
await message.safeReply(response);
},
async interactionRun(interaction) {
const time = interaction.options.getString("time");
const response = seekTo(interaction, time);
const time = interaction.client.utils.parseTime(interaction.options.getString("time"));
if (!time) {
return await interaction.followUp("Invalid time format. Use 10s, 1m 50s, 1h");
}
const response = await seekTo(interaction, time);
await interaction.followUp(response);
},
};
@ -44,14 +48,17 @@ module.exports = {
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
* @param {number} time
*/
function seekTo({ client, guildId }, time) {
const player = client.musicManager?.players.resolve(guildId);
const seekTo = durationToMillis(time);
async function seekTo({ client, guildId }, time) {
const player = client.musicManager.getPlayer(guildId);
if (seekTo > player.queue.current.info.length) {
return "The duration you provide exceeds the duration of the current track";
if (!player || !player.queue.current) {
return "🚫 There's no music currently playing";
}
player.seek(seekTo);
return `Seeked to ${prettyMs(seekTo, { colonNotation: true, secondsDecimalDigits: 0 })}`;
if (time > player.queue.current.length) {
return "The duration you provided exceeds the duration of the current track";
}
player.seek(time);
return `Seeked song duration to **${client.utils.formatTime(time)}**`;
}

View File

@ -30,7 +30,16 @@ module.exports = {
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
function shuffle({ client, guildId }) {
const player = client.musicManager.players.resolve(guildId);
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "🚫 There's no music currently playing";
}
if (player.queue.tracks.length < 2) {
return "🚫 Not enough tracks to shuffle";
}
player.queue.shuffle();
return "🎶 Queue has been shuffled";
}

View File

@ -5,12 +5,12 @@ const { musicValidations } = require("@helpers/BotUtils");
*/
module.exports = {
name: "skip",
description: "skip the current song",
description: "Skip the current song",
category: "MUSIC",
validations: musicValidations,
command: {
enabled: true,
aliases: ["next"],
aliases: ["next", "s"],
},
slashCommand: {
enabled: true,
@ -31,20 +31,18 @@ module.exports = {
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
async function skip({ client, guildId }) {
const player = client.musicManager.players.resolve(guildId);
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "There is no song currently being played.";
return "🚫 There's no music currently playing";
}
const title = player.queue.current.info.title;
// Check if there is a next song in the queue
if (player.queue.tracks.length === 0) {
return "There is no next song to skip to.";
}
if (player.queue.tracks.length === 0) {
return "There is no next song to skip to";
}
// Skip to the next song
player.queue.next();
return `⏯️ ${title} was skipped successfully.`;
}
await player.skip();
return `⏯️ ${title} was skipped`;
}

View File

@ -5,24 +5,24 @@ const { musicValidations } = require("@helpers/BotUtils");
*/
module.exports = {
name: "stop",
description: "stop the music player",
description: "Stop the music player",
category: "MUSIC",
validations: musicValidations,
command: {
enabled: true,
aliases: ["leave"],
aliases: [""],
},
slashCommand: {
enabled: true,
},
async messageRun(message, args) {
const response = await stop(message);
async messageRun(message, args, data) {
const response = await stop(message, data.settings);
await message.safeReply(response);
},
async interactionRun(interaction) {
const response = await stop(interaction);
async interactionRun(interaction, data) {
const response = await stop(interaction, data.settings);
await interaction.followUp(response);
},
};
@ -31,8 +31,17 @@ module.exports = {
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
async function stop({ client, guildId }) {
const player = client.musicManager.players.resolve(guildId);
player.voice.disconnect();
await client.musicManager.players.destroy(guildId);
return "🎶 The music player is stopped and queue has been cleared";
const player = client.musicManager.getPlayer(guildId);
if (!player || !player.queue.current) {
return "🚫 There's no music currently playing";
}
if (player.get("autoplay") === true) {
player.set("autoplay", false);
}
player.stopPlaying(true, false);
return "🎶 The music player is stopped, and the queue has been cleared";
}

View File

@ -11,14 +11,15 @@ module.exports = {
validations: musicValidations,
command: {
enabled: true,
usage: "<1-100>",
aliases: ["vol"],
usage: "<0-100>",
},
slashCommand: {
enabled: true,
options: [
{
name: "amount",
description: "Enter a value to set [1 to 100]",
description: "Enter a value to set [0 to 100]",
type: ApplicationCommandOptionType.Integer,
required: false,
},
@ -42,15 +43,19 @@ module.exports = {
* @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0
*/
async function getVolume({ client, guildId }, amount) {
const player = client.musicManager.players.resolve(guildId);
const player = client.musicManager.getPlayer(guildId);
if (!amount) return `> The player volume is \`${player.volume}\`.`;
if (!player || !player.queue.current) {
return "🚫 There's no music currently playing";
}
if (!amount) return `> The player volume is \`${player.volume}\``;
if (isNaN(amount) || amount < 0 || amount > 100) {
return "You need to give me a volume between 1 and 100.";
return "You need to give me a volume between 0 and 100";
}
// Set the player volume
await player.setVolume(amount);
return `🎶 Music player volume is set to \`${amount}\`.`;
return `🎶 Music player volume is set to \`${amount}\``;
}

View File

@ -1,9 +1,13 @@
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { EmbedBuilder, ApplicationCommandOptionType, AttachmentBuilder } = require("discord.js");
const { EMBED_COLORS } = require("@root/config");
// This dummy token will be replaced by the actual token
const DUMMY_TOKEN = "MY_TOKEN_IS_SECRET";
const AsyncFunctionConstructor = (async() => {}).constructor
let prev = null
let store = Object.create(null)
/**
* @type {import("@structures/Command")}
*/
@ -13,71 +17,113 @@ module.exports = {
category: "OWNER",
botPermissions: ["EmbedLinks"],
command: {
enabled: true,
enabled: process.env.DEV === 'true',
usage: "<script>",
minArgsCount: 1,
},
slashCommand: {
enabled: false,
enabled: process.env.DEV === 'true',
options: [
{
name: "expression",
description: "content to evaluate",
description: "Code to evaluate",
type: ApplicationCommandOptionType.String,
required: true,
required: false,
},
{
name: "attachment",
description: "Code to evaluate",
type: ApplicationCommandOptionType.Attachment,
required: false,
},
{
name: "async",
description: "Use async - requires return to retreive value",
type: ApplicationCommandOptionType.Boolean,
required: false
}
],
},
async messageRun(message, args) {
const input = args.join(" ");
async messageRun(message, args, data) {
let content = message.content.slice(data.prefix.length + data.invoke.length).trim()
if (!input) return message.safeReply("Please provide code to eval");
let response;
try {
const output = eval(input);
response = buildSuccessResponse(output, message.client);
} catch (ex) {
response = buildErrorResponse(ex);
if (content.startsWith('```')) {
content = content.replace(/^```(js)?/, '')
if (content.endsWith('```')) content = content.slice(0, -3)
}
await message.safeReply(response);
else if (content.startsWith('``')) {
content = content.slice(2, -2)
}
const attachment = message.attachments.at(0)
await message.reply(await execute(false, attachment, content, { message, args, data }, message.client));
},
async interactionRun(interaction) {
const input = interaction.options.getString("expression");
async interactionRun(interaction, data) {
const attachment = interaction.options.getAttachment("attachment")
const code = interaction.options.getString("expression")
const useAsync = interaction.options.getBoolean("async") ?? false
let response;
try {
const output = eval(input);
response = buildSuccessResponse(output, interaction.client);
} catch (ex) {
response = buildErrorResponse(ex);
}
await interaction.followUp(response);
await interaction.followUp(await execute(useAsync, attachment, code, { interaction, data }, interaction.client));
},
};
const buildSuccessResponse = (output, client) => {
async function execute(useAsync, attachment, code, context, client) {
if (!code) {
if (!attachment) return "Specify code or attachment to run"
if (attachment.contentType !== 'text/javascript') return "Attachment type must be JavaScript"
const res = await fetch(attachment.url)
if (!res.ok && !code) return "Failed to retreive attachment"
code = await res.text()
}
Object.assign(context, {
client,
store: store,
_: prev
})
try {
const exec = useAsync
? new AsyncFunctionConstructor('ctx', `with (ctx) {${code}}; return "return not set"`)
: new Function('ctx', `with (ctx) return eval(${JSON.stringify(code)})`)
const output = await exec.call(client, context)
prev = output
return buildResponse(output, client);
} catch (ex) {
return buildResponse(ex, client, true);
}
}
const buildResponse = (output, client, isError) => {
// Token protection
output = require("util").inspect(output, { depth: 0 }).replaceAll(client.token, DUMMY_TOKEN);
output = require("util").inspect(output, {
maxArrayLength: 5,
maxStringLength: 500,
depth: 3
}).replaceAll(client.token, DUMMY_TOKEN);
const embed = new EmbedBuilder()
.setAuthor({ name: "📤 Output" })
.setDescription("```js\n" + (output.length > 4096 ? `${output.substr(0, 4000)}...` : output) + "\n```")
.setColor("Random")
.setTimestamp(Date.now());
// use file
if (output.length > 200 || (output.match(/\n/g)?.length ?? 0) > 10) {
const file = new AttachmentBuilder(Buffer.from(output), { name: 'output.js' })
return { embeds: [embed] };
};
const buildErrorResponse = (err) => {
const embed = new EmbedBuilder();
embed
.setAuthor({ name: "📤 Error" })
.setDescription("```js\n" + (err.length > 4096 ? `${err.substr(0, 4000)}...` : err) + "\n```")
.setColor(EMBED_COLORS.ERROR)
.setTimestamp(Date.now());
return { embeds: [embed] };
return {
content: isError ? 'Error!' : 'Output:',
files: [file]
}
}
// use embed
else {
const embed = new EmbedBuilder()
.setAuthor({ name: isError ? "📤 Error" : "📤 Output" })
.setDescription("```js\n" + output + "\n```")
.setColor(isError ? EMBED_COLORS.ERROR : EMBED_COLORS.SUCCESS)
.setTimestamp(Date.now());
return { embeds: [embed] }
}
};

View File

@ -0,0 +1,120 @@
const path = require('path')
const { ApplicationCommandOptionType, ApplicationCommandType } = require('discord.js')
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "reloadcmd",
description: "Reloads commands",
category: "OWNER",
command: {
enabled: process.env.DEV === 'true',
usage: "<filename> [guild]",
minArgsCount: 1,
},
slashCommand: {
enabled: process.env.DEV === 'true',
options: [
{
name: "filename",
description: "File name to load. Reloads all if not specified",
type: ApplicationCommandOptionType.String,
required: true,
}, {
name: "guild",
description: "guild",
type: ApplicationCommandOptionType.String,
required: false,
}, {
name: "reregister",
description: "Reregisters command to manager",
type: ApplicationCommandOptionType.Boolean,
required: false,
},
],
},
async messageRun(message, args) {
await message.safeReply(await runReload(message.client, args[0], args[1]))
},
async interactionRun(interaction) {
await interaction.followUp(await runReload(
interaction.client,
interaction.options.getString("filename"),
interaction.options.getString("guild"),
interaction.options.getBoolean("reregister"),
))
}
};
async function runReload(client, filename, guildid, rereg = false) {
// guild id
let guild
if (guildid) {
guild = client.guilds.cache.get(guildid);
if (!guild) return "No matching guild"
}
// load
if (filename[0] === '/') {
const cmdname = filename.slice(1)
const cmd = client.commands.get(cmdname)
if (!cmd) return "Cannot find command with name " + cmd
if (!cmd.category) return `Command category ${cmd} is undefined`
filename = cmd.category.toLowerCase() + '/' + cmdname
}
if (!path.extname(filename)) filename += '.js'
const target = path.resolve('src', 'commands', path.normalize(filename))
const oldCmd = require.cache[target]?.exports
const oldSlashEnable = oldCmd?.slashCommand?.enabled
delete require.cache[target]
const newCmd = require(target)
const newSlashEnable = newCmd?.slashCommand?.enabled
if (rereg) {
// register / edit
const cmdManager = guild ? guild.commands : client.application.commands
const slashCmd = {
name: newCmd.name,
description: newCmd.description,
type: ApplicationCommandType.ChatInput,
options: newCmd.slashCommand?.options
}
// slash commands
if (oldSlashEnable) {
const oldSlash = client.application.commands.cache.find(v => v.name === oldCmd.name)
if (oldSlash) {
if (newSlashEnable) await cmdManager.edit(oldSlash, slashCmd)
else await cmdManager.delete(oldSlash)
}
}
else if (newSlashEnable) {
await cmdManager.create(slashCmd)
}
}
// remove old
if (oldCmd?.command?.enabled) {
client.commands.delete(oldCmd.name.toLowerCase())
for (const alias of oldCmd.command.aliases ?? []) client.commandAlias.delete(alias)
}
if (oldSlashEnable) {
client.slashCommands.delete(oldCmd.name);
}
// add new
if (newCmd?.command?.enabled) {
client.commands.set(newCmd.name.toLowerCase(), newCmd)
for (const alias of newCmd.command.aliases ?? []) client.commandAlias.set(alias, newCmd)
}
if (newSlashEnable) {
client.slashCommands.set(newCmd.name, newCmd);
}
return `Reloaded ${filename}`
}

View File

@ -310,7 +310,7 @@ function getMsgCategoryEmbeds(client, category, prefix) {
// For REMAINING Categories
const commands = client.commands.filter((cmd) => cmd.category === category);
if (commands.length === 0) {
if (commands.size === 0) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setThumbnail(CommandCategory[category]?.image)
@ -323,8 +323,9 @@ function getMsgCategoryEmbeds(client, category, prefix) {
const arrSplitted = [];
const arrEmbeds = [];
while (commands.length) {
let toAdd = commands.splice(0, commands.length > CMDS_PER_PAGE ? CMDS_PER_PAGE : commands.length);
const cmdArr = Array.from(commands.values())
while (cmdArr.length) {
let toAdd = cmdArr.splice(0, cmdArr.size > CMDS_PER_PAGE ? CMDS_PER_PAGE : cmdArr.size);
toAdd = toAdd.map((cmd) => `\`${prefix}${cmd.name}\`\n ${cmd.description}\n`);
arrSplitted.push(toAdd);
}

View File

@ -6,7 +6,7 @@ const { byteUnit } = require('@root/src/helpers/Units');
module.exports = {
name: "nodes",
description: "Lists lavalink nodes",
description: "Lists Audio nodes",
category: "UTILITY",
command: {
enabled: true,
@ -41,13 +41,13 @@ async function fetchNodesAndTurnItToTable(nodes) {
const res = await fetchNodeStat(node).then(HttpUtil.assertStatusOk)
stat = await res.json()
} catch(e) {
error(node.identifier, e)
error(node.id, e)
}
if (stat) {
const { uptime, memory: { free, reservable }, cpu: { lavalinkLoad }, playingPlayers, players } = stat
return {
name: `:green_circle: ${node.identifier}`,
name: `:green_circle: ${node.id}`,
value: [
` **Uptime**: ${Utils.formatTime(uptime)}`,
` **Memory**: ${byteUnit(free)} (${(free / reservable * 100).toPrecision(3)}%) of ${byteUnit(reservable)}`,
@ -57,27 +57,27 @@ async function fetchNodesAndTurnItToTable(nodes) {
}
} else {
return {
name: `:red_circle: ${node.identifier}`,
name: `:red_circle: ${node.id}`,
value: 'Node is offline'
}
}
}))
return new EmbedBuilder({
title: 'Lavalink Nodes',
title: 'Audio Nodes',
color: 0x0088ff,
fields: fields
})
}
async function fetchNodeStat(node, connTimeout = 5000) {
const baseUrl = (node.info.port !== 80 && node.info.secure)
? `https://${node.info.host}:${node.info.port}`
: `http://${node.info.host}:${node.info.port}`;
const baseUrl = (node.port !== 80 && node.secure)
? `https://${node.host}:${node.port}`
: `http://${node.host}:${node.port}`;
return HttpUtil.fetchWithConnTimeout(
`${baseUrl}/v4/stats`,
{ headers: { Authorization: node.info.auth } },
{ headers: { Authorization: node.authorization } },
connTimeout
);
}

View File

@ -1,123 +0,0 @@
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { MESSAGES, EMBED_COLORS } = require("@root/config");
const fetch = require('node-fetch');
const subscriptions = new Map(); // Store subscriptions in memory (consider using the mongodb or maybe gonna use redis)
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "subscribe",
description: "Subscribe to YouTube or TikTok notifications.",
category: "UTILITY",
botPermissions: ["SendMessages"],
command: {
enabled: true,
usage: "<platform> <channel/user_id>",
},
slashCommand: {
enabled: true,
options: [
{
name: "platform",
description: "The platform to subscribe to (youtube/tiktok)",
type: ApplicationCommandOptionType.String,
required: true,
choices: [
{ name: "YouTube", value: "youtube" },
{ name: "TikTok", value: "tiktok" },
],
},
{
name: "channel_or_user_id",
description: "The YouTube channel ID or TikTok user ID",
type: ApplicationCommandOptionType.String,
required: true,
},
],
},
async messageRun(message, args) {
const platform = args[0].toLowerCase();
const channelOrUserId = args[1];
const response = await subscribe(message.channel, platform, channelOrUserId);
await message.safeReply(response);
},
async interactionRun(interaction) {
const platform = interaction.options.getString("platform");
const channelOrUserId = interaction.options.getString("channel_or_user_id");
const response = await subscribe(interaction.channel, platform, channelOrUserId);
await interaction.followUp(response);
},
};
/**
* Subscribe to notifications for a specific platform and channel/user.
* @param {import("discord.js").TextChannel} channel
* @param {string} platform
* @param {string} channelOrUserId
*/
async function subscribe(channel, platform, channelOrUserId) {
const key = `${platform}:${channelOrUserId}`;
if (subscriptions.has(key)) {
return "🚫 You are already subscribed to this channel/user.";
}
subscriptions.set(key, channel.id);
return `✅ Successfully subscribed to ${platform} updates for ${channelOrUserId}.`;
}
/**
* Function to check for updates (YouTube and TikTok)
* This function should be called periodically (e.g., using setInterval)
*/
async function checkForUpdates() {
for (const [key, channelId] of subscriptions.entries()) {
const [platform, id] = key.split(":");
let updateMessage = "";
if (platform === "youtube") {
const updates = await fetchYouTubeUpdates(id);
if (updates) {
updateMessage = `New YouTube update for ${id}: ${updates}`;
}
} else if (platform === "tiktok") {
const updates = await fetchTikTokUpdates(id);
if (updates) {
updateMessage = `New TikTok update for ${id}: ${updates}`;
}
}
if (updateMessage) {
const channel = await channelId.fetch();
channel.send(updateMessage);
}
}
}
/**
* Fetch updates from YouTube (placeholder function)
* @param {string} channelId
*/
async function fetchYouTubeUpdates(channelId) {
// Implement your logic to fetch updates from YouTube
// Return the update message or null if no updates
return null; // Placeholder
}
/**
* Fetch updates from TikTok (placeholder function)
* @param {string} userId
*/
async function fetchTikTokUpdates(userId) {
// Implement your logic to fetch updates from TikTok
// Return the update message or null if no updates
return null; // Placeholder
}
// Set an interval to check for updates every 10 minutes (600000 ms)
setInterval(checkForUpdates, 600000);

View File

@ -0,0 +1,120 @@
//@ts-check
const queue = require("@root/src/commands/music/queue");
const mongoose = require("mongoose");
const queueSchema = new mongoose.Schema({
guildId: { type: String, required: true, unique: true },
tracks: [
{
title: String,
uri: String,
duration: Number,
requester: String,
},
],
currentTrack: {
title: String,
uri: String,
duration: Number,
requester: String,
},
});
const QueueModel = mongoose.model("Queue", queueSchema)
module.exports = {
model: QueueModel,
async save(player) {
const queue = player.queue
const currentQueue = queue.current
const queueData = {
guildId: player.guildId,
tracks: queue.tracks.map(track => ({
title: track.info.title,
uri: track.info.uri,
duration: track.info.duration,
requester: track.requester.id,
})),
currentTrack: currentQueue?.info?.title ? {
title: currentQueue.info.title,
uri: currentQueue.info.uri,
duration: currentQueue.info.duration,
requester: currentQueue.requester.id,
} : null,
};
await QueueModel.findOneAndUpdate(
{ guildId: player.guildId },
queueData,
{ upsert: true, new: true }
)
},
async saveCurrentTrack(player, track) {
const queueData = {
guildId: player.guildId,
currentTrack: {
title: track.info.title,
uri: track.info.uri,
duration: track.info.duration,
requester: track.requester.id,
position: player.position || 0, // Save the current position
},
};
await QueueModel.findOneAndUpdate(
{ guildId: player.guildId },
queueData,
{ upsert: true, new: true }
);
},
async findGuild(guildOrPlayer) {
if ('guildId' in guildOrPlayer) guildOrPlayer = guildOrPlayer.guildId
return QueueModel.findOne({ guildId: guildOrPlayer });
},
async deleteGuild(guildOrPlayer) {
if ('guildId' in guildOrPlayer) guildOrPlayer = guildOrPlayer.guildId
return QueueModel.deleteOne({ guildId: guildOrPlayer });
},
/**
* @param {import('lavalink-client').Player} player
* @returns
*/
async restore(player) {
const queueData = await this.findGuild(player)
if (!queueData) return false
const { tracks, currentTrack } = queueData
player.queue.utils.save()
player.queue.add(tracks.map(track => ({
info: {
title: track.title,
uri: track.uri,
duration: track.duration,
},
requester: { id: track.requester },
})));
if (currentTrack?.uri) {
player.queue.current = {
info: {
title: currentTrack.title,
uri: currentTrack.uri,
duration: currentTrack.duration,
},
requester: { id: currentTrack.requester },
};
// Set the current position if available
player.position = currentTrack.position || 0; // Default to 0 if not set
}
return true
}
};

View File

@ -0,0 +1,11 @@
const joinCommand = require("@src/commands/music/join");
client.on("interactionCreate", async (interaction) => {
if (!interaction.isButton()) return;
const { customId } = interaction;
if (customId === "resume") {
await joinCommand.execute(interaction);
}
});

View File

@ -2,6 +2,7 @@ const { inviteHandler, greetingHandler } = require("@src/handlers");
const { getSettings } = require("@schemas/Guild");
const { AuditLogEvent } = require("discord.js");
const { EmbedBuilder } = require("discord.js");
/**
* @param {import('@src/structures').BotClient} client
* @param {import('discord.js').GuildMember|import('discord.js').PartialGuildMember} member
@ -34,13 +35,21 @@ module.exports = async (client, member) => {
if (possibleLog) {
if (settings.logging?.members) return;
const logChannel = client.channels.cache.get(settings.logging.members);
// Check if logChannel is defined
if (!logChannel) {
console.error(`Log channel not found for guild: ${guild.id}`);
return; // Exit if the log channel is not found
}
const embed = new EmbedBuilder()
.setAuthor({ name: "Member kicked" })
.setColor("Red")
.setTitle(`${member.displayName} (\`${member.id}\` was kicked.)`)
.setTitle(`${member.displayName} (\`${member.id}\`) was kicked.`)
.setDescription(`Reason: ${possibleLog.reason || "none"}`)
.setTimestamp()
.setFooter({ text: `ID: ${member.id} | Executor: ${possibleLog.executor.username}` })
await logChannel.send({ embeds: [embed] })
.setFooter({ text: `ID: ${member.id} | Executor: ${possibleLog.executor.username}` });
await logChannel.send({ embeds: [embed] });
}
};
};

View File

@ -0,0 +1,13 @@
module.exports = async (client, player) => {
const guild = client.guilds.cache.get(player.guildId);
if (!guild) return;
if (player.voiceChannelId) {
await client.utils.setVoiceStatus(client, player.voiceChannelId, "");
}
const msg = player.get("message");
if (msg && msg.deletable) {
await msg.delete().catch(() => {});
}
};

View File

@ -0,0 +1,38 @@
const Queue = require("@src/database/schemas/Queue");
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require("discord.js");
module.exports = async (client, player) => {
const guild = client.guilds.cache.get(player.guildId);
if (!guild) return;
await Queue.save(player)
if (player.voiceChannelId) {
await client.utils.setVoiceStatus(client, player.voiceChannelId, "");
}
const msg = player.get("message");
if (msg && msg.deletable) {
await msg.delete().catch(() => {});
}
// Send a message with a button to resume playback
const channel = guild.channels.cache.get(player.textChannelId);
if (channel) {
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("resume")
.setLabel("Resume Music")
.setStyle(ButtonStyle.Primary)
);
await channel.safeSend({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setDescription("The music has stopped due to a disconnection. You can resume it by clicking the button below.")
],
components: [row]
});
}
};

View File

@ -0,0 +1,34 @@
const { MUSIC, EMBED_COLORS } = require("@root/config");
const { EmbedBuilder } = require("discord.js");
const Queue = require("@src/database/schemas/Queue");
module.exports = async (client, player) => {
const guild = client.guilds.cache.get(player.guildId);
if (!guild) return;
if (player.voiceChannelId) {
await client.utils.setVoiceStatus(client, player.voiceChannelId, "Silence? Use /play to start the beat!");
}
if (player.volume > 100) {
await player.setVolume(MUSIC.DEFAULT_VOLUME);
}
const msg = player.get("message");
if (msg && msg.deletable) {
await msg.delete().catch(() => {});
}
const channel = guild.channels.cache.get(player.textChannelId);
if (channel) {
await channel.safeSend(
{
embeds: [new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED).setDescription("Queue has ended.")],
},
10
);
}
// Clean up the queue from the database if all tracks are played
await Queue.deleteGuild(player)
};

View File

@ -0,0 +1,38 @@
const { autoplayFunction } = require("@handlers/player");
const { MUSIC } = require("@root/config.js");
const Queue = require("@src/database/schemas/Queue");
module.exports = async (client, player, track) => {
const guild = client.guilds.cache.get(player.guildId);
if (!guild) return;
if (player.volume > 100) {
await player.setVolume(MUSIC.DEFAULT_VOLUME);
}
const msg = player.get("message");
if (msg && msg.deletable) {
await msg.delete().catch(() => {});
}
// Check if the current track was saved in the database
const queueData = await Queue.findGuild(player)
// Remove the current track from the database after it has been played
if (queueData) {
const updatedTracks = queueData.tracks.filter(t => t.uri !== track.info.uri);
await Queue.model.updateOne(
{ guildId: player.guildId },
{ tracks: updatedTracks, currentTrack: null }
);
// If there are no more tracks left, consider cleaning up the database entry
if (updatedTracks.length === 0) {
await Queue.deleteGuild(player)
}
}
if (player.get("autoplay") === true) {
await autoplayFunction(client, track, player);
}
};

View File

@ -0,0 +1,221 @@
const { EmbedBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder } = require("discord.js");
const { EMBED_COLORS } = require("@root/config");
const Queue = require("@src/database/schemas/Queue");
const joinCommand = require("@src/commands/music/join");
module.exports = async (client, player, track) => {
const guild = client.guilds.cache.get(player.guildId);
if (!guild) return;
// Check if the queue exists in the database
await Queue.restore(player)
// Save the current track to the database
await Queue.saveCurrentTrack(player, track);
if (!player.textChannelId || !track) return;
const channel = guild.channels.cache.get(player.textChannelId);
if (!channel) return;
if (player.get("autoplay") === true) {
await player.queue.previous.push(track);
}
if (player.voiceChannelId) {
await client.utils.setVoiceStatus(client, player.voiceChannelId, `Playing: **${track.info.title}**`);
}
const previous = await player.queue.shiftPrevious();
const row1 = (player) =>
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("volumedown")
.setEmoji("🔉")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("previous")
.setEmoji("⏮️")
.setStyle(ButtonStyle.Secondary)
.setDisabled(!previous),
new ButtonBuilder()
.setCustomId("pause")
.setEmoji(player.paused ? "▶️" : "⏸️")
.setStyle(player.paused ? ButtonStyle.Success : ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("skip")
.setEmoji("⏭️")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("volumeup")
.setEmoji("🔊")
.setStyle(ButtonStyle.Secondary)
);
const row2 = (player) =>
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("autoplay")
.setEmoji("♾️")
.setStyle(player.get("autoplay") ? ButtonStyle.Success : ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("loop")
.setEmoji("🔁")
.setStyle(player.repeatMode === "off" ? ButtonStyle.Secondary : ButtonStyle.Success),
new ButtonBuilder()
.setCustomId("stop")
.setEmoji("⏹️")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId("shuffle")
.setEmoji("🔀")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("queue")
.setEmoji("📑")
.setStyle(ButtonStyle.Secondary)
);
const msg = await channel.safeSend({
embeds: [
new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setAuthor({ name: "Track Started" })
.setDescription(`🎶 **Now playing [${track.info.title}](${track.info.uri})**`)
.setThumbnail(track.info.artworkUrl)
.setFooter({
text: `Requested by: ${track.requester.displayName}`,
})
.addFields(
{
name: "Duration",
value: track.info.isStream ? "Live" : client.utils.formatTime(track.info.duration),
inline: true,
},
{ name: "Author", value: track.info.author || "Unknown", inline: true }
),
],
components: [row1(player), row2(player)],
});
if (msg) player.set("message", msg);
const collector = msg.createMessageComponentCollector({
filter: async (int) => {
const sameVc = int.guild.members.me.voice.channelId === int.member.voice.channelId;
return sameVc;
},
});
collector.on("collect", async (int) => {
if (!int.isButton()) return;
await int.deferReply({ ephemeral: true });
let description;
switch (int.customId) {
case "previous":
description = previous ? "Playing the previous track..." : "No previous track available";
if (previous) player.play({ clientTrack: previous });
break;
case "pause":
if (player.paused) {
player.resume();
description = "Track resumed";
} else {
player.pause();
description = "Track paused";
}
await msg.edit({ components: [row1(player), row2(player)] });
break;
case "stop":
player.stopPlaying(true, false);
description = "Playback stopped";
break;
case "skip":
description = player.queue.tracks.length > 0 ? "Skipped to the next track" : "The queue is empty!";
if (player.queue.tracks.length > 0) player.skip();
break;
case "shuffle":
if (player.queue.tracks.length < 2) {
description = "The queue is too short to shuffle!";
} else {
player.queue.shuffle();
description = "The queue has been shuffled!";
}
break;
case "volumeup":
const newVolUp = Math.min(player.volume + 10, 100);
await player.setVolume(newVolUp);
description = `Volume increased to \`${newVolUp}%\``;
break;
case "volumedown":
const newVolDown = Math.max(player.volume - 10, 0);
await player.setVolume(newVolDown);
description = `Volume decreased to \`${newVolDown}%\``;
break;
case "loop":
const currentMode = player.repeatMode;
if (currentMode === "off") {
player.setRepeatMode("track");
description = "Loop mode set to `track`";
} else if (currentMode === "track") {
player.setRepeatMode("queue");
description = "Loop mode set to `queue`";
} else {
player.setRepeatMode("off");
description = "Loop mode `disabled`";
}
await msg.edit({ components: [row1(player), row2(player)] });
break;
case "autoplay":
const autoplay = !player.get("autoplay");
player.set("autoplay", autoplay);
description = autoplay ? "Autoplay activated!" : "Autoplay deactivated";
await msg.edit({ components: [row1(player), row2(player)] });
break;
case "queue":
const queue = player.queue;
const tracks = queue.tracks.slice(0, 10); // Get first 10 tracks
const queueEmbed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setAuthor({ name: "Music Queue" })
.setDescription(
tracks.length > 0
? tracks
.map((track, i) => `${i + 1}. [${track.info.title}](${track.info.uri})`)
.join("\n")
: "No tracks in queue"
)
.addFields({
name: "Now Playing",
value: `[${queue.current.info.title}](${queue.current.info.uri})`,
});
if (tracks.length > 0) {
queueEmbed.setFooter({
text: `${queue.tracks.length} track(s) in queue`,
});
}
description = "Queue sent as new message";
await channel.send({ embeds: [queueEmbed] });
break;
}
await int.followUp({
embeds: [new EmbedBuilder().setDescription(description).setColor(EMBED_COLORS.BOT_EMBED)],
});
});
};

3
src/events/raw.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = async (client, data) => {
client.musicManager.sendRawData(data);
};

View File

@ -10,7 +10,7 @@ module.exports = async (client) => {
// Initialize Music Manager
if (client.config.MUSIC.ENABLED) {
client.musicManager.connect({ userId: client.user.id });
client.musicManager.init({ ...client.user, shards: "auto" });
client.logger.success("Music Manager initialized");
}

View File

@ -22,9 +22,9 @@ module.exports = async (client, oldState, newState) => {
if (oldState.channel.members.size === 1) {
setTimeout(() => {
// if 1 (you), wait 1 minute
if (!oldState.channel.members.size - 1) {
const player = client.musicManager.players.resolve(guild.id);
if (player) client.musicManager.players.destroy(guild.id).then(player.voice.disconnect()); // destroy the player
if (oldState.channel.members.size === 1) {
const player = client.musicManager.getPlayer(guild.id);
if (player) player.destroy(); // destroy the player
}
}, client.config.MUSIC.IDLE_TIME * 1000);
}

View File

@ -11,4 +11,6 @@ module.exports = {
suggestionHandler: require("./suggestion"),
ticketHandler: require("./ticket"),
translationHandler: require("./translation"),
managerHandler: require("./manager"),
playerHandler: require("./player"),
};

View File

@ -1,75 +0,0 @@
const { EmbedBuilder, GatewayDispatchEvents } = require("discord.js");
const { Cluster } = require("lavaclient");
const { formatTime } = require("@helpers/Utils");
require("@lavaclient/plugin-queue").load();
/**
* @param {import("@structures/BotClient")} client
*/
module.exports = (client) => {
const lavaclient = new Cluster({
nodes: client.config.MUSIC.LAVALINK_NODES,
ws: client.config.MUSIC.LAVALINK_WS,
discord: {
sendGatewayCommand: (id, payload) => client.guilds.cache.get(id)?.shard?.send(payload),
},
});
client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (data) => lavaclient.players.handleVoiceUpdate(data));
client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (data) => lavaclient.players.handleVoiceUpdate(data));
lavaclient.on("nodeConnected", (node, event) => {
client.logger.log(`Node "${node.identifier}" connected`);
});
lavaclient.on("nodeDisconnected", (node, event) => {
client.logger.log(`Node "${node.identifier}" disconnected`);
const reconnectInterval = 20000; // Time in MS, change as needed.
setTimeout(() => {
node.connect();
}, reconnectInterval);
});
lavaclient.on("nodeError", (node, error) => {
client.logger.error(`Node "${node.identifier}" encountered an error: ${error.message}.`, error);
});
lavaclient.on("nodeDebug", (node, event) => {
client.logger.debug(`Node "${node.identifier}" debug: ${event.message}`);
});
lavaclient.on("nodeTrackStart", async (_node, queue, track) => {
const fields = [];
const embed = new EmbedBuilder()
.setAuthor({ name: "Now Playing" })
.setColor(client.config.EMBED_COLORS.BOT_EMBED)
.setDescription(`[${track.info.title}](${track.info.uri})`)
.setFooter({ text: `Requested By: ${track.requesterId}` })
.setThumbnail(track.info.artworkUrl);
fields.push({
name: "Song Duration",
value: "`" + formatTime(track.info.length) + "`",
inline: true,
});
if (queue.tracks.length > 0) {
fields.push({
name: "Position in Queue",
value: (queue.tracks.length + 1).toString(),
inline: true,
});
}
embed.setFields(fields);
queue.data.channel.safeSend({ embeds: [embed] });
});
lavaclient.on("nodeQueueFinish", async (_node, queue) => {
queue.data.channel.safeSend("Queue has ended.");
await client.musicManager.players.destroy(queue.player.guildId).then(() => queue.player.voice.disconnect());
});
return lavaclient;
};

43
src/handlers/manager.js Normal file
View File

@ -0,0 +1,43 @@
const { LavalinkManager } = require("lavalink-client");
const { MUSIC } = require("@root/config.js");
class Manager extends LavalinkManager {
constructor(client) {
super({
nodes: MUSIC.LAVALINK_NODES,
sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
emitNewSongsOnly: false,
queueOptions: {
maxPreviousTracks: 30,
},
playerOptions: {
defaultSearchPlatform: MUSIC.DEFAULT_SOURCE,
onDisconnect: {
autoReconnect: true,
destroyPlayer: false,
},
},
linksAllowed: true,
linksBlacklist: ["porn"],
linksWhitelist: [],
});
this.nodeManager.on("connect", (node) => {
client.logger.success(`Lavalink node ${node.id} connected!`);
});
this.nodeManager.on("disconnect", (node, reason) => {
client.logger.warn(`Lavalink node "${node.id}" disconnected. Reason: ${JSON.stringify(reason)}`);
});
this.nodeManager.on("error", (node, error) => {
client.logger.error(`Error occurred on Lavalink node "${node.id}": ${error.message}`);
});
this.nodeManager.on("destroy", (node) => {
client.logger.warn(`Lavalink node "${node.id}" destroyed`);
});
}
}
module.exports = Manager;

71
src/handlers/player.js Normal file
View File

@ -0,0 +1,71 @@
const { EMBED_COLORS, MUSIC } = require("@root/config");
const { EmbedBuilder } = require("discord.js");
module.exports = {
autoplayFunction: async (client, track, player) => {
if (player.queue.tracks.length > 0) return;
if (!track) return;
const channel = client.guilds.cache.get(player.guildId)?.channels.cache.get(player.textChannelId);
let url, source;
switch (track.info.sourceName) {
case "spotify":
url = `seed_tracks=${track.info.identifier ?? "11xbo8bGa5XTrXrGP77zwc"}&limit=20&min_popularity=30`;
source = "sprec";
break;
case "youtube":
url = `https://youtube.com/watch?v=${track.info.identifier ?? "lpeuIu-ZYJY"}&list=RD${track.info.identifier ?? "lpeuIu-ZYJY"}`;
source = "ytsearch";
break;
case "jiosaavn":
url = `${track.info.identifier ?? "Hvma-gqd"}`;
source = "jsrec";
break;
default:
url = track.info.author;
source = MUSIC.DEFAULT_SOURCE;
break;
}
const res = await player.search({ query: url, source: source }, track.requester);
if (!res || res.tracks.length === 0) {
await channel.safeSend(
{
embeds: [new EmbedBuilder().setColor(EMBED_COLORS.WARNING).setDescription("> Autoplay, No results found")],
},
10
);
return player.destroy();
}
for (let songs = 0; songs < 3; ) {
const chosen = res.tracks[Math.floor(Math.random() * res.tracks.length)];
if (
!player.queue.previous?.some((o) => o.info.identifier === chosen.info.identifier) &&
!player.queue.tracks.some((o) => o.info.identifier === chosen.info.identifier)
) {
await player.queue.add(chosen);
songs++;
}
}
if (player.queue.tracks.length === 0) {
await channel?.safeSend(
{
embeds: [
new EmbedBuilder().setColor(EMBED_COLORS.WARNING).setDescription("> Autoplay, No unique track found"),
],
},
10
);
return player.destroy();
}
},
};

View File

@ -63,8 +63,8 @@ module.exports = class BotUtils {
static get musicValidations() {
return [
{
callback: ({ client, guildId }) => client.musicManager.players.resolve(guildId),
message: "🚫 No music is being played!",
callback: ({ client, guildId }) => client.musicManager.getPlayer(guildId),
message: "🚫 I'm not in a voice channel.",
},
{
callback: ({ member }) => member.voice?.channelId,
@ -72,7 +72,7 @@ module.exports = class BotUtils {
},
{
callback: ({ member, client, guildId }) =>
member.voice?.channelId === client.musicManager.players.resolve(guildId)?.voice.channelId,
member.voice?.channelId === client.musicManager.getPlayer(guildId)?.voiceChannelId,
message: "🚫 You're not in the same voice channel.",
},
];

View File

@ -162,21 +162,43 @@ module.exports = class Utils {
* @returns {string} - Formatted time string
*/
static formatTime(ms) {
const minuteMs = 60 * 1000;
const hourMs = 60 * minuteMs;
const dayMs = 24 * hourMs;
if (ms < minuteMs) {
return `${ms / 1000}s`;
} else if (ms < hourMs) {
return `${Math.floor(ms / minuteMs)}m ${Math.floor(
(ms % minuteMs) / 1000
)}s`;
} else if (ms < dayMs) {
return `${Math.floor(ms / hourMs)}h ${Math.floor(
(ms % hourMs) / minuteMs
)}m`;
} else {
return `${Math.floor(ms / dayMs)}d ${Math.floor((ms % dayMs) / hourMs)}h`;
}
return ms < 1000
? `${ms / 1000}s`
: ["d", "h", "m", "s"]
.map((unit, i) => {
const value = [864e5, 36e5, 6e4, 1e3][i];
const amount = Math.floor(ms / value);
ms %= value;
return amount ? `${amount}${unit}` : null;
})
.filter((x) => x !== null)
.join(" ") || "0s";
}
/**
* Parses a time string into milliseconds
* @param {string} string - The time string (e.g., "1d", "2h", "3m", "4s")
* @returns {number} - The time in milliseconds
*/
static parseTime(string) {
const time = string.match(/([0-9]+[dhms])/g);
if (!time) return 0;
return time.reduce((ms, t) => {
const unit = t[t.length - 1];
const amount = Number(t.slice(0, -1));
return ms + amount * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit];
}, 0);
}
/**
* Updates voice channel status
* @param {string} channel - The voice channel ID
* @param {string} status - The status to update
* @param {object} client - The bot client
*/
static async setVoiceStatus(client, channelId, message) {
const url = `/channels/${channelId}/voice-status`;
const payload = { status: message };
await client.rest.put(url, { body: payload }).catch(() => {});
}
};

View File

@ -44,14 +44,15 @@ module.exports = class Validator {
// Music
if (config.MUSIC.ENABLED) {
if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET) {
warn("env: SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET are missing. Spotify music links won't work");
}
if (config.MUSIC.LAVALINK_NODES.length == 0) {
warn("config.js: There must be at least one node for Lavalink");
}
if (!["ytsearch", "ytmsearch", "spsearch", "scsearch"].includes(config.MUSIC.DEFAULT_SOURCE)) {
warn("config.js: MUSIC.DEFAULT_SOURCE must be either ytsearch, ytmsearch, spsearch or scsearch");
if (
!["ytsearch", "ytmsearch", "scsearch", "spsearch", "dzsearch", "jssearch"].includes(config.MUSIC.DEFAULT_SOURCE)
) {
warn(
"config.js: MUSIC.DEFAULT_SOURCE must be either ytsearch, ytmsearch, scsearch, spsearch, dzsearch or jssearch"
);
}
}

View File

@ -13,7 +13,7 @@ const { recursiveReadDirSync } = require("../helpers/Utils");
const { validateCommand, validateContext } = require("../helpers/Validator");
const { schemas } = require("@src/database/mongoose");
const CommandCategory = require("./CommandCategory");
const lavaclient = require("../handlers/lavaclient");
const Manager = require("../handlers/manager");
const giveawaysHandler = require("../handlers/giveaway");
const { DiscordTogether } = require("discord-together");
@ -43,10 +43,10 @@ module.exports = class BotClient extends Client {
this.config = require("@root/config"); // load the config file
/**
* @type {import('@structures/Command')[]}
* @type {Collection<string, import('@structures/Command')>}
*/
this.commands = []; // store actual command
this.commandIndex = new Collection(); // store (alias, arrayIndex) pair
this.commands = new Collection(); // store actual command
this.commandAlias = new Map(); // store (alias, command) pair
/**
* @type {Collection<string, import('@structures/Command')>}
@ -65,7 +65,7 @@ module.exports = class BotClient extends Client {
: undefined;
// Music Player
if (this.config.MUSIC.ENABLED) this.musicManager = lavaclient(this);
if (this.config.MUSIC.ENABLED) this.musicManager = new Manager(this);
// Giveaways
if (this.config.GIVEAWAYS.ENABLED) this.giveawaysManager = giveawaysHandler(this);
@ -76,6 +76,9 @@ module.exports = class BotClient extends Client {
// Database
this.database = schemas;
// Utils
this.utils = require("../helpers/Utils");
// Discord Together
this.discordTogether = new DiscordTogether(this);
}
@ -92,18 +95,23 @@ module.exports = class BotClient extends Client {
recursiveReadDirSync(directory).forEach((filePath) => {
const file = path.basename(filePath);
const dir = path.basename(path.dirname(filePath));
try {
const eventName = path.basename(file, ".js");
const event = require(filePath);
this.on(eventName, event.bind(null, this));
if (dir === "player") {
this.musicManager.on(eventName, event.bind(null, this));
} else {
this.on(eventName, event.bind(null, this));
}
clientEvents.push([file, "✓"]);
delete require.cache[require.resolve(filePath)];
success += 1;
} catch (ex) {
failed += 1;
this.logger.error(`loadEvent - ${file}`, ex);
this.logger.error(`Failed to load event - ${file}`, ex);
}
});
@ -127,8 +135,8 @@ module.exports = class BotClient extends Client {
* @returns {import('@structures/Command')|undefined}
*/
getCommand(invoke) {
const index = this.commandIndex.get(invoke.toLowerCase());
return index !== undefined ? this.commands[index] : undefined;
const invokeLower = invoke.toLowerCase()
return this.commands.get(invokeLower) ?? this.commandAlias.get(invokeLower);
}
/**
@ -143,18 +151,17 @@ module.exports = class BotClient extends Client {
}
// Prefix Command
if (cmd.command?.enabled) {
const index = this.commands.length;
if (this.commandIndex.has(cmd.name)) {
const name = cmd.name.toLowerCase()
if (this.commands.has(name)) {
throw new Error(`Command ${cmd.name} already registered`);
}
if (Array.isArray(cmd.command.aliases)) {
cmd.command.aliases.forEach((alias) => {
if (this.commandIndex.has(alias)) throw new Error(`Alias ${alias} already registered`);
this.commandIndex.set(alias.toLowerCase(), index);
if (this.commandAlias.has(alias)) throw new Error(`Alias ${alias} already registered`);
this.commandAlias.set(alias.toLowerCase(), cmd);
});
}
this.commandIndex.set(cmd.name.toLowerCase(), index);
this.commands.push(cmd);
this.commands.set(name, cmd);
} else {
this.logger.debug(`Skipping command ${cmd.name}. Disabled!`);
}
@ -187,7 +194,7 @@ module.exports = class BotClient extends Client {
}
}
this.logger.success(`Loaded ${this.commands.length} commands`);
this.logger.success(`Loaded ${this.commands.size} commands`);
this.logger.success(`Loaded ${this.slashCommands.size} slash commands`);
if (this.slashCommands.size > 100) throw new Error("A maximum of 100 slash commands can be enabled");
}