diff --git a/.env-example b/.env-example index c5fed01..862ff0c 100644 --- a/.env-example +++ b/.env-example @@ -1 +1,2 @@ -BOT_TOKEN=Your bot token here \ No newline at end of file +BOT_TOKEN=Your bot token here +CLIENT_ID=Your client id here \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6ebbb63..2f21b32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .env node_modules .idea -.vscode \ No newline at end of file +.vscode +Docker* +*.lock \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index beb0111..0000000 --- a/crowdin.yml +++ /dev/null @@ -1,3 +0,0 @@ -files: - - source: src/language/language/en.json - translation: /src/language/language/%two_letters_code%.%file_extension% diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0cdff33 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,8 @@ +import js from "@eslint/js"; +import globals from "globals"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, +]); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d7f1832..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1341 +0,0 @@ -{ - "name": "anichan", - "version": "2025.1.25.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "anichan", - "version": "2025.1.25.1", - "license": "MIT", - "dependencies": { - "@discordjs/builders": "^1.10.0", - "@discordjs/rest": "^2.4.2", - "axios": "^1.7.9", - "discord-api-types": "^0.37.117", - "discord.js": "^14.17.3", - "dotenv": "^16.4.7", - "i18n": "^0.15.1", - "minecraft-server-util": "^5.4.4", - "nodemon": "^3.1.9", - "weather-js": "^2.0.0" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", - "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.6.0", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.37.114", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", - "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.37.114" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", - "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.37.114", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", - "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.4.1", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "^0.37.114", - "tslib": "^2.6.2", - "ws": "^8.17.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@messageformat/core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz", - "integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==", - "license": "MIT", - "dependencies": { - "@messageformat/date-skeleton": "^1.0.0", - "@messageformat/number-skeleton": "^1.0.0", - "@messageformat/parser": "^5.1.0", - "@messageformat/runtime": "^3.0.1", - "make-plural": "^7.0.0", - "safe-identifier": "^0.4.1" - } - }, - "node_modules/@messageformat/date-skeleton": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz", - "integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==", - "license": "MIT" - }, - "node_modules/@messageformat/number-skeleton": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", - "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==", - "license": "MIT" - }, - "node_modules/@messageformat/parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.0.tgz", - "integrity": "sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==", - "license": "MIT", - "dependencies": { - "moo": "^0.5.1" - } - }, - "node_modules/@messageformat/runtime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", - "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", - "license": "MIT", - "dependencies": { - "make-plural": "^7.0.0" - } - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.3.tgz", - "integrity": "sha512-x7zadcfJGxFka1Q3f8gCts1F0xMwCKbZweM85xECGI0hBTeIZJGGCrHgLggihBoprlQ/hBmDR5LKfIPqnmHM3w==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@types/node": { - "version": "22.10.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.8.tgz", - "integrity": "sha512-rk+QvAEGsbX/ZPiiyel6hJHNUS9cnSbPWVaZLvE+Er3tLqQFzWMz9JOfWW7XUmKvRPfxJfbl3qYWve+RGXncFw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "license": "Apache-2.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/discord-api-types": { - "version": "0.37.117", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", - "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", - "license": "MIT" - }, - "node_modules/discord.js": { - "version": "14.17.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz", - "integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.10.0", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.0", - "@discordjs/rest": "^2.4.2", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "^1.2.0", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.37.114", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/discord.js/node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-printf": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz", - "integrity": "sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==", - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.1.4" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/i18n": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.1.tgz", - "integrity": "sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA==", - "license": "MIT", - "dependencies": { - "@messageformat/core": "^3.0.0", - "debug": "^4.3.3", - "fast-printf": "^1.6.9", - "make-plural": "^7.0.0", - "math-interval-parser": "^2.0.1", - "mustache": "^4.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/mashpie" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "license": "MIT" - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "license": "MIT" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "license": "MIT" - }, - "node_modules/magic-bytes.js": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", - "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", - "license": "MIT" - }, - "node_modules/make-plural": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", - "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==", - "license": "Unicode-DFS-2016" - }, - "node_modules/math-interval-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz", - "integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minecraft-motd-util": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/minecraft-motd-util/-/minecraft-motd-util-1.1.12.tgz", - "integrity": "sha512-5TuTRjrRupSTruea0nRC37r0FdhkS1O4wIJKAYfwJRCQd/X4Zyl/dVIs96h9UVW6N8jhIuz9pNkrDsqyN7VBdA==", - "deprecated": "no longer maintained", - "license": "MIT" - }, - "node_modules/minecraft-server-util": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/minecraft-server-util/-/minecraft-server-util-5.4.4.tgz", - "integrity": "sha512-uaE0N9PHPtJudi8BlCvKDD9K30w23ZpeS1I53wEJo9nX0HSmg5qFvwj8gMG0CYCUwWH/g5hjKaGAwXXN/vhMJw==", - "deprecated": "no longer maintained, please use https://mcstatus.io", - "license": "MIT", - "dependencies": { - "minecraft-motd-util": "^1.1.9" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/moo": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", - "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "license": "BSD-3-Clause" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-identifier": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", - "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", - "license": "ISC" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "license": "MIT" - }, - "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/weather-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/weather-js/-/weather-js-2.0.0.tgz", - "integrity": "sha512-vM7BUa+QUOu4mW48Gdnqe6HRPFpBkNy0gjKvnOHwYpbdylvJorIG5+EjUVaf2SmWrkE0Je1tfsJG+pWQBEs16g==", - "license": "MIT", - "dependencies": { - "request": "2.x.x", - "xml2js": "0.4.x" - } - }, - "node_modules/ws": { - "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" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - } - } -} diff --git a/package.json b/package.json index c3166b4..226be6d 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,30 @@ { "name": "anichan", - "version": "2025.1.25.1", + "version": "2026.2.x", "description": "", "main": "./src/index.js", "scripts": { - "start": "node ./src/index.js", - "unregister": "node ./src/utli/unregister_slash_commands.js", - "debug": "nodemon ./src/index.js" + "start": "bun ./src/index.js", + "unregister": "bun ./src/utils/unregister_slash_commands.js", + "debug": "nodemon ./src/index.js", + "lint": "eslint ." }, "keywords": [], "author": "", "license": "MIT", "dependencies": { - "@discordjs/builders": "^1.10.0", - "@discordjs/rest": "^2.4.2", - "axios": "^1.7.9", - "discord-api-types": "^0.37.117", - "discord.js": "^14.17.3", - "dotenv": "^16.4.7", - "i18n": "^0.15.1", - "minecraft-server-util": "^5.4.4", - "nodemon": "^3.1.9", + "@discordjs/rest": "^2.6.0", + "axios": "^1.13.5", + "discord-api-types": "^0.38.39", + "discord.js": "^14.25.1", + "dotenv": "^17.3.1", "weather-js": "^2.0.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "bun": "^1.3.9", + "eslint": "^10.0.0", + "globals": "^17.3.0", + "nodemon": "^3.1.11" } } diff --git a/src/bancheck.js b/src/bancheck.js deleted file mode 100644 index 4a54ed4..0000000 --- a/src/bancheck.js +++ /dev/null @@ -1,35 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { EmbedBuilder } = require('discord.js'); -const language = require('./language/language_setup'); - -async function checkBan(interaction) { - const userId = interaction.user.id; - const userName = interaction.user.username; - const banlistDir = path.join(__dirname, 'banlist'); - const banFilePath = path.join(banlistDir, `${userId}.txt`); - - if (fs.existsSync(banFilePath)) { - const banData = fs.readFileSync(banFilePath, 'utf8').split(', '); - const [time, date, reason] = banData; - const bantime = time +" "+ date; - - const embed = new EmbedBuilder() - .setTitle(`${language.__n('userban.bantitle')}`) - .setThumbnail(interaction.user.avatarURL()) - .addFields( - { name: `${language.__n('userban.username')}`, value: userName, inline: true}, - { name: `${language.__n('userban.uuid')}`, value: userId, inline: true }, - { name: `${language.__n('userban.bantime')}`, value: bantime, inline: true }, - { name: `${language.__n('userban.reason')}`, value: reason || `${language.__n('userban.noreason')}`, inline: true } - ) - .setFooter({ text: `${language.__n('userban.contact')}` }) - .setTimestamp(); - - await interaction.reply({ embeds: [embed], ephemeral: false }); - return true; - } - return false; -} - -module.exports = { checkBan }; \ No newline at end of file diff --git a/src/banlist/banstruc.txt b/src/banlist/banstruc.txt deleted file mode 100644 index b575691..0000000 --- a/src/banlist/banstruc.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Follow this ban structure "Time, date, reason" -# For example: -# UUID discord users are 12345678 -# Create 1 txt file with the name "12345678" and the file content as below -# 1H11, 1/1/1111, Exemplary ban \ No newline at end of file diff --git a/src/commands/anime_commands/anime.js b/src/commands/anime_commands/anime.js index 2556912..9821a70 100644 --- a/src/commands/anime_commands/anime.js +++ b/src/commands/anime_commands/anime.js @@ -1,52 +1,40 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('anime') - .setDescription(`${language.__n('anime.command_description')}`) - .addStringOption(option => option.setName('name').setDescription(`${language.__n('anime.anime_name')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('anime'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations) + .addStringOption(option => option.setName('name').setDescription(getLocalizedMessage('anime', 'anime_name')).setRequired(true)); + })(), async execute(interaction) { try { await interaction.deferReply(); const animeName = interaction.options.getString('name'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/anime.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/anime.graphql'); const variables = { name: animeName }; - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; + const data = await queryAnilistFromFile(queryPath, variables); const animeData = data.data.Media; if (!animeData) { - return interaction.editReply(`${language.__n('global.no_results')} **${animeName}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)} **${animeName}**`); } const genres = animeData.genres; if (genres.includes('Ecchi') || genres.includes('Hentai')) { - return interaction.editReply(`**${language.__n('global.nsfw_block')} ${animeName}**\n${language.__n('global.nsfw_block_reason')}`); + return interaction.editReply(`**${getLocalizedMessage('global', 'nsfw_block', interaction.locale)} ${animeName}**\n${getLocalizedMessage('global', 'nsfw_block_reason', interaction.locale)}`); } - let description = animeData.description; - if (description) { - description = description.replace(/\n*
/g, ''); - if (description.length > 600) { - description = description.slice(0, 600) + '...'; - } - } + const description = parseHtmlText(animeData.description, 600); const embedImage = "https://img.anili.st/media/" + animeData.id; @@ -56,23 +44,32 @@ module.exports = { .setDescription(description) .setColor('#66FFFF') .addFields( - { name: `${language.__n('global.episodes')}`, value: `${animeData.episodes || `${language.__n('global.unavailable')}`}`, inline: true }, - { name: `${language.__n('global.status')}`, value: `${animeData.status}`, inline: true }, - { name: `${language.__n('global.average_score')}`, value: `${animeData.averageScore}/100`, inline: true }, - { name: `${language.__n('global.mean_score')}`, value: `${animeData.meanScore}/100`, inline: true }, - { name: `${language.__n('global.season')}`, value: `${animeData.season} - ${animeData.startDate.year}`, inline: true }, - { name: `${language.__n('global.studio')}`, value: `${animeData.studios.edges.map(edge => edge.node.name).join(', ')}`, inline: true } + { name: getLocalizedMessage('global', 'episodes', interaction.locale), value: `${animeData.episodes || getLocalizedMessage('global', 'unavailable', interaction.locale)}`, inline: true }, + { name: getLocalizedMessage('global', 'status', interaction.locale), value: `${animeData.status}`, inline: true }, + { name: getLocalizedMessage('global', 'average_score', interaction.locale), value: `${animeData.averageScore}/100`, inline: true }, + { name: getLocalizedMessage('global', 'mean_score', interaction.locale), value: `${animeData.meanScore}/100`, inline: true }, + { name: getLocalizedMessage('global', 'season', interaction.locale), value: `${animeData.season} - ${animeData.startDate.year}`, inline: true }, + { name: getLocalizedMessage('global', 'studio', interaction.locale), value: `${animeData.studios.edges.map(edge => edge.node.name).join(', ')}`, inline: true } ) .setImage(embedImage) .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(animeData.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); + const errorMessage = getLocalizedMessage('global', 'error_reply', interaction.locale); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(errorMessage); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(errorMessage); } } }, diff --git a/src/commands/anime_commands/characters.js b/src/commands/anime_commands/characters.js index ac7a628..4438912 100644 --- a/src/commands/anime_commands/characters.js +++ b/src/commands/anime_commands/characters.js @@ -1,44 +1,35 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('characters') - .setDescription(`${language.__n('character.command_description')}`) - .addStringOption(option => option.setName('name').setDescription(`${language.__n('character.character_name')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('characters'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() + .addStringOption(option => option.setName('name').setDescription(getLocalizedMessage('character', 'character_name')).setRequired(true)), async execute(interaction) { try { await interaction.deferReply(); const characterName = interaction.options.getString('name'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/characters.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/characters.graphql'); const variables = { search: characterName }; - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; + const data = await queryAnilistFromFile(queryPath, variables); const characterData = data.data.Character; if (!characterData) { - return interaction.editReply(`${language.__n('global.no_results')} **${characterName}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)} **${characterName}**`); } - let description = characterData.description || `${language.__n('global.no_description')}`; - if (description && description.length > 600) { - description = description.slice(0, 600) + '...'; - } + const description = parseHtmlText(characterData.description, 600) || getLocalizedMessage('global', 'no_description', interaction.locale); const uniqueAnimeAppearances = [...new Set(characterData.media.nodes.map(node => node.title.romaji))]; @@ -46,18 +37,26 @@ module.exports = { .setTitle(characterData.name.full) .setURL(characterData.siteUrl) .setDescription(description) - .addFields({ name: `${language.__n('character.anime_appearances')}`, value: uniqueAnimeAppearances.join(', ') || `${language.__n('global.no_results')}` }) + .addFields({ name: `${getLocalizedMessage('character', 'anime_appearances', interaction.locale)}`, value: uniqueAnimeAppearances.join(', ') || `${getLocalizedMessage('global', 'no_results', interaction.locale)}` }) .setImage(characterData.image.large) .setColor('#C6FFFF') .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(characterData.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/charactersearch.js b/src/commands/anime_commands/charactersearch.js index a5bab10..c923ac8 100644 --- a/src/commands/anime_commands/charactersearch.js +++ b/src/commands/anime_commands/charactersearch.js @@ -1,57 +1,58 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('character_search') - .setDescription(`${language.__n('character_search.command_description')}`) - .addStringOption(option => option.setName('character').setDescription(`${language.__n('character_search.command_description')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('character_search'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() + .addStringOption(option => option.setName('character').setDescription(getLocalizedMessage('character_search', 'command_description')).setRequired(true)), async execute(interaction) { try { await interaction.deferReply(); const character = interaction.options.getString('character'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/characters.graphql'), 'utf8'); - const variables = { character }; - - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); + const queryPath = path.join(__dirname, '../../queries/characters.graphql'); + const variables = { search: character }; - const data = response.data; + const data = await queryAnilistFromFile(queryPath, variables); const characterData = data.data.Character; if (!characterData) { - return interaction.editReply(`${language.__n('global.no_results')} **${character}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)} **${character}**`); } const embed = new EmbedBuilder() - .setTitle(`${language.__n('character_search.anime_list')} ${characterData.name.full}`) - .setDescription(`${language.__n('character_search.anime_list')} **${characterData.name.full}**:`) + .setTitle(`${getLocalizedMessage('character_search', 'anime_list', interaction.locale)} ${characterData.name.full}`) + .setDescription(`${getLocalizedMessage('character_search', 'anime_list', interaction.locale)} **${characterData.name.full}**:`) .setTimestamp(); characterData.media.nodes.forEach(anime => { - const animeTitle = anime.title.romaji || `${language.__n('global.unavailable')}`; + const animeTitle = anime.title.romaji || `${getLocalizedMessage('global', 'unavailable', interaction.locale)}`; embed.addFields({ name: animeTitle, value: '\u200B', inline: false }); }); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(characterData.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/manga.js b/src/commands/anime_commands/manga.js index fae019c..4361ae6 100644 --- a/src/commands/anime_commands/manga.js +++ b/src/commands/anime_commands/manga.js @@ -1,85 +1,86 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('manga') - .setDescription(`${language.__n('manga.command_description')}`) - .addStringOption(option => option.setName('name').setDescription(`${language.__n('manga.manga_name')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('manga'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() + .addStringOption(option => option.setName('name').setDescription(getLocalizedMessage('manga', 'manga_name')).setRequired(true)), async execute(interaction) { try { await interaction.deferReply(); const mangaName = interaction.options.getString('name'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/manga.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/manga.graphql'); const variables = { name: mangaName }; - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; + const data = await queryAnilistFromFile(queryPath, variables); const mangaData = data.data.Media; if (!mangaData) { - return interaction.editReply(`${language.__n('global.no_results')} **${mangaName}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)} **${mangaName}**`); } const mangaGenre = mangaData.genres; if (mangaGenre.includes('Ecchi') || mangaGenre.includes('Hentai')) { - return interaction.editReply(`**${language.__n('global.nsfw_block')} ${mangaName}**\n${language.__n('global.nsfw_block_reason')}`); + return interaction.editReply(`**${getLocalizedMessage('global', 'nsfw_block', interaction.locale)} ${mangaName}**\n${getLocalizedMessage('global', 'nsfw_block_reason', interaction.locale)}`); } - const description = mangaData.description ? mangaData.description.slice(0, 500) + '...' : 'Không có thông tin.'; + const description = parseHtmlText(mangaData.description, 500) || getLocalizedMessage('global', 'no_description', interaction.locale); const embedImage = "https://img.anili.st/media/" + mangaData.id; - console.log(embedImage); const embed = new EmbedBuilder() .setTitle(mangaData.title.romaji) .setURL(mangaData.siteUrl) .setDescription(description) .addFields( { - name: `${language.__n('global.chapters')}`, - value: mangaData.chapters ? mangaData.chapters : `${language.__n('global.unavailable')}`, + name: `${getLocalizedMessage('global', 'chapters', interaction.locale)}`, + value: mangaData.chapters ? mangaData.chapters : `${getLocalizedMessage('global', 'unavailable', interaction.locale)}`, inline: true }, { - name: `${language.__n('global.genres')}`, + name: `${getLocalizedMessage('global', 'genres', interaction.locale)}`, value: mangaData.genres.join(', '), inline: true }, { - name: `${language.__n('global.average_score')}`, + name: `${getLocalizedMessage('global', 'average_score', interaction.locale)}`, value: `${mangaData.averageScore}/100`, inline: true }, { - name: `${language.__n('global.mean_score')}`, - value: `${mangaData.meanScore ? mangaData.meanScore + '/100' : `${language.__n('global.unavailable')}`}`, + name: `${getLocalizedMessage('global', 'mean_score', interaction.locale)}`, + value: `${mangaData.meanScore ? mangaData.meanScore + '/100' : `${getLocalizedMessage('global', 'unavailable', interaction.locale)}`}`, inline: true }, ) .setImage(embedImage) .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(mangaData.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/popular.js b/src/commands/anime_commands/popular.js index a80534c..c557872 100644 --- a/src/commands/anime_commands/popular.js +++ b/src/commands/anime_commands/popular.js @@ -1,56 +1,56 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('popular') - .setDescription(`${language.__n('popular.command_description')}`), + data: (() => { + const localization = getCommandLocalization('popular'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })(), async execute(interaction) { try { await interaction.deferReply(); + const queryPath = path.join(__dirname, '../../queries/popular.graphql'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/popular.graphql'), 'utf8'); + const data = await queryAnilistFromFile(queryPath, {}); - const response = await axios.post('https://graphql.anilist.co', { - query: query - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; const popularAnime = data.data.Page.media; let currentPage = 0; const updateEmbed = () => { const anime = popularAnime[currentPage]; - const description = anime.description ? anime.description.replace(/<[^>]+>/g, '').slice(0, 250) + '...' : `${language.__n('global.unavailable')}`; + const description = parseHtmlText(anime.description, 250) || getLocalizedMessage('global', 'unavailable', interaction.locale); const embedImage = "https://img.anili.st/media/" + anime.id; const embed = new EmbedBuilder() .setTitle(anime.title.romaji) .setURL(anime.siteUrl) - .setDescription(`__**${language.__n('global.description')}:**__ ${description}\n__**${language.__n('global.average_score')}:**__ ${anime.averageScore}/100\n__**${language.__n('global.mean_score')}:**__ ${anime.meanScore ? anime.meanScore + '/100' : `${language.__n('global.unavailable')}`}\n\n__**${language.__n('global.page')}:**__ ${currentPage + 1}/${popularAnime.length}`) + .setDescription(`__**${getLocalizedMessage('global', 'description', interaction.locale)}:**__ ${description}\n__**${getLocalizedMessage('global', 'average_score', interaction.locale)}:**__ ${anime.averageScore}/100\n__**${getLocalizedMessage('global', 'mean_score', interaction.locale)}:**__ ${anime.meanScore ? anime.meanScore + '/100' : `${getLocalizedMessage('global', 'unavailable', interaction.locale)}`}`) .setImage(embedImage) + .setFooter({ text: `${getLocalizedMessage('global', 'page', interaction.locale)}: ${currentPage + 1}/${popularAnime.length}` }) .setTimestamp(); const row = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('prev') - .setLabel(`${language.__n('global.preview_button')}`) + .setLabel(`${getLocalizedMessage('global', 'preview_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) .setDisabled(currentPage === 0), new ButtonBuilder() .setCustomId('next') - .setLabel(`${language.__n('global.next_button')}`) + .setLabel(`${getLocalizedMessage('global', 'next_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) - .setDisabled(currentPage === popularAnime.length - 1) + .setDisabled(currentPage === popularAnime.length - 1), + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(anime.siteUrl) + .setStyle(ButtonStyle.Link) ); return { embeds: [embed], components: [row] }; @@ -74,15 +74,15 @@ module.exports = { try { await interaction.editReply({ components: [] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); } }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/random.js b/src/commands/anime_commands/random.js index 07f45e2..9ad950e 100644 --- a/src/commands/anime_commands/random.js +++ b/src/commands/anime_commands/random.js @@ -1,102 +1,254 @@ -const { SlashCommandBuilder } = require("@discordjs/builders"); -const { EmbedBuilder } = require("discord.js"); -const axios = require("axios"); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require("./../../language/language_setup.js"); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); + +// Helper function to fetch random anime +async function fetchRandomAnime() { + let randomAnime; + let attempts = 0; + const maxAttempts = 10; + let maxPages = null; + + // Retry mechanism to get valid, non-NSFW content + while (attempts < maxAttempts && !randomAnime) { + try { + // Add random sorting + const sortOptions = ['POPULARITY_DESC', 'SCORE_DESC', 'TRENDING_DESC', 'FAVOURITES_DESC']; + const randomSort = sortOptions[Math.floor(Math.random() * sortOptions.length)]; + + // First request: get pageInfo if not cached + if (maxPages === null) { + const queryPath = path.join(__dirname, '../../queries/random.graphql'); + const infoData = await queryAnilistFromFile(queryPath, { page: 1, sort: randomSort, perPage: 1 }, { timeout: 10000 }); + + maxPages = Math.min(infoData.data.Page.pageInfo.lastPage, 500); + } + + // Generate random page within actual limits + const randomPage = Math.floor(Math.random() * maxPages) + 1; + + const variables = { + page: randomPage, + sort: randomSort, + perPage: 25 + }; + + const queryPath = path.join(__dirname, '../../queries/random.graphql'); + const data = await queryAnilistFromFile(queryPath, variables, { timeout: 10000 }); + + const animeList = data.data.Page.media; + + if (animeList && animeList.length > 0) { + // Filter out NSFW content before random selection + const restrictedGenres = ["Ecchi", "Hentai"]; + const safeAnimeList = animeList.filter(anime => + !anime.genres.some(genre => restrictedGenres.includes(genre)) + ); + + if (safeAnimeList.length > 0) { + // Use crypto.getRandomValues for better randomness if available + let randomIndex; + if (typeof crypto !== 'undefined' && crypto.getRandomValues) { + const array = new Uint32Array(1); + crypto.getRandomValues(array); + randomIndex = array[0] % safeAnimeList.length; + } else { + randomIndex = Math.floor(Math.random() * safeAnimeList.length); + } + + randomAnime = safeAnimeList[randomIndex]; + break; + } + } + + attempts++; + if (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + + } catch { + attempts++; + if (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } + + return randomAnime; +} + +// Helper function to create embed and buttons +function createAnimeEmbed(randomAnime, locale) { + // Double-check NSFW filtering (extra safety) + const restrictedGenres = ["Ecchi", "Hentai"]; + if (randomAnime.genres.some(genre => restrictedGenres.includes(genre))) { + return null; + } + + const description = parseHtmlText(randomAnime.description, 600); + + // AniList image format + const embedImage = `https://img.anili.st/media/${randomAnime.id}`; + + const embed = new EmbedBuilder() + .setTitle(randomAnime.title.romaji) + .setURL(randomAnime.siteUrl) + .setDescription(description) + .setColor("#66FFFF") + .addFields( + { + name: `${getLocalizedMessage('global', 'episodes', locale)}`, + value: `${randomAnime.episodes || getLocalizedMessage('global', 'unavailable', locale)}`, + inline: true, + }, + { + name: `${getLocalizedMessage('global', 'status', locale)}`, + value: `${randomAnime.status}`, + inline: true, + }, + { + name: `${getLocalizedMessage('global', 'average_score', locale)}`, + value: `${randomAnime.averageScore ? randomAnime.averageScore : getLocalizedMessage('global', 'unavailable', locale)}/100`, + inline: true, + }, + { + name: `${getLocalizedMessage('global', 'mean_score', locale)}`, + value: `${randomAnime.meanScore ? randomAnime.meanScore : getLocalizedMessage('global', 'unavailable', locale)}/100`, + inline: true, + }, + { + name: `${getLocalizedMessage('global', 'season', locale)}`, + value: `${randomAnime.season || getLocalizedMessage('global', 'unavailable', locale)}${randomAnime.startDate && randomAnime.startDate.year ? ` - ${randomAnime.startDate.year}` : ''}`, + inline: true, + }, + { + name: `${getLocalizedMessage('global', 'studio', locale)}`, + value: `${randomAnime.studios.edges.map(edge => edge.node.name).join(", ") || getLocalizedMessage('global', 'unavailable', locale)}`, + inline: true, + } + ) + .setImage(embedImage) + .setTimestamp(); + + // Create action row with buttons + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('random_again') + .setLabel(getLocalizedMessage('global', 'random_again', locale)) + .setStyle(ButtonStyle.Primary) + .setEmoji('🎲'), + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', locale)) + .setURL(randomAnime.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + return { embed, row, anime: randomAnime }; +} module.exports = { - data: new SlashCommandBuilder() - .setName("random") - .setDescription(`${language.__n("random.command_description")}`), + data: (() => { + const localization = getCommandLocalization('random'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })(), async execute(interaction) { try { await interaction.deferReply(); - const randomPage = Math.floor(Math.random() * 419);// 419 is the last page of the AniList API (update: 2025/02/1) - console.log(`Random page: ${randomPage}`); - const variables = { page: randomPage }; - - const query = fs.readFileSync(path.join(__dirname, '../../queries/random.graphql'), 'utf8'); - const response = await axios.post("https://graphql.anilist.co", { - query: query, - variables: variables, - }, { - headers: { - "Content-Type": "application/json", - Accept: "application/json", - } - }); - - const animeList = response.data.data.Page.media; - - if (!animeList.length) { - return interaction.editReply(language.__n("global.no_results")); + + // Fetch random anime + const randomAnime = await fetchRandomAnime(); + + // If no anime found after all attempts + if (!randomAnime) { + return interaction.editReply(getLocalizedMessage('global', 'no_results', interaction.locale)); } - const randomAnime = animeList[Math.floor(Math.random() * animeList.length)]; - const restrictedGenres = ["Ecchi", "Hentai"]; - if (randomAnime.genres.some(genre => restrictedGenres.includes(genre))) { + // Create embed and buttons + const result = createAnimeEmbed(randomAnime, interaction.locale); + + if (!result) { return interaction.editReply( - `**${language.__n("global.nsfw_block")} ${randomAnime.title.romaji}**\n${language.__n("global.nsfw_block_reason")}` + `**${getLocalizedMessage('global', 'nsfw_block', interaction.locale)} ${randomAnime.title.romaji}**\n${getLocalizedMessage('global', 'nsfw_block_reason', interaction.locale)}` ); } - let description = randomAnime.description ? randomAnime.description.replace(/\n*
/g, "") : ""; - if (description.length > 600) { - description = description.slice(0, 600) + "..."; - } + const message = await interaction.editReply({ embeds: [result.embed], components: [result.row] }); - const embedImage = `https://img.anili.st/media/${randomAnime.id}`; + // Create collector for button interactions + const collector = message.createMessageComponentCollector({ time: 300000 }); // 5 minutes - const embed = new EmbedBuilder() - .setTitle(randomAnime.title.romaji) - .setURL(randomAnime.siteUrl) - .setDescription(description) - .setColor("#66FFFF") - .addFields( - { - name: `${language.__n("global.episodes")}`, - value: `${randomAnime.episodes || language.__n("global.unavailable")}`, - inline: true, - }, - { - name: `${language.__n("global.status")}`, - value: `${randomAnime.status}`, - inline: true, - }, - { - name: `${language.__n("global.average_score")}`, - value: `${randomAnime.averageScore}/100`, - inline: true, - }, - { - name: `${language.__n("global.mean_score")}`, - value: `${randomAnime.meanScore}/100`, - inline: true, - }, - { - name: `${language.__n("global.season")}`, - value: `${randomAnime.season} - ${randomAnime.startDate.year}`, - inline: true, - }, - { - name: `${language.__n("global.studio")}`, - value: `${randomAnime.studios.edges.map(edge => edge.node.name).join(", ") || language.__n("global.unavailable")}`, - inline: true, + collector.on('collect', async i => { + if (i.customId === 'random_again') { + try { + await i.deferUpdate(); + + // Fetch new random anime + const newAnime = await fetchRandomAnime(); + + if (!newAnime) { + await i.editReply({ + content: getLocalizedMessage('global', 'no_results', i.locale), + embeds: [], + components: [] + }); + return; + } + + // Create new embed and buttons + const newResult = createAnimeEmbed(newAnime, i.locale); + + if (!newResult) { + await i.editReply({ + content: `**${getLocalizedMessage('global', 'nsfw_block', i.locale)} ${newAnime.title.romaji}**\n${getLocalizedMessage('global', 'nsfw_block_reason', i.locale)}`, + embeds: [], + components: [] + }); + return; + } + + await i.editReply({ embeds: [newResult.embed], components: [newResult.row] }); + } catch { + // Error handling for button interaction } - ) - .setImage(embedImage) - .setTimestamp(); + } + }); - await interaction.editReply({ embeds: [embed] }); + collector.on('end', async () => { + try { + // Disable buttons after collector expires + const disabledRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('random_again_disabled') + .setLabel(getLocalizedMessage('global', 'random_again', interaction.locale)) + .setStyle(ButtonStyle.Primary) + .setEmoji('🎲') + .setDisabled(true), + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(result.anime.siteUrl) + .setStyle(ButtonStyle.Link) + ); + await interaction.editReply({ components: [disabledRow] }); + } catch { + // Message might be deleted, ignore error + } + }); } catch (error) { - console.error(`${language.__n("global.error")}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n("global.error_reply")}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n("global.error_reply")}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/schedule.js b/src/commands/anime_commands/schedule.js index 1888784..eb4bda5 100644 --- a/src/commands/anime_commands/schedule.js +++ b/src/commands/anime_commands/schedule.js @@ -1,41 +1,36 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('schedule') - .setDescription(`${language.__n('schedule.command_description')}`), + data: (() => { + const localization = getCommandLocalization('schedule'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })(), async execute(interaction) { try { await interaction.deferReply(); - const query = fs.readFileSync(path.join(__dirname, '../../queries/schedule.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/schedule.graphql'); let currentPage = 1; const perPage = 10; const airingAtGreater = Math.floor(Date.now() / 1000); const fetchPage = async (page) => { const variables = { page, perPage, airingAtGreater }; - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - return response.data.data.Page; + const data = await queryAnilistFromFile(queryPath, variables); + return data.data.Page; }; let pageData = await fetchPage(currentPage); if (!pageData.airingSchedules.length) { - return interaction.editReply(`${language.__n('global.no_results')}`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)}`); } const updateEmbed = () => { @@ -45,20 +40,21 @@ module.exports = { }).join('\n'); const embed = new EmbedBuilder() - .setTitle(`${language.__n('schedule.airing_schedule')}`) + .setTitle(`${getLocalizedMessage('schedule', 'airing_schedule', interaction.locale)}`) .setDescription(scheduleList) + .setFooter({ text: `${getLocalizedMessage('global', 'page', interaction.locale)}: ${currentPage}` }) .setTimestamp(); const row = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('prev') - .setLabel(`${language.__n('global.preview_button')}`) + .setLabel(`${getLocalizedMessage('global', 'preview_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) .setDisabled(currentPage === 1), new ButtonBuilder() .setCustomId('next') - .setLabel(`${language.__n('global.next_button')}`) + .setLabel(`${getLocalizedMessage('global', 'next_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) .setDisabled(!pageData.pageInfo.hasNextPage) ); @@ -85,15 +81,15 @@ module.exports = { try { await interaction.editReply({ components: [] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); } }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/search.js b/src/commands/anime_commands/search.js index 77c640a..703c7a2 100644 --- a/src/commands/anime_commands/search.js +++ b/src/commands/anime_commands/search.js @@ -1,40 +1,45 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const axios = require('axios'); -const fs = require('fs'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); const commandCooldown = new Map(); module.exports = { cooldown: 60, - data: new SlashCommandBuilder() - .setName('search') - .setDescription(`${language.__n('search.command_description')}`) + data: (() => { + const localization = getCommandLocalization('search'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() .addSubcommandGroup(group => group.setName('image') - .setDescription(`${language.__n('search.image_option')}`) + .setDescription(getLocalizedMessage('search', 'image_option')) .addSubcommand(subcommand => subcommand.setName('url') - .setDescription(`${language.__n('search.image_link')}`) + .setDescription(getLocalizedMessage('search', 'image_link')) .addStringOption(option => option.setName('url') - .setDescription(`${language.__n('search.image_link')}`) + .setDescription(getLocalizedMessage('search', 'image_link')) .setRequired(true)) .addBooleanOption(option => option.setName('cut_black_borders') - .setDescription(`${language.__n('search.cut_black_borders')}`) + .setDescription(getLocalizedMessage('search', 'cut_black_borders')) .setRequired(true))) .addSubcommand(subcommand => subcommand.setName('upload') - .setDescription(`${language.__n('search.upload_image')}`) + .setDescription(getLocalizedMessage('search', 'upload_image')) .addAttachmentOption(option => option.setName('upload') - .setDescription(`${language.__n('search.upload_image')}`) + .setDescription(getLocalizedMessage('search', 'upload_image')) .setRequired(true)) .addBooleanOption(option => option.setName('cut_black_borders') - .setDescription(`${language.__n('search.cut_black_borders')}`) + .setDescription(getLocalizedMessage('search', 'cut_black_borders')) .setRequired(true)))), async execute(interaction) { try { @@ -59,7 +64,7 @@ module.exports = { if (currentTime - lastUsage < cooldownTime) { const remainingTime = cooldownTime - (currentTime - lastUsage); const remainingMinutes = Math.ceil(remainingTime / (60 * 1000)); - return interaction.editReply(`${language.__n('global.tracemoe_api_limit')} ${language.__n('global.retry')} ${remainingMinutes} ${language.__n('global.minute')}.`); + return interaction.editReply(`${getLocalizedMessage('global', 'tracemoe_api_limit', interaction.locale)} ${getLocalizedMessage('global', 'retry', interaction.locale)} ${remainingMinutes} ${getLocalizedMessage('global', 'minute', interaction.locale)}.`); } } @@ -74,29 +79,17 @@ module.exports = { if (data.result && data.result.length > 0) { const animeid = data.result[0].anilist; - const query = fs.readFileSync(path.join(__dirname, '../../queries/search.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/search.graphql'); const variables = { id: animeid }; - const graphqlResponse = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); + const graphqlData = await queryAnilistFromFile(queryPath, variables); - const graphqlData = graphqlResponse.data; const media = graphqlData.data.Media; const animename = media.title.english || media.title.romaji || media.title.native; const embedImage = "https://img.anili.st/media/" + animeid; - let description = media.description; - if (description && description.length > 400) { - description = description.slice(0, 400) + '...'; - } + const description = parseHtmlText(media.description, 400); const genres = media.genres; if (genres.includes('Ecchi') || genres.includes('Hentai')) { - return interaction.editReply(`**${language.__n('global.nsfw_block')} ${animename}**\n${language.__n('global.nsfw_block_reason')}`); + return interaction.editReply(`**${getLocalizedMessage('global', 'nsfw_block', interaction.locale)} ${animename}**\n${getLocalizedMessage('global', 'nsfw_block_reason', interaction.locale)}`); } const episode = data.result[0].episode; const similarity = (data.result[0].similarity * 100).toFixed(0); @@ -104,24 +97,32 @@ module.exports = { const embed = new EmbedBuilder() .setTitle(`Anime: ${animename}`) .setURL(media.siteUrl) - .setDescription(`${language.__n('global.description')}: ${description}`) + .setDescription(`${getLocalizedMessage('global', 'description', interaction.locale)}: ${description}`) .addFields( - { name: `${language.__n('search.appears_episode')}`, value: `${episode}`, inline: true }, - { name: `${language.__n('search.similarity')}`, value: `${similarity} %`, inline: true } + { name: `${getLocalizedMessage('search', 'appears_episode', interaction.locale)}`, value: `${episode}`, inline: true }, + { name: `${getLocalizedMessage('search', 'similarity', interaction.locale)}`, value: `${similarity} %`, inline: true } ) .setImage(embedImage); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(media.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); commandCooldown.set(interaction.user.id, Date.now()); } else { - interaction.editReply(`${language.__n('global.error_reply')}`); + interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - return interaction.editReply(`${language.__n('global.error_reply')}`); + return interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - return interaction.reply(`${language.__n('global.error_reply')}`); + return interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/staff.js b/src/commands/anime_commands/staff.js index 007500e..e0f7fa6 100644 --- a/src/commands/anime_commands/staff.js +++ b/src/commands/anime_commands/staff.js @@ -1,54 +1,58 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('staff') - .setDescription(`${language.__n('staff.command_description')}`) - .addStringOption(option => option.setName('name').setDescription(`${language.__n('staff.staff_name')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('staff'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() + .addStringOption(option => option.setName('name').setDescription(getLocalizedMessage('staff', 'staff_name')).setRequired(true)), async execute(interaction) { try { await interaction.deferReply(); const staffName = interaction.options.getString('name'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/staff.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/staff.graphql'); const variables = { search: staffName }; - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; + const data = await queryAnilistFromFile(queryPath, variables); const staffData = data.data.Staff; if (!staffData) { - return interaction.editReply(`${language.__n('global.no_results')} **${staffName}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)} **${staffName}**`); } + const description = parseHtmlText(staffData.description, 1000) || getLocalizedMessage('global', 'unavailable', interaction.locale); + const embed = new EmbedBuilder() - .setTitle(`${language.__n('staff.staff_info')}: ${staffData.name.first} ${staffData.name.last}`) + .setTitle(`${getLocalizedMessage('staff', 'staff_info', interaction.locale)}: ${staffData.name.first} ${staffData.name.last}`) .setURL(staffData.siteUrl) - .setDescription(staffData.description || `${language.__n('global.unavailable')}`) + .setDescription(description) .setImage(staffData.image.large) .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(staffData.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/studio.js b/src/commands/anime_commands/studio.js index a0e6fc0..a136330 100644 --- a/src/commands/anime_commands/studio.js +++ b/src/commands/anime_commands/studio.js @@ -1,45 +1,38 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('studio') - .setDescription(`${language.__n('studio.command_description')}`) - .addStringOption(option => option.setName('name').setDescription(`${language.__n('studio.studio_name')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('studio'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() + .addStringOption(option => option.setName('name').setDescription(getLocalizedMessage('studio', 'studio_name')).setRequired(true)), async execute(interaction) { try { await interaction.deferReply(); const studioName = interaction.options.getString('name'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/studio.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/studio.graphql'); const variables = { search: studioName }; - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: variables - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; + const data = await queryAnilistFromFile(queryPath, variables); const studioData = data.data.Studio; if (!studioData) { - return interaction.editReply(`${language.__n('global.no_results')} **${studioName}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)} **${studioName}**`); } const animeList = studioData.media.nodes.map((anime, index) => { const animeTitle = anime.title.romaji; const animeUrl = anime.siteUrl; - const animeYear = anime.startDate ? anime.startDate.year : `${language.__n('global.unavailable')}`; - return `${index + 1}. [${animeTitle}](${animeUrl}) - ${language.__n('studio.product_year')}: ${animeYear}`; + const animeYear = anime.startDate ? anime.startDate.year : `${getLocalizedMessage('global', 'unavailable', interaction.locale)}`; + return `${index + 1}. [${animeTitle}](${animeUrl}) - ${getLocalizedMessage('studio', 'product_year', interaction.locale)}: ${animeYear}`; }).join('\n'); const pageSize = 10; @@ -52,23 +45,28 @@ module.exports = { const displayedAnime = animeList.split('\n').slice(startIdx, endIdx).join('\n'); const embed = new EmbedBuilder() - .setTitle(`${language.__n('studio.studio_info')} ${studioData.name}`) + .setTitle(`${getLocalizedMessage('studio', 'studio_info', interaction.locale)} ${studioData.name}`) .setURL(studioData.siteUrl) - .setDescription(`${language.__n('studio.product_list')} ${studioData.name}:\n${displayedAnime}`) + .setDescription(`${getLocalizedMessage('studio', 'product_list', interaction.locale)} ${studioData.name}:\n${displayedAnime}`) + .setFooter({ text: `${getLocalizedMessage('global', 'page', interaction.locale)}: ${currentPage + 1}/${totalPages}` }) .setTimestamp(); const row = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('prev') - .setLabel(`${language.__n('global.preview_button')}`) + .setLabel(`${getLocalizedMessage('global', 'preview_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) .setDisabled(currentPage === 0), new ButtonBuilder() .setCustomId('next') - .setLabel(`${language.__n('global.next_button')}`) + .setLabel(`${getLocalizedMessage('global', 'next_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) - .setDisabled(currentPage === totalPages - 1) + .setDisabled(currentPage === totalPages - 1), + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(studioData.siteUrl) + .setStyle(ButtonStyle.Link) ); return { embeds: [embed], components: [row] }; @@ -92,15 +90,15 @@ module.exports = { try { await interaction.editReply({ components: [] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); } }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/trending.js b/src/commands/anime_commands/trending.js index c9cd79d..13dee41 100644 --- a/src/commands/anime_commands/trending.js +++ b/src/commands/anime_commands/trending.js @@ -1,56 +1,57 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { parseHtmlText } = require('./../../utils/textParser.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('trending') - .setDescription(`${language.__n('trending.command_description')}`), + data: (() => { + const localization = getCommandLocalization('trending'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })(), async execute(interaction) { try { await interaction.deferReply(); - const query = fs.readFileSync(path.join(__dirname, '../../queries/trending.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/trending.graphql'); - const response = await axios.post('https://graphql.anilist.co', { - query: query - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); + const data = await queryAnilistFromFile(queryPath, {}); - const data = response.data; const trendingAnime = data.data.Page.media; let currentPage = 0; const updateEmbed = () => { const anime = trendingAnime[currentPage]; - const description = anime.description ? anime.description.replace(/<[^>]+>/g, '').slice(0, 250) + '...' : `${language.__n('global.unavailable')}`; + const description = parseHtmlText(anime.description, 250) || getLocalizedMessage('global', 'unavailable', interaction.locale); const embedImage = "https://img.anili.st/media/" + anime.id; const embed = new EmbedBuilder() .setTitle(anime.title.romaji) .setURL(anime.siteUrl) - .setDescription(`__**${language.__n('global.description')}:**__ ${description}\n__**${language.__n('global.average_score')}:**__ ${anime.averageScore}/100\n__**${language.__n('global.mean_score')}:**__ ${anime.meanScore ? anime.meanScore + '/100' : `${language.__n('global.unavailable')}`}\n\n__**${language.__n('global.page')}:**__ ${currentPage + 1}/${trendingAnime.length}`) + .setDescription(`__**${getLocalizedMessage('global', 'description', interaction.locale)}:**__ ${description}\n__**${getLocalizedMessage('global', 'average_score', interaction.locale)}:**__ ${anime.averageScore}/100\n__**${getLocalizedMessage('global', 'mean_score', interaction.locale)}:**__ ${anime.meanScore ? anime.meanScore + '/100' : `${getLocalizedMessage('global', 'unavailable', interaction.locale)}`}`) .setImage(embedImage) + .setFooter({ text: `${getLocalizedMessage('global', 'page', interaction.locale)}: ${currentPage + 1}/${trendingAnime.length}` }) .setTimestamp(); const row = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('prev') - .setLabel(`${language.__n('global.preview_button')}`) + .setLabel(`${getLocalizedMessage('global', 'preview_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) .setDisabled(currentPage === 0), new ButtonBuilder() .setCustomId('next') - .setLabel(`${language.__n('global.next_button')}`) + .setLabel(`${getLocalizedMessage('global', 'next_button', interaction.locale)}`) .setStyle(ButtonStyle.Primary) - .setDisabled(currentPage === trendingAnime.length - 1) + .setDisabled(currentPage === trendingAnime.length - 1), + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(anime.siteUrl) + .setStyle(ButtonStyle.Link) ); return { embeds: [embed], components: [row] }; @@ -74,15 +75,15 @@ module.exports = { try { await interaction.editReply({ components: [] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); } }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/anime_commands/user.js b/src/commands/anime_commands/user.js index 10144f6..cc2053b 100644 --- a/src/commands/anime_commands/user.js +++ b/src/commands/anime_commands/user.js @@ -1,38 +1,31 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const axios = require('axios'); -const fs = require('fs'); +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); +const { queryAnilistFromFile } = require('./../../hook/anilist.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('user') - .setDescription(`${language.__n('user.command_description')}`) - .addStringOption(option => option.setName('username').setDescription(`${language.__n('user.user_name')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('user'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() + .addStringOption(option => option.setName('username').setDescription(getLocalizedMessage('user', 'user_name')).setRequired(true)), async execute(interaction) { try { await interaction.deferReply(); const username = interaction.options.getString('username'); - const query = fs.readFileSync(path.join(__dirname, '../../queries/user.graphql'), 'utf8'); + const queryPath = path.join(__dirname, '../../queries/user.graphql'); - const response = await axios.post('https://graphql.anilist.co', { - query: query, - variables: { username } - }, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - }); - - const data = response.data; + const data = await queryAnilistFromFile(queryPath, { username }); const userData = data.data.User; const userImage = `https://img.anili.st/user/${userData.id}`; if (!userData) { - return interaction.editReply(`${language.__n('global.no_results')}: **${username}**`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)}: **${username}**`); } const embed = new EmbedBuilder() @@ -41,36 +34,44 @@ module.exports = { .setColor('#C6FFFF') .addFields( { - name: `${language.__n('user.anime_count')}`, - value: `${userData.statistics.anime.count} ${language.__n('user.anime_count')}.`, + name: `${getLocalizedMessage('user', 'anime_count', interaction.locale)}`, + value: `${userData.statistics.anime.count} ${getLocalizedMessage('user', 'anime_count_value', interaction.locale)}.`, inline: true, }, { - name: `${language.__n('user.minutes_watched')}`, - value: `${userData.statistics.anime.minutesWatched} ${language.__n('user.minutes_watched')}`, + name: `${getLocalizedMessage('user', 'minutes_watched', interaction.locale)}`, + value: `${userData.statistics.anime.minutesWatched} ${getLocalizedMessage('user', 'minutes_watched_value', interaction.locale)}`, inline: true, }, { - name: `${language.__n('user.manga_count')}`, - value: `${userData.statistics.manga.count} ${language.__n('user.manga_count')}.`, + name: `${getLocalizedMessage('user', 'manga_count', interaction.locale)}`, + value: `${userData.statistics.manga.count} ${getLocalizedMessage('user', 'manga_count_value', interaction.locale)}.`, inline: true, }, { - name: `${language.__n('user.chapters_read')}`, - value: `${userData.statistics.manga.chaptersRead} ${language.__n('user.chapters_read')}.`, + name: `${getLocalizedMessage('user', 'chapters_read', interaction.locale)}`, + value: `${userData.statistics.manga.chaptersRead} ${getLocalizedMessage('user', 'chapters_read_value', interaction.locale)}.`, inline: true, } ) .setImage(userImage) .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setLabel(getLocalizedMessage('global', 'view_anilist', interaction.locale)) + .setURL(userData.siteUrl) + .setStyle(ButtonStyle.Link) + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/minecraft_commands/mcuser.js b/src/commands/minecraft_commands/mcuser.js deleted file mode 100644 index de3683a..0000000 --- a/src/commands/minecraft_commands/mcuser.js +++ /dev/null @@ -1,97 +0,0 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require('discord.js'); -const axios = require('axios'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('mcuser') - .setDescription('Get Minecraft account information') - .addStringOption(option => option.setName('identifier').setDescription('Minecraft username or UUID').setRequired(true)), - async execute(interaction) { - await interaction.deferReply(); - - const identifier = interaction.options.getString('identifier'); - let uuid, username; - - try { - const profileResponse = await axios.get(`https://api.mojang.com/users/profiles/minecraft/${identifier}`); - uuid = profileResponse.data.id; - username = profileResponse.data.name; - - const sessionResponse = await axios.get(`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`); - const properties = JSON.parse(Buffer.from(sessionResponse.data.properties[0].value, 'base64').toString('utf-8')); - const skinUrl = properties.textures.SKIN.url; - const capeUrl = properties.textures.CAPE ? properties.textures.CAPE.url : 'No cape available'; - - const headUrl = `https://cravatar.eu/helmhead/${username}`; - - const embed = new EmbedBuilder() - .setTitle(`Minecraft User: ${username}`) - .setThumbnail(headUrl) - .addFields( - { name: 'UUID', value: uuid, inline: true }, - { name: 'Username', value: username, inline: true } - ) - .setTimestamp(); - - const row = new ActionRowBuilder() - .addComponents( - new StringSelectMenuBuilder() - .setCustomId('mcuser_menu') - .setPlaceholder('Select an option') - .addOptions([ - { - label: 'Information', - description: 'View user information', - value: 'information', - }, - { - label: 'Skin & Cape', - description: 'View skin and cape information', - value: 'skin_cape', - }, - ]) - ); - - await interaction.editReply({ embeds: [embed], components: [row] }); - - const filter = i => i.customId === 'mcuser_menu' && i.user.id === interaction.user.id; - const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); - - collector.on('collect', async i => { - if (i.values[0] === 'information') { - const infoEmbed = new EmbedBuilder() - .setTitle(`Minecraft User: ${username}`) - .setThumbnail(headUrl) - .addFields( - { name: 'UUID', value: uuid, inline: true }, - { name: 'Username', value: username, inline: true } - ) - .setTimestamp(); - await i.update({ embeds: [infoEmbed], components: [row] }); - } else if (i.values[0] === 'skin_cape') { - const skinCapeEmbed = new EmbedBuilder() - .setTitle(`Skin & Cape for ${username}`) - .setThumbnail(headUrl) - .addFields( - { name: 'Skin URL', value: `[Download Skin](${skinUrl})`, inline: true }, - { name: 'Cape URL', value: capeUrl !== 'No cape available' ? `[Download Cape](${capeUrl})` : capeUrl, inline: true } - ) - .setTimestamp(); - await i.update({ embeds: [skinCapeEmbed], components: [row] }); - } - }); - - collector.on('end', async () => { - try { - await interaction.editReply({ components: [] }); - } catch (error) { - console.error('Error clearing components:', error); - } - }); - } catch (error) { - console.error('Error fetching Minecraft user data:', error); - await interaction.editReply('An error occurred while fetching the Minecraft user data. Please try again later.'); - } - }, -}; \ No newline at end of file diff --git a/src/commands/others/avatar.js b/src/commands/others/avatar.js index 38747f9..da5fb04 100644 --- a/src/commands/others/avatar.js +++ b/src/commands/others/avatar.js @@ -1,14 +1,18 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const language = require('./../../language/language_setup.js'); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('avatar') - .setDescription(`${language.__n('avatar.command_description')}`) + data: (() => { + const localization = getCommandLocalization('avatar'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })() .addUserOption(option => option.setName('user') - .setDescription(`${language.__n('avatar.user_name')}`) + .setDescription(getLocalizedMessage('avatar', 'user_name')) .setRequired(true)), async execute(interaction) { try { @@ -24,18 +28,18 @@ module.exports = { .setURL(avatar) .setImage(avatar) .setFooter({ - text: `${language.__n('avatar.requested_by')}: ${interaction.user.username}`, + text: `${getLocalizedMessage('avatar', 'requested_by', interaction.locale)}: ${interaction.user.username}`, iconURL: interaction.user.displayAvatarURL({ format: 'png', dynamic: true, size: 1024 }) }) .setColor('#eb3434'); await interaction.editReply({ embeds: [embed] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, diff --git a/src/commands/others/ban_check.js b/src/commands/others/ban_check.js deleted file mode 100644 index 8a6ccd5..0000000 --- a/src/commands/others/ban_check.js +++ /dev/null @@ -1,38 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const language = require("../../language/language_setup"); - -module.exports = { - data: new SlashCommandBuilder() - .setName('checkban') - .setDescription('Check if the user is banned'), - async execute(interaction) { - const userId = interaction.user.id; - const banlistDir = path.join(__dirname, '../../banlist'); - const banFilePath = path.join(banlistDir, `${userId}.txt`); - - if (fs.existsSync(banFilePath)) { - const banData = fs.readFileSync(banFilePath, 'utf8').split(', '); - const [time, date, reason] = banData; - const bantime = time + " "+date; - - const embed = new EmbedBuilder() - .setTitle(`${language.__n('userban.bantitle')}`) - .setThumbnail(interaction.user.avatarURL()) - .addFields( - { name: `${language.__n('userban.username')}`, value: userName, inline: true}, - { name: `${language.__n('userban.uuid')}`, value: userId, inline: true }, - { name: `${language.__n('userban.bantime')}`, value: bantime, inline: true }, - { name: `${language.__n('userban.reason')}`, value: reason || `${language.__n('userban.noreason')}`, inline: true } - ) - .setFooter({ text: `${language.__n('userban.contact')}` }) - .setTimestamp(); - - await interaction.reply({ embeds: [embed], ephemeral: true }); - } else { - await interaction.reply(`${language.__n('userban.noban')}`); - } - }, -}; \ No newline at end of file diff --git a/src/commands/others/botstats.js b/src/commands/others/botstats.js index 38b8fce..29c2ffb 100644 --- a/src/commands/others/botstats.js +++ b/src/commands/others/botstats.js @@ -1,11 +1,15 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const language = require('./../../language/language_setup.js'); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('stats') - .setDescription(`${language.__n('bot_stats.description')}`), + data: (() => { + const localization = getCommandLocalization('stats'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })(), async execute(interaction) { try { await interaction.deferReply(); @@ -16,63 +20,64 @@ module.exports = { const minutes = Math.floor(uptime / 60) % 60; const seconds = Math.floor(uptime % 60); const embed = new EmbedBuilder() - .setTitle(`${language.__n('bot_stats.title')}`) + .setTitle(`${getLocalizedMessage('stats', 'title', interaction.locale)}`) .setColor('#66ffff') .setFields( { - name: `Bot Ping`, + name: `${getLocalizedMessage('stats', 'ping', interaction.locale)}`, value: `${interaction.client.ws.ping}ms`, inline: true, }, { - name: `${language.__n('bot_stats.uptime')}`, + name: `${getLocalizedMessage('stats', 'uptime', interaction.locale)}`, value: `${days}d ${hours}h ${minutes}m ${seconds}s`, inline: true, }, { - name: `${language.__n('bot_stats.version')}`, + name: `${getLocalizedMessage('stats', 'version', interaction.locale)}`, value: `v${require('../../../package.json').version}`, inline: true, }, { - name: `CPU`, + name: `${getLocalizedMessage('stats', 'cpu', interaction.locale)}`, value: `${(process.cpuUsage().system / 1024 / 1024).toFixed(2)}%`, inline: true, }, { - name: `RAM`, + name: `${getLocalizedMessage('stats', 'ram', interaction.locale)}`, value: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`, inline: true, }, { - name: `${language.__n('bot_stats.disk_usage')}`, + name: `${getLocalizedMessage('stats', 'disk_usage', interaction.locale)}`, value: `${(process.memoryUsage().external / 1024 / 1024).toFixed(2)} MB`, inline: true, }, { - name: `${language.__n('bot_stats.os')}`, + name: `${getLocalizedMessage('stats', 'os', interaction.locale)}`, value: `${process.platform} ${process.arch}`, inline: true, }, { - name: `${language.__n('bot_stats.node')}`, + name: `${getLocalizedMessage('stats', 'node', interaction.locale)}`, value: `${process.version}`, inline: true, }, { - name: `${language.__n('bot_stats.library')}`, + name: `${getLocalizedMessage('stats', 'library', interaction.locale)}`, value: `Discord.js v${require('discord.js').version}`, inline: true, }, - ); + ) + .setFooter({ text: `Powered by STelliNeX | STNX Teams` }); await interaction.editReply({ embeds: [embed] }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } } diff --git a/src/commands/others/help.js b/src/commands/others/help.js index 098125d..742d98f 100644 --- a/src/commands/others/help.js +++ b/src/commands/others/help.js @@ -1,20 +1,24 @@ -const { SlashCommandBuilder } = require("@discordjs/builders"); -const { EmbedBuilder } = require("discord.js"); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); const fs = require("fs"); const path = require('path'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); module.exports = { - data: new SlashCommandBuilder() - .setName("help") - .setDescription(`${language.__n('help.command_description')}`), + data: (() => { + const localization = getCommandLocalization('help'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations); + })(), async execute(interaction) { try { await interaction.deferReply(); const embed = new EmbedBuilder() - .setTitle(`${language.__n('help.command_title')}`) - .setDescription(`${language.__n('help.embed_description')}`) + .setTitle(getLocalizedMessage('help', 'command_title', interaction.locale)) + .setDescription(getLocalizedMessage('help', 'embed_description')) .setTimestamp(); const commandsDirectory = path.join(__dirname, '..'); @@ -33,11 +37,12 @@ module.exports = { await interaction.editReply({ embeds: [embed], ephemeral: true }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(getLocalizedMessage('global', 'error'), error); + const errorMessage = getLocalizedMessage('global', 'error_reply', interaction.locale); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(errorMessage); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(errorMessage); } } }, diff --git a/src/commands/others/switch_language.js b/src/commands/others/switch_language.js deleted file mode 100644 index b80f0ae..0000000 --- a/src/commands/others/switch_language.js +++ /dev/null @@ -1,32 +0,0 @@ -const { SlashCommandBuilder } = require("@discordjs/builders"); -const { EmbedBuilder } = require("discord.js"); -const language = require("./../../language/language_setup.js"); - -module.exports = { - owner: true, - data: new SlashCommandBuilder() - .setName("switch_language") - .setDescription(`${language.__n("language.command_description")}`) - .addStringOption((option) => - option - .setName("language") - .setDescription(`${language.__n("language.language_option")}`) - .setRequired(true) - .addChoices( - { name: "English", value: "en" }, - { name: "Vietnamese", value: "vi" } - ) - ), - async execute(interaction) { - const languageres = interaction.options.getString("language"); - language.setLocale(languageres); - const response = language.__n(languageres); - - const embed = new EmbedBuilder() - .setTitle(`${language.__n("language.language_switch")}`) - .setDescription(`${language.__n("language.response_language")} ${response}`) - .setColor("#66ffff"); - - await interaction.reply({ embeds: [embed], ephemeral: true }); - }, -}; \ No newline at end of file diff --git a/src/commands/others/weather.js b/src/commands/others/weather.js index 58d938f..c0602bf 100644 --- a/src/commands/others/weather.js +++ b/src/commands/others/weather.js @@ -1,13 +1,17 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); const weather = require('weather-js'); -const { SlashCommandBuilder } = require('@discordjs/builders'); -const { EmbedBuilder } = require('discord.js'); -const language = require('./../../language/language_setup.js'); +const { getLocalizedMessage, getCommandLocalization } = require('./../../utils/localizations.js'); module.exports = { - data: new SlashCommandBuilder() - .setName('weather') - .setDescription(`${language.__n('weather.command_description')}`) - .addStringOption(option => option.setName('location').setDescription(`${language.__n('weather.location')}`).setRequired(true)), + data: (() => { + const localization = getCommandLocalization('weather'); + return new SlashCommandBuilder() + .setName(localization.name) + .setNameLocalizations(localization.nameLocalizations) + .setDescription(localization.description) + .setDescriptionLocalizations(localization.descriptionLocalizations) + .addStringOption(option => option.setName('location').setDescription(getLocalizedMessage('weather', 'location')).setRequired(true)); + })(), async execute(interaction) { try { @@ -17,11 +21,11 @@ module.exports = { weather.find({ search: location, degreeType: 'C' }, async function (error, result) { if (error) { - console.error(`${language.__n('global.error')}`, error); - return interaction.editReply(`${language.__n('global.error_reply')}`); + console.error(`${getLocalizedMessage('global', 'error', interaction.locale)}`, error); + return interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } if (result === undefined || result.length === 0) { - return interaction.editReply(`${language.__n('global.no_results')}`); + return interaction.editReply(`${getLocalizedMessage('global', 'no_results', interaction.locale)}`); } const current = result[0].current; @@ -34,42 +38,42 @@ module.exports = { .setTimestamp() .addFields( { - name: `${language.__n('weather.longitude')}`, + name: getLocalizedMessage('weather', 'longitude', interaction.locale), value: location.long, inline: true, }, { - name: `${language.__n('weather.latitude')}`, + name: getLocalizedMessage('weather', 'latitude', interaction.locale), value: location.lat, inline: true, }, { - name: `${language.__n('weather.degreetype')}`, + name: getLocalizedMessage('weather', 'degreetype', interaction.locale), value: `°${location.degreetype}`, inline: true, }, { - name: `${language.__n('weather.current_temperature')}`, + name: `${getLocalizedMessage('weather', 'current_temperature', interaction.locale)}`, value: `${current.temperature}°${location.degreetype}`, inline: true, }, { - name: `${language.__n('weather.feels_like')}`, + name: `${getLocalizedMessage('weather', 'feels_like', interaction.locale)}`, value: `${current.feelslike}°${location.degreetype}`, inline: true, }, { - name: `${language.__n('weather.winddisplay')}`, + name: `${getLocalizedMessage('weather', 'winddisplay', interaction.locale)}`, value: `${current.winddisplay}`, inline: true, }, { - name: `${language.__n('weather.humidity')}`, + name: `${getLocalizedMessage('weather', 'humidity', interaction.locale)}`, value: `${current.humidity}%`, inline: true, }, { - name: `${language.__n('weather.observationtime')}`, + name: `${getLocalizedMessage('weather', 'observationtime', interaction.locale)}`, value: `${current.observationtime}, GMT ${location.timezone}`, inline: true, } @@ -80,12 +84,12 @@ module.exports = { await interaction.editReply({ embeds: [embed], ephemeral: true }); }); } catch (error) { - console.error(`${language.__n('global.error')}`, error); + console.error(`${getLocalizedMessage('global', 'error', interaction.locale)}`, error); if (interaction.replied || interaction.deferred) { - await interaction.editReply(`${language.__n('global.error_reply')}`); + await interaction.editReply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } else { - await interaction.reply(`${language.__n('global.error_reply')}`); + await interaction.reply(`${getLocalizedMessage('global', 'error_reply', interaction.locale)}`); } } }, -}; \ No newline at end of file +}; diff --git a/src/hook/anilist.js b/src/hook/anilist.js new file mode 100644 index 0000000..5563ec9 --- /dev/null +++ b/src/hook/anilist.js @@ -0,0 +1,61 @@ +const axios = require('axios'); + +const API_URL = 'https://graphql.anilist.co'; + +/** + * Make a GraphQL request to AniList API + * @param {string} query - GraphQL query string + * @param {Object} variables - Query variables + * @param {Object} options - Additional options (timeout, etc.) + * @returns {Promise} Response data + */ +async function queryAnilist(query, variables = {}, options = {}) { + const { timeout = 10000 } = options; + + try { + const response = await axios.post( + API_URL, + { + query, + variables + }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + timeout + } + ); + + return response.data; + } catch (error) { + // Re-throw with more context + if (error.response) { + throw new Error(`AniList API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`, { cause: error }); + } else if (error.request) { + throw new Error('AniList API: No response received', { cause: error }); + } else { + throw error; + } + } +} + +/** + * Read GraphQL query from file and execute + * @param {string} queryFilePath - Path to .graphql file + * @param {Object} variables - Query variables + * @param {Object} options - Additional options + * @returns {Promise} Response data + */ +async function queryAnilistFromFile(queryFilePath, variables = {}, options = {}) { + const fs = require('fs'); + const query = fs.readFileSync(queryFilePath, 'utf8'); + return queryAnilist(query, variables, options); +} + +module.exports = { + queryAnilist, + queryAnilistFromFile, + API_URL +}; diff --git a/src/index.js b/src/index.js index 70e3243..4549e01 100644 --- a/src/index.js +++ b/src/index.js @@ -4,8 +4,7 @@ const { Routes } = require('discord-api-types/v10'); const fs = require('fs'); const path = require('path'); const dotenv = require('dotenv'); -const language = require('./language/language_setup.js'); -const { checkBan } = require('./bancheck.js'); +const { getLocalizedMessage } = require('./utils/localizations.js'); dotenv.config(); @@ -28,17 +27,17 @@ for (const folder of commandFolders) { } } -client.once('ready', async () => { - console.log(`${client.user.tag} ${language.__n(`global.ready`)}`); - console.log(`${language.__n(`global.waiting_command`)}`); +client.once('clientReady', async () => { + console.log(`${client.user.tag} ${getLocalizedMessage('global', 'ready')}`); + console.log(`${getLocalizedMessage('global', 'waiting_command')}`); const commandsArray = commands.map(command => command.data.toJSON()); const rest = new REST({ version: '10' }).setToken(token); try { await rest.put(Routes.applicationCommands(client.user.id), { body: commandsArray }); - console.log(`${language.__n(`global.command_register`)}`); + console.log(`${getLocalizedMessage('global', 'command_register')}`); } catch (error) { - console.error(`${language.__n(`global.command_register_error`)}`, error); + console.error(`${getLocalizedMessage('global', 'command_register_error')}`, error); } }); @@ -50,34 +49,44 @@ require('./status.js'); client.on('guildCreate', async (guild) => { try { - console.log(`${language.__n(`global.guild_join`)}: ${guild.name} (ID: ${guild.id}).`); + console.log(`${getLocalizedMessage('global', 'guild_join')}: ${guild.name} (ID: ${guild.id}).`); const commandsArray = commands.map(command => command.data.toJSON()); const rest = new REST({ version: '10' }).setToken(token); await rest.put(Routes.applicationGuildCommands(client.user.id, guild.id), { body: commandsArray }); - console.log(`${language.__n(`global.command_register`)}: ${guild.name} (ID: ${guild.id})`); + console.log(`${getLocalizedMessage('global', 'command_register')}: ${guild.name} (ID: ${guild.id})`); } catch (error) { - console.error(`${language.__n(`global.server_register_error`)} ${guild.name} (ID: ${guild.id})`, error); + console.error(`${getLocalizedMessage('global', 'server_register_error')} ${guild.name} (ID: ${guild.id})`, error); } }); client.on('interactionCreate', async (interaction) => { - if (!interaction.isCommand()) return; - - const { commandName } = interaction; - - const command = commands.get(commandName); - if (!command) return; - - const isBanned = await checkBan(interaction); - if (isBanned) return; - - try { - await command.execute(interaction); - } catch (error) { - console.error(error); - await interaction.reply(`${language.__n(`global.command_error`)}`); + // slash command handle + if (interaction.isCommand()) { + const command = commands.get(interaction.commandName); + if (!command) return; + + try { + await command.execute(interaction); + } catch (error) { + console.error(error); + if (!interaction.replied && !interaction.deferred) { + await interaction.reply(`${getLocalizedMessage('global', 'command_error', interaction.locale)}`); + } + } + } + + // autocomplete handle + else if (interaction.isAutocomplete()) { + const command = commands.get(interaction.commandName); + if (!command || !command.autocomplete) return; + + try { + await command.autocomplete(interaction); + } catch (error) { + console.error('Error handling autocomplete:', error); + } } }); \ No newline at end of file diff --git a/src/language/language/en.json b/src/language/language/en.json deleted file mode 100644 index 05f7ec1..0000000 --- a/src/language/language/en.json +++ /dev/null @@ -1,164 +0,0 @@ -{ - "global": { - "tracemoe_api_limit": "Because of the trace.moe server's limited requirements, you can only use this command once every 60 minutes.", - "ready": "Ready", - "status_ready": "Bot status ready", - "command_error": "An error occurred while executing the command: ", - "command_register": "Command registered", - "waiting_command": "Waiting for register command...", - "guild_join": "Joined a new server: ", - "command_register_error": "Error when registering command: ", - "server_register_error": "Error when registering command for server: ", - "preview_button": "Preview", - "next_button": "Next", - "page": "Page", - "unavailable": "Unavailable", - "no_description": "No description.", - "description": "Description", - "error": "Error: ", - "error_reply": "An error occurred. Please try again later.", - "no_results": "No results found for search: ", - "nsfw_block": "AniChan block content: ", - "nsfw_block_reason": "__Reason:__ To protect your server from Discord's service, AniChan blocks search results containing 18+ content.", - "episodes": "Episodes", - "chapters": "Chapters", - "genres": "Genres", - "status": "Status", - "average_score": "Average score", - "mean_score": "Mean score", - "season": "Season", - "studio": "Studio", - "minute": "minute", - "retry": "Retry" - }, - "help": { - "command_description": "Shows the usage and functions of all commands.", - "command_title": "Command list", - "embed_description": "List of available commands:" - }, - "anime_season": { - "command_description": "Get the anime schedule for a current season.", - "anime_name": "Schedule list" - }, - "anime": { - "command_description": "Search for information about a specific anime.", - "anime_name": "Anime name" - }, - "character": { - "command_description": "Search for information about a specific character.", - "character_name": "Character name", - "anime_appearances": "Anime appearances" - }, - "character_search": { - "command_description": "Search for anime series that contain character.", - "character_name": "Character name", - "anime_list": "List of anime containing characters: " - }, - "manga": { - "command_description": "Search for information about a specific manga.", - "manga_name": "Manga name" - }, - "search": { - "command_description": "Search anime by image. (Supports maximum file size of 25MB)", - "image_link": "Image link", - "cut_black_borders": "Cut black borders", - "similarity": "Similarity", - "appears_episode": "Appears in episode: ", - "file_too_large": "File too large. Maximum file size is 25MB.", - "tracemoe_api_limit": "The request limit for the trace.moe server has been reached. Please try again later.", - "image_option": "upload option", - "upload_image": "Upload image" - }, - "staff": { - "command_description": "Search for information about a specific staff.", - "staff_name": "Staff name", - "staff_info": "Staff info" - }, - "studio": { - "command_description": "Get information about the studio and a list of anime produced by the studio.", - "studio_name": "Studio name", - "product_year": "Product year", - "studio_info": "Studio info", - "product_list": "List of anime produced by the studio" - }, - "trending": { - "command_description": "Get a list of 10 trending anime on AniList.", - "trending_title": "Trending anime", - "trending_description": "Description" - }, - "user": { - "command_description": "Get information about a specific user.", - "user_name": "User name", - "anime_count": "anime count", - "manga_count": "manga count", - "minutes_watched": "minutes watched", - "chapters_read": "chapters read" - }, - "ascii": { - "command_description": "Convert text to ASCII.", - "text": "Text" - }, - "avatar": { - "command_description": "Get the avatar of a user.", - "user_name": "User name", - "requested_by": "Requested by" - }, - "ping": { - "description": "Bot ping." - }, - "weather": { - "command_description": "Get the weather for a specific location.", - "location": "Location", - "longitude": "Longitude", - "latitude": "Latitude", - "degreetype": "Degree type", - "current_temperature": "Current temperature", - "feels_like": "Feels like", - "winddisplay": "Wind", - "humidity": "Humidity", - "observationtime": "Update time" - }, - "language": { - "command_description": "Change the bot's language", - "language_switch": "Bot language has been set to:", - "response_language": "Response language has been set to: ", - "select_language": "Select language", - "language_option": "Language list" - }, - "langlist": { - "command_description": "Get a list of available language & current language.", - "title": "Current language:", - "description": "Language list: " - }, - "bot_stats": { - "description": "Get bot information.", - "title": "Bot information", - "uptime": "Uptime", - "version": "Version", - "disk_usage": "Disk usage", - "os": "Operating system", - "node": "Node.js version", - "library": "Library version" - }, - "popular": { - "command_description": "Get the list of 10 popular anime on AniList." - }, - "en": "English", - "vi": "Vietnamese", - "schedule": { - "command_description": "Get the airing schedule for a specific season.", - "airing_schedule": "Airing schedule" - }, - "userban": { - "bantitle": "You have been banned from using AniChan", - "username": "Username", - "uuid": "UUID", - "bantime": "Ban time", - "reason": "Reason", - "contact": "❗ Contact the bot support server for more details about the ban.", - "noban": "You are not banned." - }, - "random": { - "command_description": "Get random anime." - } -} \ No newline at end of file diff --git a/src/language/language/vi.json b/src/language/language/vi.json deleted file mode 100644 index 4c8ac93..0000000 --- a/src/language/language/vi.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "global": { - "tracemoe_api_limit": "Vì bị giới hạn yêu cầu đến máy chủ trace.moe, bạn chỉ có thể sử dụng lệnh này mỗi 60 phút một lần.", - "ready": "Sẵn sàng", - "status_ready": "Trạng thái bot sẵn sàng", - "command_error": "Đã xảy ra lỗi khi thực hiện lệnh này.", - "command_register": "Đã đăng ký lệnh", - "waiting_command": "Đang đăng kí lệnh...", - "guild_join": "Đã tham gia máy chủ: ", - "command_register_error": "Lỗi khi đăng ký lệnh: ", - "server_register_error": "Lỗi khi đăng ký lệnh cho máy chủ: ", - "preview_button": "Trang trước", - "next_button": "Trang sau", - "unavailable": "không xác định", - "no_description": "Không có mô tả", - "description": "Mô tả", - "error": "Lỗi: ", - "error_reply": "Đã xảy ra lỗi khi tìm kiếm thông tin. Vui lòng thử lại sau.", - "no_results": "Không tìm thấy: ", - "nsfw_block": "AniChan đã chặn kết quả tìm kiếm: ", - "nsfw_block_reason": "__Lý do:__ Để bảo vệ máy chủ của bạn khỏi điều khoản dịch vụ của Discord, AniChan chặn các kết quả tìm kiếm chứa nội dung người lớn.", - "episodes": "Số Tập", - "chapters": "Số Chương", - "genres": "Thể loại", - "status": "Trạng thái", - "average_score": "Điểm đánh giá", - "mean_score": "Điểm xếp hạng", - "season": "Mùa", - "studio": "Studio", - "minute": "phút", - "retry": "Thử lại sau", - "page": "Trang" - }, - "help": { - "command_description": "Hiển thị cách sử dụng và chức năng của tất cả các lệnh.", - "command_title": "Danh sách các lệnh", - "embed_description": "Dưới đây là danh sách lệnh có sẵn: " - }, - "anime": { - "command_description": "Tìm kiếm thông tin về một bộ anime cụ thể.", - "anime_name": "Tên anime" - }, - "character": { - "command_description": "Tìm kiếm thông tin về một nhân vật cụ thể.", - "character_name": "Tên nhân vật", - "anime_appearances": "Xuất hiện trong anime: " - }, - "character_search": { - "command_description": "Tìm kiếm thông tin về một nhân vật cụ thể.", - "character_name": "Tên nhân vật", - "anime_list": "Danh sách anime có chứa nhân vật: " - }, - "manga": { - "command_description": "Tìm kiếm thông tin về một bộ manga cụ thể.", - "manga_name": "Tên manga" - }, - "search": { - "command_description": "Tìm kiếm anime bằng hình ảnh. (Hỗ trợ dung lượng ảnh tối đa 25MB)", - "image_link": "Liên kết đến hình ảnh", - "cut_black_borders": "Cắt bỏ viền đen", - "similarity": "Tỉ lệ trùng khớp", - "appears_episode": "Xuất hiện trong tập: ", - "file_too_large": "Dung lượng ảnh quá lớn. Vui lòng thử lại với ảnh có dung lượng nhỏ hơn 25MB.", - "tracemoe_api_limit": "Đã đạt giới hạn yêu cầu đến máy chủ trace.moe. Vui lòng thử lại sau.", - "image_option": "Chọn kểu tải lên", - "upload_image": "Tải ảnh lên" - }, - "staff": { - "command_description": "Tìm kiếm thông tin về một nhân viên cụ thể.", - "staff_name": "Tên staff", - "staff_info": "Thông tin cá nhân của staff" - }, - "studio": { - "command_description": "Lấy thông tin về studio và danh sách anime được sản xuất bởi studio.", - "studio_name": "Tên studio", - "product_year": "Năm sản xuất", - "studio_info": "Thông tin studio", - "product_list": "Danh sách các anime được sản xuất bởi studio" - }, - "trending": { - "command_description": "Hiển thị danh sách 10 bộ anime đang thịnh hành trên AniList.", - "trending_title": "Danh sách anime đang thịnh hành", - "trending_description": "Danh sách 10 bộ anime đang thịnh hành trên AniList." - }, - "user": { - "command_description": "Xem thông tin về người dùng trên AniList.", - "user_name": "Tên người dùng", - "anime_count": "bộ anime", - "manga_count": "bộ manga", - "minutes_watched": "phút đã xem", - "chapters_read": "chương đã đọc" - }, - "ascii": { - "command_description": "Chuyển văn bản thành ASCII.", - "text": "Văn bản cần chuyển", - "text_limit": "Văn bản không được vượt quá 2000 ký tự." - }, - "avatar": { - "command_description": "Lấy ảnh đại diện của người dùng.", - "user_name": "Tên người dùng", - "requested_by": "Yêu cầu bởi" - }, - "ping": { - "description": "Kiểm tra độ trễ của bot." - }, - "weather": { - "command_description": "Xem thông tin thời tiết của một địa điểm cụ thể.", - "location": "Địa điểm", - "longitude": "Kinh độ", - "latitude": "Vĩ độ", - "degreetype": "Đơn vị nhiệt độ", - "current_temperature": "Nhiệt độ đo được", - "feels_like": "Nhiệt độ cảm nhận", - "winddisplay": "Tốc độ gió", - "humidity": "Độ ẩm", - "observationtime": "Cập nhật lúc" - }, - "language": { - "command_description": "Chuyển đổi ngôn ngữ cho bot.", - "language_option": "Danh sách ngôn ngữ", - "language_switch": "Ngôn ngữ đã thay đổi", - "select_language": "Chọn ngôn ngữ", - "response_language": "Ngôn ngữ hiện tại của bot đã được đặt thành: " - }, - "langlist": { - "command_description": "Xem ngôn ngữ hiện tại của bot và danh sách ngôn ngữ có sẵn.", - "title": "Ngôn ngữ hiện tại của bot: ", - "description": "Danh sách ngôn ngữ có sẵn: " - }, - "bot_stats": { - "description": "Lấy thông tin về bot.", - "title": "Thông tin bot", - "uptime": "Hoạt động", - "version": "Phiên bản", - "disk_usage": "Dung lượng ổ đĩa sử dụng", - "node": "Phiên bản Node.js", - "library": "Phiên bản thư viện", - "os": "Hệ điều hành" - }, - "popular": { - "command_description": "Lấy danh sách 10 anime phổ biến" - }, - "en": "Tiếng Anh", - "vi": "Tiếng Việt", - "schedule": { - "command_description": "Lấy lịch phát sóng anime.", - "airing_schedule": "Lịch phát sóng" - }, - "userban": { - "bantitle": "Bạn đã bị cấm sử dụng AniChan", - "username": "Tên người dùng", - "uuid": "UUID", - "bantime": "Thời gian cấm", - "reason": "Lý do", - "contact": "❗ Liên hệ với quản trị viên trong máy chủ hỗ trợ để biết thêm thông tin.", - "noban": "Bạn không bị cấm sử dụng." - }, - "random": { - "command_description": "Lấy anime ngẫu nhiên." - } -} \ No newline at end of file diff --git a/src/language/language_setup.js b/src/language/language_setup.js deleted file mode 100644 index 51420f4..0000000 --- a/src/language/language_setup.js +++ /dev/null @@ -1,10 +0,0 @@ -const language = require('i18n'); - -language.configure({ - locales: ['vi', 'en'], - directory: __dirname + '/language', - defaultLocale: 'vi', - objectNotation: true, -}); - -module.exports = language; \ No newline at end of file diff --git a/src/queries/random.graphql b/src/queries/random.graphql index a85890a..f313648 100644 --- a/src/queries/random.graphql +++ b/src/queries/random.graphql @@ -1,6 +1,13 @@ -query($page: Int){ - Page(page: $page, perPage: 50) { - media(sort: [SCORE_DESC, ID_DESC], type: ANIME) { +query($page: Int, $sort: [MediaSort], $perPage: Int){ + Page(page: $page, perPage: $perPage) { + pageInfo { + total + currentPage + lastPage + hasNextPage + perPage + } + media(sort: $sort, type: ANIME) { id title { romaji diff --git a/src/status.js b/src/status.js index 45fa79a..a60aad4 100644 --- a/src/status.js +++ b/src/status.js @@ -2,7 +2,7 @@ const { Client, GatewayIntentBits, ActivityType } = require('discord.js'); const dotenv = require('dotenv'); dotenv.config(); const client = new Client({ intents: [GatewayIntentBits.Guilds] }); -const language = require('./language/language_setup.js'); +const { getLocalizedMessage } = require('./utils/localizations.js'); const activities = [ { type: ActivityType.Watching, text: 'anime' }, @@ -15,8 +15,8 @@ function setRandomActivity() { client.user.setActivity(randomActivity.text, { type: randomActivity.type }); } -client.once('ready', () => { - console.log(`${language.__n(`global.status_ready`)}`); +client.once('clientReady', () => { + console.log(`${getLocalizedMessage('global', 'status_ready')}`); setRandomActivity(); setInterval(() => { diff --git a/src/utils/localizations.js b/src/utils/localizations.js new file mode 100644 index 0000000..bf46c06 --- /dev/null +++ b/src/utils/localizations.js @@ -0,0 +1,624 @@ +const commandLocalizations = { + // Help command + help: { + name: 'help', + nameLocalizations: { + vi: 'trợ-giúp' + }, + description: 'Shows the usage and functions of all commands.', + descriptionLocalizations: { + vi: 'Hiển thị cách sử dụng và chức năng của tất cả các lệnh.' + } + }, + + changelog: { + name: 'changelog', + nameLocalizations: { + vi: 'nhật-ký-thay-đổi' + }, + description: 'View the latest changelog.', + descriptionLocalizations: { + vi: 'Xem nhật ký thay đổi mới nhất.' + } + }, + + // Anime commands + anime: { + name: 'anime', + nameLocalizations: {}, + description: 'Search for information about a specific anime.', + descriptionLocalizations: { + vi: 'Tìm kiếm thông tin về một bộ anime cụ thể.' + } + }, + + characters: { + name: 'characters', + nameLocalizations: { + vi: 'nhân-vật' + }, + description: 'Search for information about a specific character.', + descriptionLocalizations: { + vi: 'Tìm kiếm thông tin về một nhân vật cụ thể.' + } + }, + + character_search: { + name: 'charactersearch', + nameLocalizations: { + vi: 'tìm-nhân-vật' + }, + description: 'Search for anime series that contain character.', + descriptionLocalizations: { + vi: 'Tìm kiếm thông tin về một nhân vật cụ thể.' + } + }, + + manga: { + name: 'manga', + nameLocalizations: {}, + description: 'Search for information about a specific manga.', + descriptionLocalizations: { + vi: 'Tìm kiếm thông tin về một bộ manga cụ thể.' + } + }, + + + popular: { + name: 'popular', + nameLocalizations: { + vi: 'phổ-biến' + }, + description: 'Get the list of 10 popular anime on AniList.', + descriptionLocalizations: { + vi: 'Lấy danh sách 10 anime phổ biến.' + } + }, + + random: { + name: 'random', + nameLocalizations: { + vi: 'ngẫu-nhiên' + }, + description: 'Get random anime.', + descriptionLocalizations: { + vi: 'Lấy anime ngẫu nhiên.' + } + }, + + schedule: { + name: 'schedule', + nameLocalizations: { + vi: 'lịch-phát' + }, + description: 'Get the airing schedule for a specific season.', + descriptionLocalizations: { + vi: 'Lấy lịch phát sóng anime.' + } + }, + + search: { + name: 'search', + nameLocalizations: { + vi: 'tìm-kiếm' + }, + description: 'Search anime by image. (Supports maximum file size of 25MB)', + descriptionLocalizations: { + vi: 'Tìm kiếm anime bằng hình ảnh. (Hỗ trợ dung lượng ảnh tối đa 25MB)' + } + }, + + staff: { + name: 'staff', + nameLocalizations: { + vi: 'nhân-viên' + }, + description: 'Search for information about a specific staff.', + descriptionLocalizations: { + vi: 'Tìm kiếm thông tin về một nhân viên cụ thể.' + } + }, + + studio: { + name: 'studio', + nameLocalizations: {}, + description: 'Get information about the studio and a list of anime produced by the studio.', + descriptionLocalizations: { + vi: 'Lấy thông tin về studio và danh sách anime được sản xuất bởi studio.' + } + }, + + trending: { + name: 'trending', + nameLocalizations: { + vi: 'thịnh-hành' + }, + description: 'Get a list of 10 trending anime on AniList.', + descriptionLocalizations: { + vi: 'Hiển thị danh sách 10 bộ anime đang thịnh hành trên AniList.' + } + }, + + user: { + name: 'user', + nameLocalizations: { + vi: 'người-dùng' + }, + description: 'Get information about a specific user.', + descriptionLocalizations: { + vi: 'Xem thông tin về người dùng trên AniList.' + } + }, + + // Other commands + avatar: { + name: 'avatar', + nameLocalizations: { + vi: 'ảnh-đại-diện' + }, + description: 'Get the avatar of a user.', + descriptionLocalizations: { + vi: 'Lấy ảnh đại diện của người dùng.' + } + }, + + stats: { + name: 'stats', + nameLocalizations: { + vi: 'stats' + }, + description: 'Get bot information.', + descriptionLocalizations: { + vi: 'Lấy thông tin về bot.' + } + }, + + weather: { + name: 'weather', + nameLocalizations: { + vi: 'thời-tiết' + }, + description: 'Get the weather for a specific location.', + descriptionLocalizations: { + vi: 'Xem thông tin thời tiết của một địa điểm cụ thể.' + } + }, + + ratelimit: { + name: 'ratelimit', + nameLocalizations: { + vi: 'giới-hạn-yêu-cầu' + }, + description: 'Get information about the rate limit of all APIs.', + descriptionLocalizations: { + vi: 'Xem thông tin về giới hạn yêu cầu của tất cả API.' + } + }, + + donate: { + name: 'donate', + nameLocalizations: { + vi: 'ủng-hộ' + }, + description: 'Get information about how to support the project.', + descriptionLocalizations: { + vi: 'Lấy thông tin về cách ủng hộ dự án.' + } + }, + + // Minecraft commands + mcuser: { + name: 'mcuser', + nameLocalizations: { + vi: 'mc-người-dùng' + }, + description: 'Get information about a Minecraft user.', + descriptionLocalizations: { + vi: 'Lấy thông tin về người chơi Minecraft.' + } + } +}; + +// Response messages +const responseMessages = { + global: { + en: { + tracemoe_api_limit: "Because of the trace.moe server's limited requirements, you can only use this command once every 60 minutes.", + ready: "Ready", + status_ready: "Bot status ready", + command_error: "An error occurred while executing the command: ", + command_register: "Command registered", + waiting_command: "Waiting for register command...", + guild_join: "Joined a new server: ", + command_register_error: "Error when registering command: ", + server_register_error: "Error when registering command for server: ", + preview_button: "Preview", + next_button: "Next", + page: "Page", + unavailable: "Unavailable", + no_description: "No description.", + description: "Description", + error: "Error: ", + error_reply: "An error occurred. Please try again later.", + no_results: "No results found for search: ", + nsfw_block: "AniChan block content: ", + nsfw_block_reason: "__Reason:__ To protect your server from Discord's service, AniChan blocks search results containing 18+ content.", + episodes: "Episodes", + chapters: "Chapters", + genres: "Genres", + status: "Status", + average_score: "Average score", + mean_score: "Mean score", + season: "Season", + studio: "Studio", + minute: "minute", + retry: "Retry", + random_again: "Random Again", + view_anilist: "View on AniList" + }, + vi: { + tracemoe_api_limit: "Vì bị giới hạn yêu cầu đến máy chủ trace.moe, bạn chỉ có thể sử dụng lệnh này mỗi 60 phút một lần.", + ready: "Sẵn sàng", + status_ready: "Trạng thái bot sẵn sàng", + command_error: "Đã xảy ra lỗi khi thực hiện lệnh này.", + command_register: "Đã đăng ký lệnh", + waiting_command: "Đang đăng kí lệnh...", + guild_join: "Đã tham gia máy chủ: ", + command_register_error: "Lỗi khi đăng ký lệnh: ", + server_register_error: "Lỗi khi đăng ký lệnh cho máy chủ: ", + preview_button: "Trang trước", + next_button: "Trang sau", + unavailable: "không xác định", + no_description: "Không có mô tả", + description: "Mô tả", + error: "Lỗi: ", + error_reply: "Đã xảy ra lỗi khi tìm kiếm thông tin. Vui lòng thử lại sau.", + no_results: "Không tìm thấy: ", + nsfw_block: "AniChan đã chặn kết quả tìm kiếm: ", + nsfw_block_reason: "__Lý do:__ Để bảo vệ máy chủ của bạn khỏi điều khoản dịch vụ của Discord, AniChan chặn các kết quả tìm kiếm chứa nội dung người lớn.", + episodes: "Số Tập", + chapters: "Số Chương", + genres: "Thể loại", + status: "Trạng thái", + average_score: "Điểm đánh giá", + mean_score: "Điểm xếp hạng", + season: "Mùa", + studio: "Studio", + minute: "phút", + retry: "Thử lại sau", + page: "Trang", + random_again: "Random Lại", + view_anilist: "Xem trên AniList" + } + }, + + anime: { + en: { + anime_name: "Anime name" + }, + vi: { + anime_name: "Tên anime" + } + }, + + character: { + en: { + command_description: "Search for information about a specific character.", + character_name: "Character name", + anime_appearances: "Anime appearances" + }, + vi: { + command_description: "Tìm kiếm thông tin về một nhân vật cụ thể.", + character_name: "Tên nhân vật", + anime_appearances: "Xuất hiện trong anime: " + } + }, + + character_search: { + en: { + command_description: "Search for anime series that contain character.", + character_name: "Character name", + anime_list: "List of anime containing characters: " + }, + vi: { + command_description: "Tìm kiếm các bộ anime có chứa nhân vật.", + character_name: "Tên nhân vật", + anime_list: "Danh sách anime chứa nhân vật: " + } + }, + + manga: { + en: { + manga_name: "Manga name" + }, + vi: { + manga_name: "Tên manga" + } + }, + + studio: { + en: { + studio_name: "Studio name", + product_year: "Year", + studio_info: "Studio Information:", + product_list: "Productions by" + }, + vi: { + studio_name: "Tên studio", + product_year: "Năm", + studio_info: "Thông tin Studio:", + product_list: "Sản phẩm của" + } + }, + + staff: { + en: { + staff_name: "Staff name", + staff_info: "Staff Information" + }, + vi: { + staff_name: "Tên nhân viên", + staff_info: "Thông tin Nhân viên" + } + }, + + schedule: { + en: { + airing_schedule: "Airing Schedule" + }, + vi: { + airing_schedule: "Lịch Phát Sóng" + } + }, + + search: { + en: { + image_option: "Search by image", + image_link: "Image URL", + upload_image: "Upload image", + cut_black_borders: "Cut black borders", + similarity: "Similarity", + appears_episode: "Appears in episode: ", + file_too_large: "File too large. Maximum file size is 25MB.", + tracemoe_api_limit: "The request limit for the trace.moe server has been reached. Please try again later." + }, + vi: { + image_option: "Tìm kiếm bằng hình ảnh", + image_link: "Liên kết ảnh", + upload_image: "Tải ảnh lên", + cut_black_borders: "Cắt viền đen", + similarity: "Tỉ lệ trùng khớp", + appears_episode: "Xuất hiện trong tập: ", + file_too_large: "Dung lượng ảnh quá lớn. Vui lòng thử lại với ảnh có dung lượng nhỏ hơn 25MB.", + tracemoe_api_limit: "Đã đạt giới hạn yêu cầu đến máy chủ trace.moe. Vui lòng thử lại sau." + } + }, + + user: { + en: { + user_name: "Username", + anime_count: "anime count", + manga_count: "manga count", + minutes_watched: "minutes watched", + chapters_read: "chapters read" + }, + vi: { + user_name: "Tên người dùng", + anime_count: "Anime", + anime_count_value: "bộ anime", + manga_count: "Manga", + manga_count_value: "bộ manga", + minutes_watched: "Đã xem", + minutes_watched_value: "phút đã xem", + chapters_read: "Đã đọc", + chapters_read_value: "chương đã đọc" + + } + }, + + help: { + en: { + command_title: "Command list", + embed_description: "List of available commands:" + }, + vi: { + command_title: "Danh sách các lệnh", + embed_description: "Dưới đây là danh sách lệnh có sẵn: " + } + }, + + changelog: { + en: { + no_release: "No release found.", + no_changelog: "No changelog found.", + changelog_title: "Latest Changelog", + changelog_description: "Here are the latest changes and updates:" + }, + vi: { + no_release: "Không tìm thấy bản phát hành.", + no_changelog: "Không tìm thấy nhật ký thay đổi.", + changelog_title: "Nhật Ký Thay Đổi Mới Nhất", + changelog_description: "Dưới đây là những thay đổi và cập nhật mới nhất:" + } + }, + + weather: { + en: { + location: "Location name", + longitude: "Longitude", + latitude: "Latitude", + degreetype: "Degree type", + current_temperature: "Current temperature", + feels_like: "Feels like", + winddisplay: "Wind", + humidity: "Humidity", + observationtime: "Update time" + }, + vi: { + location: "Tên địa điểm", + longitude: "Kinh độ", + latitude: "Vĩ độ", + degreetype: "Đơn vị nhiệt độ", + current_temperature: "Nhiệt độ đo được", + feels_like: "Nhiệt độ cảm nhận", + winddisplay: "Tốc độ gió", + humidity: "Độ ẩm", + observationtime: "Cập nhật lúc" + } + }, + + avatar: { + en: { + user_name: "User", + requested_by: "Requested by" + }, + vi: { + user_name: "Người dùng", + requested_by: "Được yêu cầu bởi" + } + }, + + stats: { + en: { + title: "Bot Statistics", + ping: "Ping", + uptime: "Uptime", + version: "Version", + cpu: "CPU", + ram: "RAM", + disk_usage: "Disk Usage", + os: "Operating System", + node: "Node.js Version", + library: "Library" + }, + vi: { + title: "Thống Kê", + ping: "Ping", + uptime: "Thời Gian Hoạt Động", + version: "Phiên Bản", + cpu: "CPU", + ram: "RAM", + disk_usage: "Dung Lượng Đĩa", + os: "Hệ Điều Hành", + node: "Phiên Bản Node.js", + library: "Thư Viện" + } + }, + + // remove if you dont have a donate command - immizsx + donate: { + en: { + title: "Sponsor Project", + description: "If you’d like to support us, your donation will help keep the bot running and improve its features.", + footer: "Thank you for your consideration! <3" + }, + vi: { + title: "Ủng Hộ Dự án", + description: "Nếu bạn muốn ủng hộ chúng tôi, sự đóng góp của bạn sẽ giúp duy trì và cải thiện các tính năng của bot.", + footer: "Cảm ơn bạn đã xem xét! <3" + } + }, + module: { + en: { + command_description: "Manage bot modules.", + module_list_title: "Module List", + module_list_description: "List of all available modules and their statuses.", + module_id: "Module ID", + module_name: "Module Name", + module_status: "Status", + enabled: "Enabled", + disabled: "Disabled", + toggle_success: "Module status updated successfully.", + toggle_fail: "Failed to update module status. Please try again.", + permission_denied: "You need 'Manage Server' permission to use this command.", + invalid_module: "Invalid module ID. Please choose a valid module." + }, + vi: { + command_description: "Quản lý các module của bot.", + module_list_title: "Danh Sách Module", + module_list_description: "Danh sách tất cả các module có sẵn và trạng thái của chúng.", + module_id: "ID Module", + module_name: "Tên Module", + module_status: "Trạng Thái", + enabled: "Đã Bật", + disabled: "Đã Tắt", + toggle_success: "Cập nhật trạng thái module thành công.", + toggle_fail: "Cập nhật trạng thái module thất bại. Vui lòng thử lại.", + permission_denied: "Bạn cần quyền 'Quản lý máy chủ' để sử dụng lệnh này.", + invalid_module: "ID module không hợp lệ. Vui lòng chọn một module hợp lệ." + } + }, + hyperlink: { + en: { + name: "Fake Hyperlink Detector", + description: "Detect fake link and send alert for suspicious hyperlinks.", + embed_Title: "Fake Hyperlink Detected", + embed_description: "A potentially malicious hyperlink has been detected and removed.", + embed_field_sender: "Sender", + embed_field_time: "Time", + embed_field_channel: "Channel", + embed_field_displaylink: "Displayed Link", + embed_field_actuallink: "Actual Link", + embed_field_messagecontent: "Message Content", + embed_footer: "Message ID: " + + }, + vi: { + name: "Trình Phát Hiện Liên Kết Giả", + description: "Phát hiện liên kết giả và gửi cảnh báo cho các liên kết nghi ngờ.", + embed_Title: "Liên Kết Giả Đã Bị Phát Hiện", + embed_description: "Một liên kết có thể độc hại đã bị phát hiện và bị xóa.", + embed_field_sender: "Người Gửi", + embed_field_time: "Thời Gian", + embed_field_channel: "Kênh", + embed_field_displaylink: "Liên Kết Được Hiển Thị", + embed_field_actuallink: "Liên Kết Thực", + embed_field_messagecontent: "Nội Dung Tin Nhắn", + embed_footer: "ID Tin Nhắn: " + } + } +}; + +/** + * Get localized message based on interaction locale + * @param {string} section - Message section + * @param {string} key - Message key + * @param {string} locale - Discord locale ('en-US', 'vi') + * @returns {string} Localized message + */ +function getLocalizedMessage(section, key, locale = 'en-US') { + // Convert locale + const lang = locale.startsWith('vi') ? 'vi' : 'en'; + + if (responseMessages[section] && responseMessages[section][lang] && responseMessages[section][lang][key]) { + return responseMessages[section][lang][key]; + } + + // Fallback to English + if (responseMessages[section] && responseMessages[section]['en'] && responseMessages[section]['en'][key]) { + return responseMessages[section]['en'][key]; + } + + return key; // Return key if no translation found +} + +/** + * Get command localization data for slash command registration + * @param {string} commandName - Name of the command + * @returns {Object} Command localization data + */ +function getCommandLocalization(commandName) { + return commandLocalizations[commandName] || { + name: commandName, + description: `Command: ${commandName}`, + nameLocalizations: {}, + descriptionLocalizations: {} + }; +} + +module.exports = { + commandLocalizations, + responseMessages, + getLocalizedMessage, + getCommandLocalization +}; diff --git a/src/utils/textParser.js b/src/utils/textParser.js new file mode 100644 index 0000000..cd3087e --- /dev/null +++ b/src/utils/textParser.js @@ -0,0 +1,60 @@ +/** + * Parse and clean HTML content from AniList API responses + * @param {string} text - Raw text with HTML tags + * @param {number} maxLength - Maximum length to truncate to (optional) + * @returns {string} Cleaned text + */ +function parseHtmlText(text, maxLength = null) { + if (!text) return ''; + + let cleaned = text; + + // Replace
tags with newlines + cleaned = cleaned.replace(//gi, '\n'); + + // Replace

and with double newlines for paragraph separation + cleaned = cleaned.replace(/<\/(p|div)>/gi, '\n\n'); + + // Remove all other HTML tags + cleaned = cleaned.replace(/<[^>]+>/g, ''); + + // Decode HTML entities + cleaned = cleaned + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/ /g, ' ') + .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec)) + .replace(/&#x([0-9a-f]+);/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16))); + + // Remove excessive newlines (more than 2 consecutive) + cleaned = cleaned.replace(/\n{3,}/g, '\n\n'); + + // Trim whitespace + cleaned = cleaned.trim(); + + // Truncate if maxLength is specified + if (maxLength && cleaned.length > maxLength) { + cleaned = cleaned.slice(0, maxLength).trim(); + // Try to cut at the last complete sentence or word + const lastPeriod = cleaned.lastIndexOf('.'); + const lastSpace = cleaned.lastIndexOf(' '); + + if (lastPeriod > maxLength * 0.8) { + cleaned = cleaned.slice(0, lastPeriod + 1); + } else if (lastSpace > maxLength * 0.8) { + cleaned = cleaned.slice(0, lastSpace); + } + + cleaned += '...'; + } + + return cleaned; +} + +module.exports = { + parseHtmlText +}; diff --git a/src/utils/unregister_slash_commands.js b/src/utils/unregister_slash_commands.js new file mode 100644 index 0000000..4e73587 --- /dev/null +++ b/src/utils/unregister_slash_commands.js @@ -0,0 +1,21 @@ +const { REST } = require('@discordjs/rest'); +const { Routes } = require('discord.js'); +require('dotenv').config(); + +const token = process.env.BOT_TOKEN; +const clientId = process.env.CLIENT_ID; + +const rest = new REST({ version: '10' }).setToken(token); + +(async () => { + try { + console.log('Started removing all slash commands.'); + + // Delete all global commands + await rest.put(Routes.applicationCommands(clientId), { body: [] }); + console.log('Successfully removed all global slash commands.'); + + } catch (error) { + console.error('Error removing slash commands:', error); + } +})(); \ No newline at end of file diff --git a/src/utli/unregister_slash_commands.js b/src/utli/unregister_slash_commands.js deleted file mode 100644 index 3a8910a..0000000 --- a/src/utli/unregister_slash_commands.js +++ /dev/null @@ -1,40 +0,0 @@ -const { REST } = require('@discordjs/rest'); -const { Routes } = require('discord.js'); -require('dotenv').config(); - -const token = process.env.BOT_TOKEN; -const clientId = process.env.CLIENT_ID; - -const rest = new REST({ version: '10' }).setToken(token); - -(async () => { - try { - console.log('Started removing all slash commands.'); - - const globalCommands = await rest.get(Routes.applicationCommands(clientId)); - const globalCommandIds = globalCommands.map(command => command.id); - - for (const commandId of globalCommandIds) { - await rest.delete(Routes.applicationCommand(clientId, commandId)); - } - - console.log('Successfully removed all global slash commands.'); - - const guilds = await rest.get(Routes.userGuilds()); - - for (const guild of guilds) { - const guildCommands = await rest.get(Routes.applicationGuildCommands(clientId, guild.id)); - const guildCommandIds = guildCommands.map(command => command.id); - - for (const commandId of guildCommandIds) { - await rest.delete(Routes.applicationGuildCommand(clientId, guild.id, commandId)); - } - - console.log(`Successfully removed all slash commands in guild ${guild.id}.`); - } - - console.log('Successfully removed all slash commands in all guilds.'); - } catch (error) { - console.error('Error removing slash commands:', error); - } -})(); \ No newline at end of file