diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c81b8d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +.env diff --git a/.gitignore b/.gitignore index c6bba59..8056630 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Sqlite +sqlite +sqlite3 +*.sqlite +*.sqlite3 + # Logs logs *.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..27e6d6e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Nodejs +FROM node:22.5.1 + +WORKDIR /app + +COPY package*.json ./ + +# Remove any existing node_modules and package-lock.json to ensure a clean install. +RUN rm -rf node_modules package-lock.json + +# Install Node.js dependencies. +RUN npm install --build-from-source + +COPY . . + +CMD ["npm", "start"] diff --git a/README.md b/README.md index 95e5a1c..a8bdd64 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ -# gobattlebot -GoBattle.io Discord Bot +# GoBattle.io Discord Bot + +This Discord bot is a community project initiated by __Kuwazy#703__ and provides Discord users with utility commands related to the MMORPG [GoBattle.io](https://gobattle.io/). +The door is open to all potential contributors. The project is now managed by Shinobit. + +----------------- + +## Configuration and prerequisites +- You must have a Discord app that you have previously created in the Discord Developer Portal. +- You also need to have nodejs v22 or higher installed on your server. +- You must have the application token and configure the `.env` file accordingly. + +## To start the project, open a terminal at the root of the project and use the following commands: +(Make sure you have set the environment variables correctly in the `.env` file.) +The `.env` file must be created manually at the root of the project and should look like this: + +```env +TOKEN = "token_discord_app" +# The GUILD_ADMIN_ID variable gives certain administrator privileges to the specified guilds. +GUILD_ADMIN_ID = ["380588354934276097"] +``` + +Assume you are on a Linux environment: + +1. Install nodejs and npm: +```bash +sudo apt install -y nodejs +sudo apt install -y npm +``` + +2. Check the installation: +```bash +node -v +npm -v +``` + âš ī¸ Note that the minimum node js version for the project must be v22.5.0. âš ī¸ + +3. Install dependencies: +```bash +npm install +``` + +4. Start the bot. +```bash +npm start +``` +OR +```bash +node --env-file=.env ./src/index.js +``` + +## If you want to start the project from docker: + +Suppose you already have Docker installed on your system: + +1. Build the Docker image: +```bash +sudo docker-compose build +``` + +2. Start container: +```bash +sudo docker-compose up +``` + +Brutal method to stop all containers: +```bash +sudo docker-compose down +``` + +## Screenshot Gallery +

+ + + + + + +

diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4921916 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.8" + +services: + bot: + build: . + container_name: gobattleio_bot + env_file: + - .env + volumes: + - .:/app + command: ["npm", "start"] + restart: unless-stopped diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6b0fc82 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,283 @@ +{ + "name": "gobattlebot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gobattlebot", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "discord.js": "^14.14.1" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.8.2.tgz", + "integrity": "sha512-6wvG3QaCjtMu0xnle4SoOIeFB4y6fKMN6WZfy3BMKJdQQtPLik8KGzDwBVL/+wTtcE/ZlFjgEk74GublyEVZ7g==", + "dependencies": { + "@discordjs/formatters": "^0.4.0", + "@discordjs/util": "^1.1.0", + "@sapphire/shapeshift": "^3.9.7", + "discord-api-types": "0.37.83", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "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==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.4.0.tgz", + "integrity": "sha512-fJ06TLC1NiruF35470q3Nr1bi95BdvKFAF+T5bNfZJ4bNdqZ3VZ+Ttg6SThqTxm6qumSG3choxLBHMC69WXNXQ==", + "dependencies": { + "discord-api-types": "0.37.83" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.3.0.tgz", + "integrity": "sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "0.37.83", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.2", + "undici": "6.13.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.0.tgz", + "integrity": "sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.0.tgz", + "integrity": "sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==", + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.1.1.tgz", + "integrity": "sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.3.0", + "@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.83", + "tslib": "^2.6.2", + "ws": "^8.16.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.0.tgz", + "integrity": "sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.2.tgz", + "integrity": "sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.7.tgz", + "integrity": "sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==", + "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==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.4.tgz", + "integrity": "sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.83", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.83.tgz", + "integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==" + }, + "node_modules/discord.js": { + "version": "14.15.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.15.3.tgz", + "integrity": "sha512-/UJDQO10VuU6wQPglA4kz2bw2ngeeSbogiIPx/TsnctfzV/tNf+q+i1HlgtX1OGpeOBpJH9erZQNO5oRM2uAtQ==", + "dependencies": { + "@discordjs/builders": "^1.8.2", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.4.0", + "@discordjs/rest": "^2.3.0", + "@discordjs/util": "^1.1.0", + "@discordjs/ws": "^1.1.1", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "0.37.83", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "2.6.2", + "undici": "6.13.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "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==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/undici": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.13.0.tgz", + "integrity": "sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..652b62b --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "gobattlebot", + "version": "1.0.0", + "description": "I am a Bot capable of giving you useful information about the MMORPG GoBattle.io.", + "main": "./src/index.js", + "scripts": { + "start": "node --env-file=.env --experimental-sqlite ./src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Shinobit/gobattlebot.git" + }, + "keywords": [], + "author": "Shinobit LLC, Kuwazy (Samuel Marzin), and contributors", + "license": "MIT", + "bugs": { + "url": "https://github.com/Shinobit/gobattlebot/issues" + }, + "homepage": "https://github.com/Shinobit/gobattlebot#readme", + "dependencies": { + "discord.js": "^14.14.1" + } +} diff --git a/screenshot_gallery/info.png b/screenshot_gallery/info.png new file mode 100644 index 0000000..b2c348d Binary files /dev/null and b/screenshot_gallery/info.png differ diff --git a/screenshot_gallery/item_ultrarare_drop.png b/screenshot_gallery/item_ultrarare_drop.png new file mode 100644 index 0000000..9ce7f48 Binary files /dev/null and b/screenshot_gallery/item_ultrarare_drop.png differ diff --git a/screenshot_gallery/ranking_king.png b/screenshot_gallery/ranking_king.png new file mode 100644 index 0000000..c19a0f5 Binary files /dev/null and b/screenshot_gallery/ranking_king.png differ diff --git a/screenshot_gallery/speedrun.png b/screenshot_gallery/speedrun.png new file mode 100644 index 0000000..c4cadfd Binary files /dev/null and b/screenshot_gallery/speedrun.png differ diff --git a/screenshot_gallery/user_friend_list.png b/screenshot_gallery/user_friend_list.png new file mode 100644 index 0000000..0a356b0 Binary files /dev/null and b/screenshot_gallery/user_friend_list.png differ diff --git a/screenshot_gallery/user_info.png b/screenshot_gallery/user_info.png new file mode 100644 index 0000000..7623b0e Binary files /dev/null and b/screenshot_gallery/user_info.png differ diff --git a/src/commands/asset.js b/src/commands/asset.js new file mode 100644 index 0000000..8905d4f --- /dev/null +++ b/src/commands/asset.js @@ -0,0 +1,215 @@ +const {SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, ComponentType} = require("discord.js"); +const zlib = require("zlib"); +const {restrict_text} = require("../utils.js"); + +const asset_command = new SlashCommandBuilder(); +asset_command.setName("asset"); +asset_command.setDescription("Command relating to game data files (client side)."); +asset_command.addSubcommand((subcommand) => { + subcommand.setName("info"); + subcommand.setDescription("Get a specific client file."); + subcommand.addIntegerOption((option) => { + option.setName("file_id"); + option.setDescription("The identifier of the item."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +asset_command.addSubcommand((subcommand) => { + subcommand.setName("list"); + subcommand.setDescription("Get the list of files with their respective ID."); + + return subcommand; +}); + +async function get_asset(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + if (subcommand == "list"){ + await get_list(interaction, client); + }else if (subcommand == "info"){ + await get_info(interaction, client); + }else{ + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_list(interaction, client){ + await interaction.deferReply(); + + try{ + const platform = "iOS"; + const version = 115; + + const response = await fetch(`https://gobattle.io/api.php/bootstrap/${version}?platform=${platform}&ud=`); + + if (!response.ok){ + await interaction.editReply(`Unable to recover information from game files.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data = await response.json(); + + const embed = new EmbedBuilder(); + embed.setTitle("đŸ—‚ī¸ List of assets đŸ—‚ī¸"); + + const header_description = "List of assets with their respective identifiers:\n"; + const max_items_by_pages = 10; + const pages = new Array(Math.ceil(data.files.length / max_items_by_pages)); + + let current_page = -1; + for (let i = 0; i < data.files.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = header_description; + } + + const file = data.files[i]; + const is_compressed = file.file.endsWith("$"); + const file_name = is_compressed ? file.file.slice(0, -1) : file.file; + + if (is_compressed){ + pages[current_page] += `* **${file_name}**#${i + 1}\n`; + }else{ + const url = new URL(`${data.filesBaseURL}/${file_name}`); + pages[current_page] += `* **[${file_name}](${url})**#${i + 1}\n`; + } + } + + current_page = 1; + embed.setDescription(pages[current_page - 1] || "***There are no items to display in this list at the moment...***"); + + const nb_pages = pages.length || 1; + + embed.setFooter({text: `Page ${current_page}/${nb_pages}`}); + embed.setTimestamp(); + + const previous_button = new ButtonBuilder(); + previous_button.setCustomId("previous"); + previous_button.setEmoji("â—€ī¸"); + previous_button.setStyle(ButtonStyle.Primary); + previous_button.setDisabled(current_page == 1); + + const next_button = new ButtonBuilder(); + next_button.setCustomId("next"); + next_button.setEmoji("â–ļī¸"); + next_button.setStyle(ButtonStyle.Primary); + next_button.setDisabled(current_page == nb_pages); + + const row = new ActionRowBuilder(); + row.addComponents(previous_button, next_button); + + const response_interaction = await interaction.editReply({ + embeds: [embed], + components: [row] + }); + + function collector_filter(m){ + const result = m.user.id == interaction.user.id; + + if (!result){ + m.reply({content: "You cannot interact with a command that you did not initiate yourself.", ephemeral: true}).catch((error) => { + console.error(error); + }); + } + + return result; + } + + async function button_interaction_logic(response_interaction){ + try{ + const confirmation = await response_interaction.awaitMessageComponent({filter: collector_filter, componentType: ComponentType.Button, time: 60_000}); + + if (confirmation.customId === "previous"){ + current_page--; + } else if (confirmation.customId === "next"){ + current_page++; + } + + embed.setDescription(pages[current_page - 1]); + embed.setFooter({text: `Page ${current_page}/${nb_pages}`}); + previous_button.setDisabled(current_page == 1); + next_button.setDisabled(current_page == nb_pages); + + response_interaction = await confirmation.update({embeds: [embed], components: [row]}); + await button_interaction_logic(response_interaction); + }catch (_error){ + await interaction.editReply({content: "-# ⓘ This interaction has expired, please use the command again to be able to navigate the list.", components: []}); + } + } + + await button_interaction_logic(response_interaction); + }catch(error){ + await interaction.editReply(`Unable to retrieve file list. \nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_info(interaction, client){ + await interaction.deferReply(); + + try{ + const platform = "iOS"; + const version = 115; + + const response = await fetch(`https://gobattle.io/api.php/bootstrap/${version}?platform=${platform}&ud=`); + + if (!response.ok){ + await interaction.editReply(`Unable to recover information from game files.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data = await response.json(); + + const file_id = interaction.options.get("file_id")?.value; + const nb_files = data.files.length; + if (file_id < 1 || file_id > nb_files){ + await interaction.editReply(`Invalid file ID, try another identifier between 1 and ${nb_files} inclusive.`); + return; + } + + const file_data = data.files[file_id - 1]; + + const url = new URL(`${data.filesBaseURL}/${file_data.file}`); + const response_secondary = await fetch(url); + + if (!response_secondary.ok){ + await interaction.editReply(`Unable to recover information from game files.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const array_buffer = await response_secondary.arrayBuffer(); + const file_buffer = Buffer.from(array_buffer); + const is_compressed = file_data.file.endsWith("$"); + + const attachment = new AttachmentBuilder(is_compressed ? zlib.inflateSync(file_buffer) : file_buffer); + attachment.name = is_compressed ? file_data.file.slice(0, -1) : file_data.file; + + const embed = new EmbedBuilder(); + embed.setTitle(restrict_text(`GoBattle.io Asset __\"${attachment.name}\"__.`, 60)); + embed.setColor(0x500000); + + embed.addFields( + {name: "> đŸˇī¸ __ID__", value: `> ${file_id}`, inline: true}, + {name: "> 🔗 __Source__", value: `> [Click here](${url.href})`, inline: true}, + {name: "> đŸ—œī¸ __Compressed__", value: `> ${is_compressed ? "zlib": "_No_"}`, inline: true} + ); + + embed.setFooter({text: "Š SHINOBIT LLC"}); + embed.setTimestamp(); + + await interaction.editReply({ + embeds: [embed], + files: [attachment] + }); + }catch(error){ + await interaction.editReply(`Unable to recover file.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +exports.get_asset = get_asset; +exports.asset_command = asset_command; diff --git a/src/commands/date_new_king.js b/src/commands/date_new_king.js new file mode 100644 index 0000000..01f27ff --- /dev/null +++ b/src/commands/date_new_king.js @@ -0,0 +1,15 @@ +const {SlashCommandBuilder} = require("discord.js"); +const {get_utc_time_next_king} = require("../utils.js"); + +const date_new_king_command = new SlashCommandBuilder(); +date_new_king_command.setName("get_date_new_king"); +date_new_king_command.setDescription("Get the date and time remaining before the next king arrives."); + +async function get_date_new_king(interaction, client){ + const utc = Math.floor(get_utc_time_next_king() / 1000); + + await interaction.reply(`New king on ().`); +} + +exports.get_date_new_king = get_date_new_king; +exports.date_new_king_command = date_new_king_command; diff --git a/src/commands/dungeon.js b/src/commands/dungeon.js new file mode 100644 index 0000000..3af6ab8 --- /dev/null +++ b/src/commands/dungeon.js @@ -0,0 +1,190 @@ +const {SlashCommandBuilder, EmbedBuilder} = require("discord.js"); +const {restrict_text, send_embed_layout, is_my_developer, application_emoji_cache} = require("../utils.js"); +const {dungeon_list} = require("../dungeon_list.json"); +const {ultra_list} = require("../ultralist.json"); + +const items_map = new Map(); +for (const item_data of ultra_list){ + if (typeof item_data.id == "number"){ + items_map.set(item_data.id, item_data); + } +} + +const dungeon_command = new SlashCommandBuilder(); +dungeon_command.setName("dungeon"); +dungeon_command.setDescription("Command relating to dungeons and other maps in the game."); +dungeon_command.addSubcommand((subcommand) => { + subcommand.setName("info"); + subcommand.setDescription("Obtain the characteristics of a dungeon."); + subcommand.addIntegerOption((option) => { + option.setName("dungeon_id"); + option.setDescription("The dungeon identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +dungeon_command.addSubcommand((subcommand) => { + subcommand.setName("list"); + subcommand.setDescription("Get the list of all dungeons in the game with their respective ID."); + + return subcommand; +}); + +async function get_dungeon(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand){ + case "info": + await get_info(interaction, client); + break; + case "list": + await get_list(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_info(interaction, client){ + if (!is_my_developer(client, interaction.user)){ + await interaction.reply("This subcommand is in development and cannot be used at this time."); + return; + } + + await interaction.deferReply(); + + try{ + const response = await fetch("https://gobattle.io/api.php/v1/stats/ultrarare/drops"); + + if (!response.ok){ + await interaction.editReply("Unable to obtain general information on the general frequency of obtaining ultrarares in Gobattle. There is a problem with the Gobattle API."); + return; + } + + const data = { + name: "Frostbite Dungeon", + id: 57, + level: null, + ultras: [ + { + id: 2, + rl: 500, + probability: 2.92 + }, + { + id: 3, + rl: 500, + probability: 2.92 + }, + { + id: 4, + rl: 3000, + probability: 17.54 + }, + { + id: 6, + rl: 4000, + probability: 23.39 + }, + { + id: 36, + rl: 5000, + probability: 29.24 + }, + { + id: 42, + rl: 3000, + probability: 17.54 + }, + { + id: 116, + rl: 100, + probability: 0.58 + }, + { + id: 168, + rl: 10000, + probability: 5.85 + } + ] + }; + + const embed = new EmbedBuilder(); + + const dungeon_emoji = "🏰"; + embed.setTitle(`${dungeon_emoji} ${data.name}#${data.id} ${dungeon_emoji}`); + + const header_description = "Here is the list of Ultrarares that you could find in this dungeon:"; + const max_items_by_pages = 8; + const pages = new Array(Math.ceil(data.ultras.length / max_items_by_pages)); + const unknown_item_emoji = application_emoji_cache.get("item_91") || "đŸ“Ļ"; + const id_emoji = application_emoji_cache.get("item_348") || "đŸˇī¸"; + const level_emoji = application_emoji_cache.get("compass_item121") || "đŸ’Ē"; + const attendance_emoji = "đŸ‘Ĩ"; + + let current_page = -1; + for (let i = 0; i < data.ultras.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const ultra_data = data.ultras[i]; + const ultra_info = items_map.get(parseInt(ultra_data.id, 10)); + const item_emoji = application_emoji_cache.get(ultra_info?.emoji) || unknown_item_emoji; + pages[current_page] += `* ${item_emoji} **${restrict_text(ultra_info?.name || "*Unknow?*", 45)}**#${ultra_data.id}: \`RL: ${ultra_data.rl}\` \`PB: ${ultra_data.probability}%\`\n`; + } + + embed.addFields( + {name: `> ${id_emoji} __Identifier__`, value: `> ${data.id}`, inline: true}, + {name: `> ${level_emoji} __Recommended level__`, value: `> ${"*Unknow?*"}`, inline: true}, + {name: `> ${attendance_emoji} __Attendance__`, value: `> ${"*Unknow?*"}`, inline: true} + ); + + embed.setTimestamp(); + + const message_content = "Dev testing..."; + await send_embed_layout(interaction, embed, pages, header_description, message_content); + }catch(error){ + await interaction.editReply(`Unable to generate template.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_list(interaction, _client){ + await interaction.deferReply(); + + const embed = new EmbedBuilder(); + embed.setTitle("🏰 Dungeon List 🏰"); + + const header_description = "Note that this list is not updated in real time unlike other lists. This is a pre-list awaiting the GoBattle.io API update."; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(dungeon_list.length / max_items_by_pages)); + + let current_page = -1; + for (let i = 0; i < dungeon_list.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const dungeon_data = dungeon_list[i]; + pages[current_page] += `* **${restrict_text(dungeon_data.name || "*Unknow?*", 45)}**#${dungeon_data.id}: \`${dungeon_data.min_level || "Unknow?"}đŸ’Ē\`\n`; + } + + embed.addFields( + {name: "> đŸ”ĸ __Number of dungeons__", value: `> ${dungeon_list.length}`, inline: true}, + {name: "> __Min level__", value: "> đŸ’Ē", inline: true} + ); + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages, header_description); +} + +exports.get_dungeon = get_dungeon; +exports.dungeon_command = dungeon_command; diff --git a/src/commands/echo.js b/src/commands/echo.js new file mode 100644 index 0000000..df37b87 --- /dev/null +++ b/src/commands/echo.js @@ -0,0 +1,50 @@ +const {SlashCommandBuilder, PermissionFlagsBits} = require("discord.js"); +const {send_echo, get_first_chat_channel} = require("../utils.js"); + +const echo_command = new SlashCommandBuilder(); +echo_command.setName("echo"); +echo_command.setDescription("Ask me to send a message in the current guild or in all the guils where I am present."); +echo_command.setDefaultMemberPermissions(PermissionFlagsBits.Administrator); +echo_command.addStringOption((option) => { + option.setName("message"); + option.setDescription("The message to send. Any abuse will be punished."); + option.setRequired(true); + option.setMinLength(1); + option.setMaxLength(1900); + + return option; +}); +echo_command.addBooleanOption((option) => { + option.setName("all_guilds"); + option.setDescription("The message will be sent to all guilds I have joined."); + option.setRequired(false); + + return option; +}); + +async function get_echo(interaction, client){ + await interaction.deferReply({ephemeral: true}); + + const message = interaction.options.get("message")?.value; + const all_guilds = interaction.options.get("all_guilds")?.value; + + try{ + if (all_guilds){ + await send_echo(client, message); + }else{ + try{ + await interaction.channel.send(message); + }catch{ + const channel = await get_first_chat_channel(interaction.guild, client); + await channel.send(message); + } + } + + await interaction.editReply("The message has been sent."); + }catch(error){ + await interaction.editReply("The message could not be sent."); + } +} + +exports.get_echo = get_echo; +exports.echo_command = echo_command; diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 0000000..811d420 --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,79 @@ +const {SlashCommandBuilder, EmbedBuilder, ApplicationCommandType, ApplicationCommandOptionType} = require("discord.js"); +const {send_embed_layout} = require("../utils.js"); + +const help_command = new SlashCommandBuilder(); +help_command.setName("help"); +help_command.setDescription("Get help on how to use me."); + +async function get_list_commands(application, command_type = ApplicationCommandType.ChatInput){ + const result = new Map(); + const commands = await application.commands.fetch(); + + for (const command of commands.values()){ + let base = true; + + if (command.type != command_type){ + continue; + } + + for (const option of command.options){ + if (option.type == ApplicationCommandOptionType.Subcommand){ + base = false; + if (option.type == command_type){ + result.set(`${command.name} ${option.name}`, {id: command.id, description: option.description, base: command}); + } + }else if (option.type == ApplicationCommandOptionType.SubcommandGroup){ + base = false; + for (const sub_option of option.options){ + if (sub_option.type == command_type){ + result.set(`${command.name} ${option.name} ${sub_option.name}`, {id: command.id, description: sub_option.description, base: command}); + } + } + } + } + + if (base){ + result.set(command.name, {id: command.id, description: command.description, base: command}); + } + } + + return result; +} + +async function get_help(interaction, client){ + await interaction.deferReply(); + + try{ + const embed = new EmbedBuilder(); + embed.setTitle("â„šī¸ List of commands â„šī¸"); + + const commands = await get_list_commands(client.application); + + const header_description = `Hi ${interaction.user}, Here are some commands that might be useful to you:`; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(commands.size / max_items_by_pages)); + + let current_page = -1; + let i = 0; + for (const [name, data] of commands.entries()){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + pages[current_page] += `* : **${data.description}**\n`; + + i++; + } + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages, header_description); + }catch (error){ + await interaction.editReply(`Unable to list commands.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +exports.get_help = get_help; +exports.help_command = help_command; diff --git a/src/commands/info.js b/src/commands/info.js new file mode 100644 index 0000000..1fd29d9 --- /dev/null +++ b/src/commands/info.js @@ -0,0 +1,47 @@ +const {SlashCommandBuilder, EmbedBuilder} = require("discord.js"); +const {get_info_application, format_score} = require("./../utils.js"); + +const info_command = new SlashCommandBuilder(); +info_command.setName("info"); +info_command.setDescription("Get metadata about the GoBattle.io Bot."); + +async function get_info(interaction, client){ + await interaction.deferReply(); + + try{ + const embed = new EmbedBuilder(); + embed.setTitle(client.application.name); + embed.setURL(client.application.customInstallURL || `https://discord.com/api/oauth2/authorize?client_id=${client.application.id}&permissions=18685255743552&scope=bot+applications.commands`); + embed.setDescription(client.application.description); + embed.setThumbnail(client.user.avatarURL()); + + const info = await get_info_application(client.application); + + const approximate_guild_count = info?.approximate_guild_count || "_Unknown?_"; + const approximate_user_install_count = info?.approximate_user_install_count || "_Unknown?_"; + const tos = info?.terms_of_service_url || "https://gobattle.io/tos.html"; + const pp = info?.privacy_policy_url || "https://www.iubenda.com/privacy-policy/8108614"; + const project_initiator_user_id = "461495109855215616"; // Please be courteous and do not change this value. + + embed.addFields( + {name: "> Official Gobattle.io Guild", value: "> [Link](https://discord.gg/gobattle-io-official-380588354934276097)", inline: true}, + {name: "> Application Owner", value: `> ${client.application.owner}`, inline: true}, + {name: "> Install Count", value: `> Servers: **${typeof approximate_guild_count == "number" ? format_score(approximate_guild_count) : approximate_guild_count}**\n> Individual Users: **${typeof approximate_user_install_count == "number" ? format_score(approximate_user_install_count) : approximate_user_install_count}**`, inline: true} + ); + + embed.addFields( + {name: "> Terms of Service", value: `> [Link](${tos})`, inline: true}, + {name: "> Privacy Policy", value: `> [Link](${pp})`, inline: true}, + {name: "> Project initiator", value: `> <@${project_initiator_user_id}>`, inline: true} + ); + + await interaction.editReply({ + embeds: [embed] + }); + }catch(error){ + console.error(error); + } +} + +exports.get_info = get_info; +exports.info_command = info_command; diff --git a/src/commands/install_link.js b/src/commands/install_link.js new file mode 100644 index 0000000..9a03522 --- /dev/null +++ b/src/commands/install_link.js @@ -0,0 +1,13 @@ +const {SlashCommandBuilder} = require("discord.js"); + +const install_link_command = new SlashCommandBuilder(); +install_link_command.setName("install_link"); +install_link_command.setDescription("Get the installation link of this application."); + +async function get_install_link(interaction, client){ + const install_url = client.application.customInstallURL || `https://discord.com/api/oauth2/authorize?client_id=${client.application.id}&permissions=18685255743552&scope=bot+applications.commands`; + await interaction.reply(`# Install me to get utility features related to the MMORPG GoBattle.io â¤ī¸: [Click here to install](${install_url}).`); +} + +exports.get_install_link = get_install_link; +exports.install_link_command = install_link_command; diff --git a/src/commands/item.js b/src/commands/item.js new file mode 100644 index 0000000..ed9a4fb --- /dev/null +++ b/src/commands/item.js @@ -0,0 +1,180 @@ +const {SlashCommandBuilder, EmbedBuilder} = require("discord.js"); +const {ultra_list} = require("../ultralist.json"); +const {restrict_text, sum, format_score, format_score_with_commas, send_embed_layout, application_emoji_cache} = require("../utils.js"); + +const items_map = new Map(); +for (const item_data of ultra_list){ + if (typeof item_data.id == "number"){ + items_map.set(item_data.id, item_data); + } +} + +const item_command = new SlashCommandBuilder(); +item_command.setName("item"); +item_command.setDescription("Command relating to items and other consumables in the game."); +item_command.addSubcommand((subcommand) => { + subcommand.setName("info"); + subcommand.setDescription("Obtain the characteristics of an item."); + subcommand.addIntegerOption((option) => { + option.setName("item_id"); + option.setDescription("The identifier of the item."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +item_command.addSubcommand((subcommand) => { + subcommand.setName("list"); + subcommand.setDescription("Get the list of all items in the game with their respective ID."); + + return subcommand; +}); +item_command.addSubcommand((subcommand) => { + subcommand.setName("ultrarare_drops"); + subcommand.setDescription("Get statistics on the overall frequency of Ultra Rares spawning in chests."); + + return subcommand; +}); + +async function get_item(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand){ + case "info": + await get_info(interaction, client); + break; + case "ultrarare_drops": + await get_ultrarare_drops(interaction, client); + break; + case "list": + await get_list(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_info(interaction, client){ + await interaction.deferReply(); + + try{ + const item_id = interaction.options.get("item_id")?.value; + + const item_info = items_map.get(item_id); + + if (!item_info){ + await interaction.editReply("Sorry, the specified item does not exist or has not yet been indexed by the Gobattle API."); + return; + } + + const unknown_item_emoji = application_emoji_cache.get("item_91") || "đŸ“Ļ"; + const item_emoji = application_emoji_cache.get(item_info?.emoji) || unknown_item_emoji; + + const embed = new EmbedBuilder(); + embed.setTitle(`${item_emoji} ${restrict_text(item_info.name, 60)}#${item_id}`); + if (typeof item_emoji != "string"){ + embed.setThumbnail(item_emoji.imageURL()); + } + embed.setDescription(item_info.description ? restrict_text(item_info.description, 250) : "_Description Unknown_"); + embed.setColor(0x500000); + embed.addFields( + {name: "> đŸˇī¸ __ID__", value: `> ${item_id}`, inline: true}, + {name: "> 🌟 __Rarity__", value: `> ***${"Ultrarare"}***`, inline: true}, + {name: "> đŸ› ī¸ __Max uses__", value: `> ${item_info.uses || "_Unknown_"}`, inline: true} + ); + + embed.addFields( + {name: "> đŸ›ī¸ __Max in inventory__", value: `> ${item_info.max_in_inventory || "_Unknown_"}`, inline: true}, + {name: "> đŸ“Ĩ __Drops in__", value: `> ${item_info.drops ? restrict_text(item_info.drops, 50) : "_Unknown_"}`, inline: true}, + {name: "> 🤝 __Tradable__", value: `> ${"_Unknown_"}`, inline: true} + ); + + embed.addFields( + {name: "> đŸē __Is relic__", value: `> ${item_info.is_relic ? (item_info.is_relic ? "Yes": "No") : "_Unknown_"}`, inline: true}, + {name: "> 💰 __Price__", value: `> ${"_Unknown_"}`, inline: true} + ); + + embed.setTimestamp(); + embed.setFooter({text: "Source: Sage of the Ivy"}); + + await interaction.editReply({ + embeds: [embed], + content: "This feature is under development, information may not be accurate." + }); + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this item.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_ultrarare_drops(interaction, client){ + await interaction.deferReply(); + + try{ + const response = await fetch("https://gobattle.io/api.php/v1/stats/ultrarare/drops"); + + if (!response.ok){ + await interaction.editReply("Unable to obtain general information on the general frequency of obtaining ultrarares in Gobattle. There is a problem with the Gobattle API."); + return; + } + + const data_json = await response.json(); + + const total = data_json.total; + const drops = data_json.drops; + const values = Object.values(drops); + + const total_ultrarare = sum(values); + + const drops_ratio = total_ultrarare / (total || 1); + + const embed = new EmbedBuilder(); + + embed.setTitle("Current average chance of getting an Ultrarare from a chest."); + + const chest_emoji = application_emoji_cache.get("item_760") || "🧰"; + const total_ultrarare_formatted = format_score_with_commas(total_ultrarare); + const header_description = `Results are calculated based on **${format_score_with_commas(total)}** samples (*chest opened by players ${chest_emoji}*) including **${total_ultrarare_formatted}** ultrarare drops.`; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(values.length / max_items_by_pages)); + const unknown_item_emoji = application_emoji_cache.get("item_91") || "đŸ“Ļ"; + + const drops_entries = Object.entries(drops); + + let current_page = -1; + for (let i = 0; i < values.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const [key, value] = drops_entries[i]; + const ultra_info = items_map.get(parseInt(key, 10)); + const item_emoji = application_emoji_cache.get(ultra_info?.emoji) || unknown_item_emoji; + pages[current_page] += `* ${item_emoji} **${restrict_text(ultra_info?.name || "*Unknow?*", 45)}**#${key}: \`${format_score(value)} (${(value / total_ultrarare * 100).toPrecision(2)}%)\`\n`; + } + + embed.addFields( + {name: "> 🌟 __Ultrarare__", value: `> ${total_ultrarare_formatted} (${(drops_ratio * 100).toPrecision(2)}%)`, inline: true}, + {name: `> ${unknown_item_emoji} __Other__`, value: `> ${format_score_with_commas(total - total_ultrarare)} (${((1 - drops_ratio) * 100).toPrecision(2)}%)`, inline: true} + ); + + embed.setTimestamp(); + + const message_content = "Special mention to _Sage of the Ivy_ for providing some metadata on most of the game's ultrarares while waiting for the GoBattle API update!\nThanks to him!"; + await send_embed_layout(interaction, embed, pages, header_description, message_content); + }catch(error){ + await interaction.editReply(`Unable to generate template.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_list(interaction, client){ + await interaction.reply("Sorry this command is under development and cannot be used at the moment."); +} + +exports.get_item = get_item; +exports.item_command = item_command; diff --git a/src/commands/leave.js b/src/commands/leave.js new file mode 100644 index 0000000..ad1e7e8 --- /dev/null +++ b/src/commands/leave.js @@ -0,0 +1,15 @@ +const {SlashCommandBuilder, PermissionFlagsBits} = require("discord.js"); + +const leave_command = new SlashCommandBuilder(); +leave_command.setName("leave"); +leave_command.setDescription("Kick me out of the current guild."); +leave_command.setDefaultMemberPermissions(PermissionFlagsBits.Administrator); + +async function get_leave(interaction, client){ + const install_url = client.application.customInstallURL || `https://discord.com/api/oauth2/authorize?client_id=${client.application.id}&permissions=18685255743552&scope=bot+applications.commands`; + await interaction.reply(`Are you asking me to leave? đŸĨē\nToo bad, maybe I wasn't up to my duties. If you have feedback to give to my developers, especially ${client.application.owner}, I may improve to better meet your needs.\nIf you change your mind, you can reinstall me with [this link](${install_url}).\nMaybe see you next time and thanks for trying me!`); + await interaction.guild.leave(); +} + +exports.get_leave = get_leave; +exports.leave_command = leave_command; diff --git a/src/commands/ping.js b/src/commands/ping.js new file mode 100644 index 0000000..a92096b --- /dev/null +++ b/src/commands/ping.js @@ -0,0 +1,16 @@ +const {SlashCommandBuilder} = require("discord.js"); + +const ping_command = new SlashCommandBuilder(); +ping_command.setName("ping"); +ping_command.setDescription("Check if the bot is operational."); + +async function get_ping(interaction, client){ + try{ + await interaction.reply({content: "Pong!", ephemeral: true}); + }catch(error){ + console.error(error); + } +} + +exports.get_ping = get_ping; +exports.ping_command = ping_command; diff --git a/src/commands/ranking.js b/src/commands/ranking.js new file mode 100644 index 0000000..2a4b41d --- /dev/null +++ b/src/commands/ranking.js @@ -0,0 +1,354 @@ +const {EmbedBuilder, SlashCommandBuilder} = require("discord.js"); +const {restrict_text, get_level, get_level_adventurer, format_score, format_speed_run_time, send_embed_layout, application_emoji_cache} = require("../utils.js"); +const head_list = require("../head_list.json"); + +const heads_map = new Map(); +for (const head_data of head_list){ + if (typeof head_data.id == "number"){ + heads_map.set(head_data.id, head_data); + } +} + +const ranking_command = new SlashCommandBuilder(); +ranking_command.setName("ranking"); +ranking_command.setDescription("Commands relating to rankings."); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("king"); + subcommand.setDescription("Get King ranking"); + + return subcommand; +}); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("weekly"); + subcommand.setDescription("Get Weekly ranking"); + + return subcommand; +}); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("monthly"); + subcommand.setDescription("Get Monthly ranking"); + + return subcommand; +}); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("overall"); + subcommand.setDescription("Get Overall (EXP) ranking"); + + return subcommand; +}); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("adventurer"); + subcommand.setDescription("Get Adventurer ranking"); + + return subcommand; +}); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("relichunter"); + subcommand.setDescription("Get Relic Hunter ranking"); + + return subcommand; +}); +ranking_command.addSubcommand((subcommand) => { + subcommand.setName("speedrun"); + subcommand.setDescription("Get Speedrun ranking"); + subcommand.addIntegerOption((option) => { + option.setName("dungeon_id"); + option.setDescription("The dungeon identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); + +async function get_ranking(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand){ + case "king": + await get_king(interaction, client); + break; + case "weekly": + case "monthly": + case "overall": + await get_general(interaction, client, subcommand); + break; + case "adventurer": + await get_adventurer(interaction, client); + break; + case "relichunter": + await get_relic_hunter(interaction, client); + break; + case "speedrun": + await get_speedrun(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_king(interaction, client){ + await interaction.deferReply(); + + try{ + const response = await fetch("https://gobattle.io/api.php/valdoranking?platform=Discord&ud="); + + if (!response.ok){ + await interaction.editReply(`Impossible to recover the ranking. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data = await response.json(); + + const embed = new EmbedBuilder(); + const crown_emoji = application_emoji_cache.get("valdorancrown") || "👑"; + embed.setTitle(`${crown_emoji} King Ranking ${crown_emoji}`); + + const ranking = data?.ranking; + + const header_description = "You can earn reputation points by killing enemies, bosses, completing dungeons, and killing other players in the arena.\nThe new king of Valdoran will be chosen every Sunday at midnight (UTC Time) and will earn 150 diamonds, ascend the throne, wear the crown and can use the `/coin` command."; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(ranking.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + + let current_page = -1; + for (let i = 0; i < ranking.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = ranking[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + pages[current_page] += `${i + 1}. ${head_emoji} **${restrict_text(field?.nick, 20)}**#${field?.id}: \`${format_score(field?.reputation)} REP\`\n`; + } + + embed.addFields( + {name: "> 🗓 __Week__", value: `> ${data?.week || "_Unknown?_"}`, inline: true}, + {name: "> 🗓 __Year__", value: `> ${data?.year || "_Unknown?_"}`, inline: true} + ); + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages, header_description); + }catch(error){ + await interaction.editReply(`Failed to generate King ranking...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_general(interaction, client, type){ + await interaction.deferReply(); + + try{ + const response = await fetch(`https://gobattle.io/api.php/stats/${type}?platform=Discord&ud=`); + + if (!response.ok){ + await interaction.editReply(`Impossible to recover the ranking. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data = await response.json(); + + const embed = new EmbedBuilder(); + + switch (type){ + case "weekly": + const weekly_emoji = application_emoji_cache.get("item_53") || "🏆"; + embed.setTitle(`${weekly_emoji} Weekly Ranking ${weekly_emoji}`); + break; + case "monthly": + const monthly_emoji = application_emoji_cache.get("item_10") || "🏆"; + embed.setTitle(`${monthly_emoji} Monthly Ranking ${monthly_emoji}`); + break; + case "overall": + const overall_emoji = application_emoji_cache.get("item_51") || "🏆"; + embed.setTitle(`${overall_emoji} Overall Ranking (EXP) ${overall_emoji}`); + break; + default: + const general_emoji = application_emoji_cache.get("valdorancrown") || "🏆"; + embed.setTitle(`${general_emoji} General Ranking ${general_emoji}`); + } + + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(data.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + const kills_emoji = application_emoji_cache.get("compass_item96") || "đŸ—Ąī¸"; + const deaths_emoji = application_emoji_cache.get("compass_item97") || "💀"; + const coins_emoji = application_emoji_cache.get("coin") || "đŸĒ™"; + const experience_emoji = application_emoji_cache.get("fuego") || "đŸ”Ĩ"; + const level_emoji = application_emoji_cache.get("compass_item121") || "🔰"; + + let current_page = -1; + for (let i = 0; i < data.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = data[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + const exp = parseInt(field?.experience, 10); + const level = get_level(exp); + pages[current_page] += `${i + 1}. ${head_emoji} **${restrict_text(field?.nick, 20)}**: ${coins_emoji}\`${format_score(field?.coins)}\` ${kills_emoji}\`${format_score(field?.kills)}\` ${deaths_emoji}\`${format_score(field?.deaths)}\` ${experience_emoji}\`${format_score(field?.experience)}\` ${level_emoji}\`${level}\`\n`; + } + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages); + }catch(error){ + await interaction.editReply(`Failed to generate Weekly ranking...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_adventurer(interaction, client){ + await interaction.deferReply(); + + try{ + const response = await fetch("https://gobattle.io/api.php/v1/stats/ranking/adventurer?platform=Discord&ud="); + + if (!response.ok){ + await interaction.editReply(`Impossible to recover the ranking. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data = await response.json(); + + const embed = new EmbedBuilder(); + + embed.setTitle("🤠 Adventurer Ranking 🤠"); + + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(data.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + + let current_page = -1; + for (let i = 0; i < data.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = data[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + const level = get_level_adventurer(field?.score); + pages[current_page] += `${i + 1}. ${head_emoji} **${restrict_text(field?.nick, 20)}**#${field?.id}: \`${format_score(field?.score)} EXP (${level} LVL)\`\n`; + } + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages); + }catch(error){ + await interaction.editReply(`Failed to generate Adventurer ranking...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_relic_hunter(interaction, client){ + await interaction.deferReply(); + + try{ + const response = await fetch("https://gobattle.io/api.php/v1/stats/ranking/relichunter?platform=Discord&ud="); + + if (!response.ok){ + await interaction.editReply(`Impossible to recover the ranking. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data = await response.json(); + + const embed = new EmbedBuilder(); + + const relic_hunter_emoji = application_emoji_cache.get("item_809") || "🎒"; + embed.setTitle(`${relic_hunter_emoji} Relic Hunter Ranking ${relic_hunter_emoji}`); + + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(data.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + + let current_page = -1; + for (let i = 0; i < data.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = data[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + const level = get_level_adventurer(field?.score); + pages[current_page] += `${i + 1}. ${head_emoji} **${restrict_text(field?.nick, 20)}**#${field?.id}: \`${format_score(field?.score)} EXP (${level} LVL)\`\n`; + } + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages); + }catch(error){ + await interaction.editReply(`Failed to generate Hunter Ranking ranking...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_speedrun(interaction, client){ + await interaction.deferReply(); + + const dungeon_id = interaction.options.get("dungeon_id")?.value; + + try{ + const response = await fetch(`https://gobattle.io/api.php/v1/stats/speedrun/${dungeon_id}`); + + if (!response.ok){ + await interaction.editReply(`Map _#${dungeon_id}_ not found, try another dungeon ID.\nNormally the dungeon ID is listed in chat when you complete your speedrun.\nIf you don't see anything, you haven't used the \`/speedrun\` command correctly on GoBattle.io.`); + return; + } + + const data = await response.json(); + + const embed = new EmbedBuilder(); + + embed.setTitle(`🏁 Speedrun Ranking (${data.name || "_Unknown?_"}) 🏁`); + + const ranking = data.ranking; + + const header_description = "Do you also want to be included in the leaderboard?\nUse the \`/speedrun\` command on GoBattle.io to start a speedrun session and try to do better than the others!"; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(ranking.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + + const list_size = ranking.length; + let current_page = -1; + for (let i = 0; i < list_size; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = ranking[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + const time = format_speed_run_time(field?.time); + pages[current_page] += `${field?.rank}. ${head_emoji} **${restrict_text(field?.nick, 20)}**#${field?.id}: \`${time}\`\n`; + } + + embed.addFields( + {name: "> 🏰 __Dungeon name__", value: `> **${data.name || "_Unknown?_"}**`, inline: true}, + {name: "> đŸˇī¸ __Dungeon ID__", value: `> ${dungeon_id}`, inline: true} + ); + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages, header_description); + }catch(error){ + await interaction.editReply(`Failed to generate Speedrun ranking...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +exports.get_ranking = get_ranking; +exports.ranking_command = ranking_command; diff --git a/src/commands/server.js b/src/commands/server.js new file mode 100644 index 0000000..5379693 --- /dev/null +++ b/src/commands/server.js @@ -0,0 +1,118 @@ +const {SlashCommandBuilder, EmbedBuilder} = require("discord.js"); +const {restrict_text, send_embed_layout} = require("../utils.js"); + +const server_command = new SlashCommandBuilder(); +server_command.setName("server"); +server_command.setDescription("Commands relating to GoBattle.io game servers."); +server_command.addSubcommand((subcommand) => { + subcommand.setName("list"); + subcommand.setDescription("Obtain the list of GoBattle.io servers currently listed for the public."); + subcommand.addStringOption((option) => { + option.setName("platform"); + option.setDescription("Platform."); + option.setRequired(false); + + option.setChoices( + {name: "Web", value: "Web"}, + {name: "iOS", value: "iOS"}, + {name: "Android", value: "Android"} + ); + + return option; + }); + subcommand.addIntegerOption((option) => { + option.setName("version"); + option.setDescription("Server version."); + option.setRequired(false); + option.setMinValue(1); + + return option; + }); + + return subcommand; +}); + +async function get_server(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + switch (subcommand){ + case "list": + await get_list(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_list(interaction, client){ + await interaction.deferReply(); + + const platform = interaction.options.get("platform")?.value || "Web"; + const version = interaction.options.get("version")?.value || 115; + + try{ + const response = await fetch(`https://gobattle.io/api.php/bootstrap/${version}?platform=${platform}&ud=`); + + if (!response.ok){ + await interaction.editReply(`I couldn't get the server list. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const data_json = await response.json(); + + const server_list = data_json?.serverlist; + + server_list.push({"id": "8", "version": "115", "friendlyName": "Alpha Server (v115)", "url": "us-west.gobattle.io", "port": "8100", "ws": "9100", "wss": "10100", "admin": "Unknown?"}); + //server_list.push({"id": "50", "version": "115", "friendlyName": "Kuwazy Server", "url": "us-west.gobattle.io", "port": "8102", "ws": "9102", "wss": "10102", "admin": "0"}); + //server_list.push({"id": "147", "version": "114", "friendlyName": "Old France Server (v114)", "url": "france.gobattle.io", "port": "8114", "ws": "9114", "wss": "10114", "admin": "0"}); + //server_list.push({"id": "148", "version": "114", "friendlyName": "Old USA West server (v114)", "url": "us-west.gobattle.io", "port": "8114", "ws": "9114", "wss": "10114", "admin": "0"}); + //server_list.push({"id": "149", "version": "114", "friendlyName": "Old Singapore Server (v114)", "url": "france.gobattle.io", "port": "8114", "ws": "9114", "wss": "10114", "admin": "0"}); + + const embed = new EmbedBuilder(); + + embed.setTitle("đŸ–Ĩī¸ Server List đŸ–Ĩī¸"); + + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(server_list.length / max_items_by_pages)); + + let current_page = -1; + for (var i = 0; i < server_list.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = server_list[i]; + + let is_online; + try{ + const response = await fetch(`https://${field.url}:${field.wss || field.ws || field.port}/rtt`); + + is_online = response.ok; + + //const data_ping = await response.text(); + }catch(error){ + is_online = false; + } + + pages[current_page] += `* **${restrict_text(field?.friendlyName, 25)}**#${field?.id}: \`${field?.version} 🔁\` \`${field?.admin} đŸ› ī¸\` \`${(is_online ? "Online" : "Down")} 🌐\`\n`; + } + + embed.addFields( + {name: "> 🎮 __Platform__", value: `> ${platform}`, inline: true}, + {name: "> 🔃 __Requested version__", value: `> ${version.toString()}`, inline: true}, + {name: "> __Version__", value: "> 🔁", inline: true}, + {name: "> __Administrator level__", value: "> đŸ› ī¸", inline: true}, + {name: "> __Server status__", value: "> 🌐", inline: true} + ); + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages); + }catch(error){ + await interaction.editReply(`Unable to retrieve server list.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +exports.get_server = get_server; +exports.server_command = server_command; diff --git a/src/commands/setting.js b/src/commands/setting.js new file mode 100644 index 0000000..86241de --- /dev/null +++ b/src/commands/setting.js @@ -0,0 +1,319 @@ +const {SlashCommandBuilder, PermissionFlagsBits, ActivityType} = require("discord.js"); + +const setting_command = new SlashCommandBuilder(); +setting_command.setName("setting"); +setting_command.setDescription("Command relating to my profile settings."); +setting_command.setDefaultMemberPermissions(PermissionFlagsBits.Administrator); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_avatar"); + subcommand.setDescription("Set my avatar image."); + subcommand.addAttachmentOption((option) => { + option.setName("image_file"); + option.setDescription("Avatar image (Recommended file: png, jpeg, gif...)"); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_banner"); + subcommand.setDescription("Set my banner image."); + subcommand.addAttachmentOption((option) => { + option.setName("image_file"); + option.setDescription("Banner image (Recommended file: png, jpeg, gif...)"); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_username"); + subcommand.setDescription("Changing my username in Discord is heavily rate limited, with only 2 requests every hour!"); + subcommand.addStringOption((option) => { + option.setName("username"); + option.setDescription("Username."); + option.setRequired(true); + option.setMinLength(1); + option.setMaxLength(32); + + return option; + }); + + return subcommand; +}); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_biography"); + subcommand.setDescription("Set my biography."); + subcommand.addStringOption((option) => { + option.setName("biography"); + option.setDescription("Biography."); + option.setRequired(true); + option.setMinLength(0); + option.setMaxLength(190); + + return option; + }); + + return subcommand; +}); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_activity"); + subcommand.setDescription("Set my activity."); + subcommand.addStringOption((option) => { + option.setName("name"); + option.setDescription("Name of the activity."); + option.setRequired(true); + option.setMinLength(0); + option.setMaxLength(190); + + return option; + }); + subcommand.addStringOption((option) => { + option.setName("state"); + option.setDescription("State of the activity."); + option.setRequired(false); + option.setMinLength(0); + option.setMaxLength(190); + + return option; + }); + subcommand.addNumberOption((option) => { + option.setName("type"); + option.setDescription("Type of the activity."); + option.setRequired(false); + option.addChoices( + {name: "Competing", value: ActivityType.Competing}, + {name: "Listening", value: ActivityType.Listening}, + {name: "Playing", value: ActivityType.Playing}, + {name: "Streaming", value: ActivityType.Streaming}, + {name: "Watching", value: ActivityType.Watching}, + {name: "Custom", value: ActivityType.Custom} + ); + + return option; + }); + subcommand.addStringOption((option) => { + option.setName("url"); + option.setDescription("Twitch / YouTube stream URL."); + option.setRequired(false); + option.setMinLength(0); + option.setMaxLength(190); + + return option; + }); + + return subcommand; +}); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_afk"); + subcommand.setDescription("Sets/removes my AFK flag."); + subcommand.addBooleanOption((option) => { + option.setName("is_afk"); + option.setDescription("Is AFK?"); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +setting_command.addSubcommand((subcommand) => { + subcommand.setName("set_status"); + subcommand.setDescription("Set my status."); + subcommand.addStringOption((option) => { + option.setName("status"); + option.setDescription("Status."); + option.setRequired(true); + option.addChoices( + {name: "Online", value: "online"}, + {name: "Invisible", value: "invisible"}, + {name: "Idle", value: "idle"}, + {name: "Dnd", value: "dnd"} + ); + + return option; + }); + + return subcommand; +}); + +async function get_setting(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand){ + case "set_avatar": + await get_set_avatar(interaction, client); + break; + case "set_banner": + await get_set_banner(interaction, client); + break; + case "set_username": + await get_set_username(interaction, client); + break; + case "set_biography": + await get_set_biography(interaction, client); + break; + case "set_activity": + await get_set_activity(interaction, client); + break; + case "set_afk": + await get_set_afk(interaction, client); + break; + case "set_status": + await get_set_status(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_set_avatar(interaction, client){ + await interaction.deferReply(); + + try{ + const attachment = interaction.options.get("image_file")?.attachment; + + const response = await fetch(attachment.url); + + if (!response.ok){ + await interaction.editReply(`The avatar image was not set successfully.\nUnable to retrieve image from Discord CDN.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + return; + } + + const array_buffer = await response.arrayBuffer(); + const buffer = Buffer.from(array_buffer); + + await client.user.setAvatar(buffer); + await interaction.editReply("The avatar image has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`The avatar image was not set successfully.\nVerify that the file is an image type accepted by Discord.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_set_banner(interaction, client){ + await interaction.deferReply(); + + try{ + const attachment = interaction.options.get("image_file")?.attachment; + + const response = await fetch(attachment.url); + + if (!response.ok){ + await interaction.editReply(`The banner image was not set successfully.\nUnable to retrieve image from Discord CDN.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + return; + } + + const array_buffer = await response.arrayBuffer(); + const buffer = Buffer.from(array_buffer); + + await client.user.setBanner(buffer); // Do not work? + await interaction.editReply("The banner image has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`The banner image was not set successfully.\nVerify that the file is an image type accepted by Discord.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_set_username(interaction, client){ + await interaction.deferReply(); + + try{ + const username = interaction.options.get("username")?.value; + + await client.user.setUsername(username); + await interaction.editReply("Username has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`Username was not set successfully.\nSets the username of the logged in client. Changing usernames in Discord is heavily rate limited, with only 2 requests every hour. Use this sparingly!\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_set_biography(interaction, client){ + await interaction.deferReply(); + + // Do not work? NAHHHHHHHHHHHHHHH Why Discord Why!!! ): + /* + // The code if against is prohibited by discord: + try{ + const biography = interaction.options.get("biography")?.value; + + const api_version = 14; + const url = `https://discord.com/api/${api_version}/users/@me`; + + const response = await fetch(url, { + method: "PATCH", + headers: { + "Authorization": process.env.TOKEN, + "Content-Type": "application/json" + }, + body: JSON.stringify({bio: biography}) + }); + + if (!response.ok){ + await interaction.editReply(`Biography was not set successfully.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(response); + return; + } + + await interaction.editReply("Biography has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`Biography was not set successfully.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } + */ + + await interaction.editReply("The Discord API doesn't let you do this properly. Wait for Discord to update its API."); +} + +async function get_set_activity(interaction, client){ + await interaction.deferReply(); + + try{ + const name_value = interaction.options.get("name")?.value; + const state_value = interaction.options.get("state")?.value; + const type_value = interaction.options.get("type")?.value; + const url_value = interaction.options.get("url")?.value; + + await client.user.setActivity({name: name_value, state: state_value, type: type_value, url: url_value}); + await interaction.editReply("Activity has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`Activity was not set successfully.\nCheck that the settings options are consistent with each other.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_set_afk(interaction, client){ + await interaction.deferReply(); + + try{ + const afk_value = interaction.options.get("is_afk")?.value; + + client.user.setAFK(afk_value); + await interaction.editReply("AFK status has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`AFK status was not set successfully.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_set_status(interaction, client){ + await interaction.deferReply(); + + try{ + const status_value = interaction.options.get("status")?.value; + + client.user.setStatus(status_value); + await interaction.editReply("Status has been successfully set. Changes may take time to appear."); + }catch (error){ + await interaction.editReply(`Status was not set successfully.\nIf the problem persists, contact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +exports.get_setting = get_setting; +exports.setting_command = setting_command; diff --git a/src/commands/ultrarare_drop_chance.js b/src/commands/ultrarare_drop_chance.js new file mode 100644 index 0000000..30ef3cd --- /dev/null +++ b/src/commands/ultrarare_drop_chance.js @@ -0,0 +1,27 @@ +const {SlashCommandBuilder} = require("discord.js"); + +const ultrarare_drop_chance_command = new SlashCommandBuilder(); +ultrarare_drop_chance_command.setName("get_ultrarare_drop_chance"); +ultrarare_drop_chance_command.setDescription("Calculate the probability of an ultra falling into a chest based on a level of luck."); +ultrarare_drop_chance_command.addNumberOption((option) => { + option.setName("luck"); + option.setDescription("Level of luck."); + option.setMinValue(0); + option.setMaxValue(20); + option.setRequired(true); + + return option; +}); + +async function get_ultrarare_drop_chance(interaction, client){ + const BASE = 0.6; + const STEP_LEVEL = 0.05; + + const luck = interaction.options.get("luck")?.value || 0; + const result = BASE + STEP_LEVEL * luck; + + await interaction.reply(`You have an \`${result.toPrecision(2)}%\` chance of getting an Ultrarare from a chest with a LUCK level of ${luck}.`); +} + +exports.get_ultrarare_drop_chance = get_ultrarare_drop_chance; +exports.ultrarare_drop_chance_command = ultrarare_drop_chance_command; diff --git a/src/commands/user.js b/src/commands/user.js new file mode 100644 index 0000000..0d0b6b6 --- /dev/null +++ b/src/commands/user.js @@ -0,0 +1,1283 @@ +const {EmbedBuilder, SlashCommandBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ButtonBuilder, ButtonStyle, ComponentType} = require("discord.js"); +const {is_my_developer, restrict_text, format_score, format_score_with_commas, get_level, get_level_to_emojis, send_embed_layout, application_emoji_cache} = require("../utils.js"); +const database = require("../database/database.js"); +const head_list = require("../head_list.json"); + +const heads_map = new Map(); +for (const head_data of head_list){ + if (typeof head_data.id == "number"){ + heads_map.set(head_data.id, head_data); + } +} + +const user_command = new SlashCommandBuilder(); +user_command.setName("user"); +user_command.setDescription("Command relating to users in the game."); +user_command.addSubcommand((subcommand) => { + subcommand.setName("create"); + subcommand.setDescription("Create a GoBattle.io account."); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("login"); + subcommand.setDescription("Log in to your GoBattle.io account."); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("logout"); + subcommand.setDescription("Log out of your GoBattle.io account."); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("recover"); + subcommand.setDescription("Recover a GoBattle.io account whose password you forgot."); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("info"); + subcommand.setDescription("Get general information about a user."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +/* +user_command.addSubcommand((subcommand) => { + subcommand.setName("king"); + subcommand.setDescription("Get general information about the current king."); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("bank"); + subcommand.setDescription("Get the list of items contained in a game user's bank."); + + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + subcommand.addIntegerOption((option) => { + option.setName("index_book"); + option.setDescription("Index of the bank book to consult."); + option.setMinValue(1); + option.setMaxValue(6); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("inventory"); + subcommand.setDescription("Get the list of items contained in a game user's inventory."); + + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +*/ +user_command.addSubcommand((subcommand) => { + subcommand.setName("gobattle_to_discord"); + subcommand.setDescription("Get Discord user from GoBattle user."); + + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("discord_to_gobattle"); + subcommand.setDescription("Get GoBattle user from Discord user."); + + subcommand.addUserOption((option) => { + option.setName("user"); + option.setDescription("Discord user."); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +user_command.addSubcommand((subcommand) => { + subcommand.setName("change_nickname"); + subcommand.setDescription("Change a GoBattle.io user's nickname."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + subcommand.addStringOption((option) => { + option.setName("nick"); + option.setDescription("New nickname."); + option.setMaxLength(1); + option.setMaxLength(20); + option.setRequired(true); + + return option; + }); + + return subcommand; +}); +user_command.addSubcommandGroup((friend_command) => { + friend_command.setName("friend"); + friend_command.setDescription("Command relating to friends in the game."); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("pending_count"); + subcommand.setDescription("Get the number of friend requests a user has."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("incoming_requests"); + subcommand.setDescription("Get the list of a user's incoming friend requests."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("outgoing_requests"); + subcommand.setDescription("Get the list of a user's outgoing friend requests."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("list"); + subcommand.setDescription("Get a user's friends list."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("add"); + subcommand.setDescription("Send a friend request to a user."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("delete"); + subcommand.setDescription("Remove a user from your friends list."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("cancel"); + subcommand.setDescription("Cancel the friend request sent to a user."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("accept"); + subcommand.setDescription("Accept a friend request from a user."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + friend_command.addSubcommand((subcommand) => { + subcommand.setName("ignore"); + subcommand.setDescription("Ignore a friend request from a user."); + subcommand.addIntegerOption((option) => { + option.setName("user_id"); + option.setDescription("The user identifier."); + option.setMinValue(1); + option.setRequired(true); + + return option; + }); + + return subcommand; + }); + + return friend_command; +}); + +async function get_user(interaction, client){ + const subcommand_group = interaction.options.getSubcommandGroup(); + const subcommand = interaction.options.getSubcommand(); + + if (!subcommand_group){ + switch (subcommand){ + case "create": + await get_create(interaction, client); + break; + case "login": + await get_login(interaction, client); + break; + case "logout": + await get_logout(interaction, client); + break; + case "recover": + await get_recover(interaction, client); + break; + case "info": + await get_info(interaction, client); + break; + case "king": + await get_king(interaction, client); + break; + case "bank": + await get_bank(interaction, client); + break; + case "inventory": + await get_inventory(interaction, client); + break; + case "gobattle_to_discord": + await get_gobattle_to_discord(interaction, client); + break; + case "discord_to_gobattle": + await get_discord_to_gobattle(interaction, client); + break; + case "change_nickname": + await get_change_nickname(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } + }else if (subcommand_group == "friend"){ + await get_friend(interaction, client); + }else{ + await interaction.reply(`Invalid subcommand group.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_friend(interaction, client){ + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand){ + case "pending_count": + await get_friend_pending_count(interaction, client); + break; + case "incoming_requests": + await get_friend_pending_requests(interaction, client, "incoming"); + break; + case "outgoing_requests": + await get_friend_pending_requests(interaction, client, "outgoing"); + break; + case "list": + await get_friend_list(interaction, client); + break; + case "add": + await get_friend_add(interaction, client); + break; + case "delete": + await get_friend_delete(interaction, client); + break; + case "cancel": + await get_friend_cancel(interaction, client); + break; + case "accept": + await get_friend_accept(interaction, client); + break; + case "ignore": + await get_friend_ignore(interaction, client); + break; + default: + await interaction.reply(`Invalid subcommand.\nContact ${client.application.owner} to resolve this issue.`); + } +} + +async function get_create(interaction, _client){ + const modal = new ModalBuilder(); + modal.setCustomId("create"); + modal.setTitle("Create a GoBattle.io account."); + + const email_input = new TextInputBuilder(); + email_input.setCustomId("email"); + email_input.setLabel("Email"); + email_input.setStyle(TextInputStyle.Short); + email_input.setRequired(true); + + const password_input = new TextInputBuilder(); + password_input.setCustomId("password"); + password_input.setLabel("Password"); + password_input.setPlaceholder("Don't give Discord password!"); + password_input.setStyle(TextInputStyle.Short); + email_input.setRequired(true); + + const email_action_row = new ActionRowBuilder().addComponents(email_input); + const password_action_row = new ActionRowBuilder().addComponents(password_input); + + modal.addComponents(email_action_row, password_action_row); + + await interaction.showModal(modal); +} + +async function get_login(interaction, _client){ + const gobattle_user_id = database.discord_user_to_gobattle_user_id(interaction.user); + if (gobattle_user_id){ + interaction.reply({content: `You are already using user account _#${gobattle_user_id}_. Use the \`/user logout\` command before registering another account.`, ephemeral: true}); + return; + } + + const modal = new ModalBuilder(); + modal.setCustomId("login"); + modal.setTitle("Login to GoBattle.io account."); + + const email_input = new TextInputBuilder(); + email_input.setCustomId("email"); + email_input.setLabel("Email"); + email_input.setPlaceholder("Email used on GoBattle.io") + email_input.setStyle(TextInputStyle.Short); + email_input.setRequired(true); + + const password_input = new TextInputBuilder(); + password_input.setCustomId("password"); + password_input.setLabel("Password"); + password_input.setPlaceholder("Don't give Discord password!"); + password_input.setStyle(TextInputStyle.Short); + password_input.setRequired(true); + + const email_action_row = new ActionRowBuilder().addComponents(email_input); + const password_action_row = new ActionRowBuilder().addComponents(password_input); + + modal.addComponents(email_action_row, password_action_row); + + await interaction.showModal(modal); +} + +async function get_logout(interaction, _client){ + const success = database.remove_gobattle_access_by_discord_user(interaction.user); + + if (success){ + await interaction.reply({content: "Your session has ended and has been successfully deleted. You can reconnect at any time with `/user login`.", ephemeral: true}); + }else{ + await interaction.reply({content: "You do not have a registered session.", ephemeral: true}); + } +} + +async function get_recover(interaction, _client){ + const modal = new ModalBuilder(); + modal.setCustomId("recover"); + modal.setTitle("Recover your GoBattle.io account."); + + const email_input = new TextInputBuilder(); + email_input.setCustomId("email"); + email_input.setLabel("Email (Used on GoBattle.io)"); + email_input.setStyle(TextInputStyle.Short); + email_input.setRequired(true); + + const email_action_row = new ActionRowBuilder().addComponents(email_input); + + modal.addComponents(email_action_row); + + await interaction.showModal(modal); +} + +async function get_info(interaction, client){ + await interaction.deferReply(); + + try{ + const user_id = interaction.options.get("user_id")?.value; + const public = true; + + const gobattle_token = database.get_gobattle_token_by_gobattle_id(user_id); + if (!gobattle_token){ + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + if (!public){ + await interaction.editReply(`Sorry, user _#${user_id}_ does not wish to expose this information to the public.`); + return; + } + + const server_version = 115; + const platform = "Web"; + const response = await fetch(`https://gobattle.io/api.php/bootstrap/${server_version}/${gobattle_token}?platform=${platform}&ud=`); + let data = await response.json(); + + if (!response.ok){ + if (data?.error == "Invalid token"){ + database.remove_gobattle_access_by_gobattle_user_id(user_id); + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + await interaction.editReply(`Unable to retrieve user data. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + function get_embed_user(data_user){ + const embed = new EmbedBuilder(); + + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + const streamer_emoji = application_emoji_cache.get("compass_item122") || "🔴"; + const head_data = heads_map.get(data_user?.skin_head); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + + embed.setTitle(`${head_emoji} ${data_user.streamer ? `${streamer_emoji} ` : ""}**${restrict_text(data_user.nick, 60)}**#${data_user.id}`); + if (typeof head_emoji != "string"){ + embed.setThumbnail(head_emoji.imageURL()); + } + + const level = get_level(data_user.experience); + const description = data_user.admin ? "This user is an **administrator**." : "_This user has no description..._"; + embed.setDescription(`${get_level_to_emojis(Math.min(level, 2000))}\n${restrict_text(description, 250)}`); + embed.setColor(0x500000); + + const id_emoji = application_emoji_cache.get("item_348") || "đŸˇī¸"; + const coins_emoji = application_emoji_cache.get("coin") || "đŸĒ™"; + const diamonds_emoji = "💎"; + const level_emoji = application_emoji_cache.get("compass_item121") || "đŸ’Ē"; + const experience_emoji = application_emoji_cache.get("fuego") || "đŸ”Ĩ"; + const role_emoji = application_emoji_cache.get("item_36") || "🎖"; + + embed.addFields( + {name: `> ${id_emoji} __Identifier__`, value: `> ${data_user.id}`, inline: true}, + {name: `> ${coins_emoji} __Coins__`, value: `> ${format_score_with_commas(data_user.coins)}`, inline: true}, + {name: `> ${diamonds_emoji} __Diamonds__`, value: `> ***${format_score_with_commas(data_user.diamonds)}***`, inline: true} + ); + + embed.addFields( + {name: `> ${level_emoji} __Level__`, value: `> ${format_score_with_commas(level)}`, inline: true}, + {name: `> ${experience_emoji} __Experience__`, value: `> ${format_score_with_commas(data_user.experience)}`, inline: true}, + {name: `> ${role_emoji} __Role__`, value: `> ${data_user.streamer ? `${streamer_emoji} Streamer` : "Player"}`, inline: true} + ); + + const equipped_attack = data_user.attack - data_user.base_attack; + const equipped_defense = data_user.defense - data_user.base_defense; + const equipped_luck = data_user.luck - data_user.base_luck; + const equipped_hp = data_user.hp - data_user.base_hp; + const equipped_regeneration = data_user.regeneration - data_user.base_regeneration; + const equipped_speed = data_user.speed - data_user.base_speed; + + const formatted_attack = data_user.attack > 0 ? "+" + data_user.attack : data_user.attack; + const formatted_defense = data_user.defense > 0 ? "+" + data_user.defense : data_user.defense; + const formatted_luck = data_user.luck > 0 ? "+" + data_user.luck : data_user.luck; + const formatted_hp = data_user.hp > 0 ? "+" + data_user.hp : data_user.hp; + const formatted_regeneration = data_user.regeneration > 0 ? "+" + data_user.regeneration : data_user.regeneration; + const formatted_speed = data_user.speed > 0 ? "+" + data_user.speed : data_user.speed; + + const formatted_base_attack = data_user.base_attack > 0 ? "+" + data_user.base_attack : data_user.base_attack; + const formatted_base_defense = data_user.base_defense > 0 ? "+" + data_user.base_defense : data_user.base_defense; + const formatted_base_luck = data_user.base_luck > 0 ? "+" + data_user.base_luck : data_user.base_luck; + const formatted_base_hp = data_user.base_hp > 0 ? "+" + data_user.base_hp : data_user.base_hp; + const formatted_base_regeneration = data_user.base_regeneration > 0 ? "+" + data_user.base_regeneration : data_user.base_regeneration; + const formatted_base_speed = data_user.base_speed > 0 ? "+" + data_user.base_speed : data_user.base_speed; + + const formatted_equipped_attack = equipped_attack > 0 ? "+" + equipped_attack : equipped_attack; + const formatted_equipped_defense = equipped_defense > 0 ? "+" + equipped_defense : equipped_defense; + const formatted_equipped_luck = equipped_luck > 0 ? "+" + equipped_luck : equipped_luck; + const formatted_equipped_hp = equipped_hp > 0 ? "+" + equipped_hp : equipped_hp; + const formatted_equipped_regeneration = equipped_regeneration > 0 ? "+" + equipped_regeneration : equipped_regeneration; + const formatted_equipped_speed = equipped_speed > 0 ? "+" + equipped_speed : equipped_speed; + + const attack_emoji = application_emoji_cache.get("compass_item95") || "âš”ī¸"; + const defense_emoji = application_emoji_cache.get("item_119") || "đŸ›Ąī¸"; + const luck_emoji = application_emoji_cache.get("item_478") || "🍀"; + const hp_emoji = application_emoji_cache.get("heart") || "â¤ī¸"; + const regeneration_emoji = application_emoji_cache.get("item_622") || "â¤ī¸â€đŸŠš"; + const speed_emoji = application_emoji_cache.get("item_122") || "⚡"; + + embed.addFields( + {name: `> ${attack_emoji} __ATT__`, value: `> **${formatted_attack}** _(Base: ${formatted_base_attack}, Equipped: ${formatted_equipped_attack})_`, inline: true}, + {name: `> ${defense_emoji} __DEF__`, value: `> **${formatted_defense}** _(Base: ${formatted_base_defense}, Equipped: ${formatted_equipped_defense})_`, inline: true}, + {name: `> ${luck_emoji} __LCK__`, value: `> **${formatted_luck}** _(Base: ${formatted_base_luck}, Equipped: ${formatted_equipped_luck})_`, inline: true} + ); + + embed.addFields( + {name: `> ${hp_emoji} __MHP__`, value: `> **${formatted_hp}** _(Base: ${formatted_base_hp}, Equipped: ${formatted_equipped_hp})_`, inline: true}, + {name: `> ${regeneration_emoji} __RGN__`, value: `> **${formatted_regeneration}** _(Base: ${formatted_base_regeneration}, Equipped: ${formatted_equipped_regeneration})_`, inline: true}, + {name: `> ${speed_emoji} __SPD__`, value: `> **${formatted_speed}** _(Base: ${formatted_base_speed}, Equipped: ${formatted_equipped_speed})_`, inline: true} + ); + + embed.setTimestamp(); + + return embed; + } + + function get_price_skills(data_user){ + return { + attack: 5 + 10 * data_user.base_attack, + defense: 5 + 10 * data_user.base_defense, + luck: 5 + 10 * data_user.base_luck, + hp: 5 + 10 * data_user.base_hp, + regeneration: 5 + 10 * data_user.base_regeneration, + speed: 5 + 10 * data_user.base_speed + }; + } + + function get_buypoint_button_user(data_user){ + const diamonds_emoji = "💎"; + const max_skill_points = 20; + + const price_skills = get_price_skills(data_user); + + const attack_button = new ButtonBuilder(); + attack_button.setCustomId("attack"); + attack_button.setEmoji(diamonds_emoji); + attack_button.setLabel(data_user.base_attack < max_skill_points ? `Buy 1 ATT point for ${price_skills.attack}` : "ATT points is at MAX"); + attack_button.setStyle(ButtonStyle.Primary); + attack_button.setDisabled(data_user.base_attack >= max_skill_points || price_skills.attack > data_user.diamonds); + + const defense_button = new ButtonBuilder(); + defense_button.setCustomId("defense"); + defense_button.setEmoji(diamonds_emoji); + defense_button.setLabel(data_user.base_defense < max_skill_points ? `Buy 1 DEF point for ${price_skills.defense}` : "DEF points is at MAX"); + defense_button.setStyle(ButtonStyle.Primary); + defense_button.setDisabled(data_user.base_defense >= max_skill_points || price_skills.defense > data_user.diamonds); + + const luck_button = new ButtonBuilder(); + luck_button.setCustomId("luck"); + luck_button.setEmoji(diamonds_emoji); + luck_button.setLabel(data_user.base_luck < max_skill_points ? `Buy 1 LCK point for ${price_skills.luck}` : "LCK points is at MAX"); + luck_button.setStyle(ButtonStyle.Primary); + luck_button.setDisabled(data_user.base_luck >= max_skill_points || price_skills.luck > data_user.diamonds); + + const mhp_button = new ButtonBuilder(); + mhp_button.setCustomId("hp"); + mhp_button.setEmoji(diamonds_emoji); + mhp_button.setLabel(data_user.base_hp < max_skill_points ? `Buy 1 MHP point for ${price_skills.hp}` : "RGN points is at MAX"); + mhp_button.setStyle(ButtonStyle.Primary); + mhp_button.setDisabled(data_user.base_hp >= max_skill_points || price_skills.hp > data_user.diamonds); + + const regeneration_button = new ButtonBuilder(); + regeneration_button.setCustomId("regeneration"); + regeneration_button.setEmoji(diamonds_emoji); + regeneration_button.setLabel(data_user.base_regeneration < max_skill_points ? `Buy 1 RGN point for ${price_skills.regeneration}` : "RGN points is at MAX"); + regeneration_button.setStyle(ButtonStyle.Primary); + regeneration_button.setDisabled(data_user.base_regeneration >= max_skill_points || price_skills.regeneration > data_user.diamonds); + + const speed_button = new ButtonBuilder(); + speed_button.setCustomId("speed"); + speed_button.setEmoji(diamonds_emoji); + speed_button.setLabel(data_user.base_speed < max_skill_points ? `Buy 1 SPD point for ${price_skills.speed}` : "SPD points is at MAX"); + speed_button.setStyle(ButtonStyle.Primary); + speed_button.setDisabled(data_user.base_speed >= max_skill_points || price_skills.speed > data_user.diamonds); + + const row_1 = new ActionRowBuilder(); + row_1.addComponents(attack_button, defense_button, luck_button); + + const row_2 = new ActionRowBuilder(); + row_2.addComponents(mhp_button, regeneration_button, speed_button); + + return [row_1, row_2]; + } + + const embed = get_embed_user(data.user); + + const gobattle_user_id_in_database = database.discord_user_to_gobattle_user_id(interaction.user); + const components_visible = data.user.id == gobattle_user_id_in_database || is_my_developer(client, interaction.user); + + const response_interaction = await interaction.editReply({ + embeds: [embed], + components: components_visible ? get_buypoint_button_user(data.user) : undefined + }); + + function collector_filter(m){ + const result = m.user.id == interaction.user.id || database.discord_user_to_gobattle_user_id(m.user) == gobattle_user_id_in_database || is_my_developer(m.user.client, m.user); + + if (!result){ + m.reply({content: "You cannot interact with a command that you did not initiate yourself.", ephemeral: true}).catch((error) => { + console.error(error); + }); + } + + return result; + } + + async function button_interaction_logic(response_interaction){ + let confirmation_message; + + try{ + const skill_interaction = await response_interaction.awaitMessageComponent({filter: collector_filter, componentType: ComponentType.Button, time: 60_000}); + + const confirm_button = new ButtonBuilder(); + confirm_button.setCustomId("confirm"); + confirm_button.setLabel("Confirm"); + confirm_button.setStyle(ButtonStyle.Success); + + const cancel_button = new ButtonBuilder(); + cancel_button.setCustomId("cancel"); + cancel_button.setLabel("Cancel"); + cancel_button.setStyle(ButtonStyle.Danger); + + const row = new ActionRowBuilder(); + row.addComponents(confirm_button, cancel_button); + + await skill_interaction.deferReply(); + confirmation_message = await skill_interaction.followUp({ + content: `Do you want to buy 1 **${skill_interaction.customId.toLocaleUpperCase()}** point for **${get_price_skills(data.user)[skill_interaction.customId]}** diamonds?`, + components: [row], + ephemeral: true + }); + + await response_interaction.edit({components: []}); + + const new_data_user = await get_purchase_confirmation_skill(confirmation_message, client, skill_interaction.user.id, user_id, gobattle_token, skill_interaction.customId); + + if (new_data_user){ + data.user = new_data_user; + } + + const embed = get_embed_user(data.user); + const rows = get_buypoint_button_user(data.user); + + await response_interaction.edit({embeds: [embed], components: rows}); + await button_interaction_logic(response_interaction); + }catch (_error){ + await interaction.editReply({content: "-# ⓘ This interaction has expired, please use the command again to be able to pay skill points.", components: []}); + + if (confirmation_message){ + await confirmation_message.edit({content: "Payment for skill point has been canceled.", components: []}); + } + } + } + + if (components_visible){ + await button_interaction_logic(response_interaction); + } + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this user.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_purchase_confirmation_skill(confirmation_message, client, discord_user_id, gobattle_user_id, gobattle_token, skill_type){ + function collector_filter(m){ + const result = m.user.id == discord_user_id || is_my_developer(m.user.client, m.user); + + if (!result){ + m.reply({content: "You cannot interact with a command that you did not initiate yourself.", ephemeral: true}).catch((error) => { + console.error(error); + }); + } + + return result; + } + + try{ + const confirmation = await confirmation_message.awaitMessageComponent({filter: collector_filter, componentType: ComponentType.Button, time: 60_000}); + if (confirmation.customId == "cancel"){ + await confirmation.update({content: "Payment for skill point has been canceled.", components: []}); + return; + } + + const platform = "Web"; + const request_info = { + method: "POST" + }; + const response = await fetch(`https://gobattle.io/api.php/buypoint/${skill_type}/${gobattle_token}?platform=${platform}&ud=`, request_info); + const data = await response.json(); + + if (!response.ok){ + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_gobattle_user_id(gobattle_user_id); + await confirmation.update({content: `User _#${gobattle_user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`, components: []}); + return; + case "Invalid item or no enough money": + await confirmation.update({content: "Unable to pay skill point. Invalid item or no enough money!", components: []}); + return; + case "Invalid category": + await confirmation.update({content: `Unable to pay skill point. Invalid category!\nContact ${client.application.owner} to resolve this issue.`, components: []}); + return; + default: + await confirmation.update({content: `Unable to pay skill point. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`, components: []}); + } + return; + } + + await confirmation.update({content: "The skill point has been paid.", components: []}); + + return data; + }catch(error){ + await confirmation_message.editReply({content: "Payment for skill point has been canceled.", components: []}); + } +} + +async function get_king(interaction, _client){ + await interaction.reply("This subcommand is currently under development. You cannot use it at the moment."); +} + +async function get_bank(interaction, _client){ + const user_id = interaction.options.get("user_id")?.value; + await interaction.reply("This subcommand is currently under development. You cannot use it at the moment."); +} + +async function get_inventory(interaction, _client){ + const user_id = interaction.options.get("user_id")?.value; + await interaction.reply("This subcommand is currently under development. You cannot use it at the moment."); +} + +async function get_gobattle_to_discord(interaction, client){ + await interaction.deferReply(); + + const user_id = interaction.options.get("user_id")?.value; + const discord_user_id = database.gobattle_user_id_to_discord_user_id(user_id); + + if (!discord_user_id){ + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + const user_discord_promise = client.users.fetch(discord_user_id.toString()); + user_discord_promise.then(async (user_discord) => { + await interaction.editReply(`GoBattle account _#${user_id}_ belongs to ${user_discord}.`); + }).catch(async (_error) => { + await interaction.editReply(`GoBattle account _#${user_id}_ belongs to an unknown or deleted user account. The user must log in to their account with \`/user login\`.`); + }); +} + +async function get_discord_to_gobattle(interaction, _client){ + await interaction.deferReply(); + + const discord_user = interaction.options.get("user").user; + const gobattle_user_id = database.discord_user_to_gobattle_user_id(discord_user); + + if (!gobattle_user_id){ + await interaction.editReply(`The GoBattle.io session for ${discord_user} is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + await interaction.editReply(`The Discord account ${discord_user} belongs to the GoBattle account _#${gobattle_user_id}_.`); +} + +async function get_change_nickname(interaction, client){ + await interaction.deferReply({ephemeral: true}); + + const user_id = interaction.options.get("user_id")?.value; + const nick = interaction.options.get("nick")?.value; + + const can_change = database.discord_user_to_gobattle_user_id(interaction.user) == user_id || is_my_developer(client, interaction.user); + if (!can_change){ + await interaction.editReply("You cannot change the nickname of a GoBattle account that does not belong to you."); + return; + } + + const gobattle_token = database.get_gobattle_token_by_gobattle_id(user_id); + if (!gobattle_token){ + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + const platform = "Web"; + const request_info = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + "nick": nick + }) + }; + const response = await fetch(`https://gobattle.io/api.php/nick/${gobattle_token}?platform=${platform}&ud=`, request_info); + const data = await response.json(); + + if (!response.ok){ + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_gobattle_user_id(user_id); + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + case "Nick already in use": + await interaction.editReply(`The nickname **${nick}** is already in use.`); + return; + case "Invalid nick": + await interaction.editReply(`The \`${nick}\` nickname is invalid, please use a valid nickname.`); + return; + default: + await interaction.editReply(`Unable to change nickname. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + await interaction.editReply(`The nickname has been changed to **${data?.nick}**.`); +} + +async function get_friend_pending_count(interaction, client){ + await interaction.deferReply(); + + const user_id = interaction.options.get("user_id")?.value; + const gobattle_token = database.get_gobattle_token_by_gobattle_id(user_id); + + if (!gobattle_token){ + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + const platform = "Web"; + const api_version = 1; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/pending/count?platform=${platform}&ud=`); + const data = await response.json(); + + if (data?.error == "Invalid token"){ + database.remove_gobattle_access_by_gobattle_user_id(user_id); + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + if (!response.ok){ + await interaction.editReply(`Unable to retrieve user data. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + await interaction.editReply(`User _#${user_id}_ currently has **${data.incoming}** incoming friend requests.`); +} + +async function get_friend_pending_requests(interaction, client, type){ + const user_id = interaction.options.get("user_id")?.value; + + const public = type != database.gobattle_user_id_to_discord_user_id(user_id)?.toString() == interaction.user.id || is_my_developer(client, interaction.user); + await interaction.deferReply({ephemeral: public && type == "incoming"}); + + const gobattle_token = database.get_gobattle_token_by_gobattle_id(user_id); + if (!gobattle_token){ + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + if (!public){ + await interaction.editReply(`Sorry, user _#${user_id}_ does not wish to expose this information to the public.`); + return; + } + + const platform = "Web"; + const api_version = 1; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/pending?platform=${platform}&ud=`); + const data = await response.json(); + + if (data?.error == "Invalid token"){ + database.remove_gobattle_access_by_gobattle_user_id(user_id); + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + if (!response.ok){ + await interaction.editReply(`Unable to retrieve user data. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const embed = new EmbedBuilder(); + const friend_emoji = "🤝"; + embed.setTitle(`${friend_emoji} Friend ${type} requests ${friend_emoji}`); + + const requests = data[type]; + const header_description = `User _#${user_id}_ has **${requests.length}** ${type} friend requests.`; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(requests.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + + let current_page = -1; + for (let i = 0; i < requests.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = requests[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + pages[current_page] += `* ${head_emoji} **${field?.nick ? restrict_text(field?.nick, 20) : "?"}**#${field?.id}\n`; + } + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages, header_description); +} + +async function get_friend_list(interaction, client){ + const user_id = interaction.options.get("user_id")?.value; + const public = database.gobattle_user_id_to_discord_user_id(user_id)?.toString() == interaction.user.id || is_my_developer(client, interaction.user); + + await interaction.deferReply({ephemeral: public}); + + const gobattle_token = database.get_gobattle_token_by_gobattle_id(user_id); + if (!gobattle_token){ + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + if (!public){ + await interaction.editReply(`Sorry, user _#${user_id}_ does not wish to expose this information to the public.`); + return; + } + + const platform = "Web"; + const api_version = 1; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/list?platform=${platform}&ud=`); + const data = await response.json(); + + if (data?.error == "Invalid token"){ + database.remove_gobattle_access_by_gobattle_user_id(user_id); + await interaction.editReply(`User _#${user_id}_ session is unknown to me or has expired. The user must log in to their account with \`/user login\`.`); + return; + } + + if (!response.ok){ + await interaction.editReply(`Unable to retrieve user data. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const embed = new EmbedBuilder(); + const friend_emoji = "🤝"; + embed.setTitle(`${friend_emoji} Friends list ${friend_emoji}`); + + const header_description = `User _#${user_id}_ has **${data.length}/100** friends.`; + const max_items_by_pages = 7; + const pages = new Array(Math.ceil(data.length / max_items_by_pages)); + const unknown_head_emoji = application_emoji_cache.get("heads_item_0") || "👤"; + const streamer_emoji = application_emoji_cache.get("compass_item122") || "🔴"; + const experience_emoji = application_emoji_cache.get("fuego") || "đŸ”Ĩ"; + const level_emoji = application_emoji_cache.get("compass_item121") || "🔰"; + + let current_page = -1; + for (let i = 0; i < data.length; i++){ + if (Math.floor(i / max_items_by_pages) != current_page){ + current_page++; + pages[current_page] = ""; + } + + const field = data[i]; + const head_data = heads_map.get(parseInt(field?.skin_head, 10)); + const head_emoji = application_emoji_cache.get(head_data?.emoji) || unknown_head_emoji; + const exp = parseInt(field?.experience, 10); + const level = get_level(exp); + /*const date = new Date(field?.ts + "Z"); + const timestamp = date.getTime() / 1000;*/ + const online_label = field?.online ? `**Online** (__*${field?.playing}*__) in __*${field?.joinFriendlyName}*__.` : "**Offline**"; + pages[current_page] += `* ${head_emoji} ${false && field?.status == 1 ? streamer_emoji : ""}**${field?.nick ? restrict_text(field?.nick, 20) : "?"}**#${field?.id}: ${online_label} ${experience_emoji}\`${format_score(field?.experience)}\` ${level_emoji}\`${level}\`\n`; + } + + embed.setTimestamp(); + + await send_embed_layout(interaction, embed, pages, header_description); +} + +async function get_friend_add(interaction, client){ + await interaction.deferReply(); + + try{ + const user_id = interaction.options.get("user_id")?.value; + + const my_gobattle_user_id = database.discord_user_to_gobattle_user_id(interaction.user); + if (!my_gobattle_user_id){ + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + } + + if (my_gobattle_user_id == user_id){ + await interaction.editReply("You cannot send yourself a friend request..."); + return; + } + + const gobattle_token = database.get_gobattle_token_by_discord_user(interaction.user); + if (!gobattle_token){ + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + } + + const api_version = 1; + const platform = "Web"; + const request_info = { + method: "POST" + }; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/request/${user_id}/add?platform=${platform}&ud=`, request_info); + + if (!response.ok){ + const data = await response.json(); + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_gobattle_user_id(my_gobattle_user_id); + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + case "Account is already a friend": + await interaction.editReply(`You are already friends with user _#${user_id}_! Congratulations!`); + return; + case "Error creating friend request": + await interaction.editReply(`User _#${user_id}_ no longer accepts friend requests.`); + return; + case "Account not found": + await interaction.editReply(`The user _#${user_id}_ no longer exists or never existed.`); + return; + case "No more requests today": + await interaction.editReply("You cannot send more friend requests today."); + return; + default: + await interaction.editReply(`Unable to send a friend request to the user. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + const data_text = await response.text(); + if (data_text == "ok"){ + await interaction.editReply(`The friend request was sent to user _#${user_id}_.`); + }else{ + await interaction.editReply(`You have already sent a friend request to user _#${user_id}_. Wait for him to give an answer.`); + } + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this user.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_friend_delete(interaction, client){ + await interaction.deferReply(); + + try{ + const user_id = interaction.options.get("user_id")?.value; + + const gobattle_token = database.get_gobattle_token_by_discord_user(interaction.user); + if (!gobattle_token){ + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + } + + const api_version = 1; + const platform = "Web"; + const request_info = { + method: "POST" + }; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/${user_id}/delete?platform=${platform}&ud=`, request_info); + + const data = await response.json(); + if (!response.ok){ + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_discord_user(interaction.user); + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + default: + await interaction.editReply(`Unable to remove user _#${user_id}_ from friends list. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + if (data == "ok"){ + await interaction.editReply(`The user _#${user_id}_ has been deleted from the friends list.`); + }else{ + await interaction.editReply(`The user _#${user_id}_ is already not present in your friends list.`); + } + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this user.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_friend_cancel(interaction, client){ + await interaction.deferReply(); + + try{ + const user_id = interaction.options.get("user_id")?.value; + + const gobattle_token = database.get_gobattle_token_by_discord_user(interaction.user); + if (!gobattle_token){ + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + } + + const api_version = 1; + const platform = "Web"; + const request_info = { + method: "POST" + }; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/request/${user_id}/delete?platform=${platform}&ud=`, request_info); + + const data = await response.json(); + if (!response.ok){ + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_discord_user(interaction.user); + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + default: + await interaction.editReply(`Unable to cancel friend request sent to user _#${user_id}_. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + if (data == "ok"){ + await interaction.editReply(`The friend request sent to user _#${user_id}_ was canceled.`); + }else{ + await interaction.editReply(`The friend request sent to user _#${user_id}_ has already been canceled.`); + } + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this user.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_friend_accept(interaction, client){ + await interaction.deferReply(); + + try{ + const user_id = interaction.options.get("user_id")?.value; + + const gobattle_token = database.get_gobattle_token_by_discord_user(interaction.user); + if (!gobattle_token){ + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + } + + const api_version = 1; + const platform = "Web"; + const request_info = { + method: "POST" + }; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/request/${user_id}/accept?platform=${platform}&ud=`, request_info); + + const data = await response.json(); + if (!response.ok){ + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_discord_user(interaction.user); + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + case "Error accepting request": + await interaction.editReply(`You have reached the limit on the number of possible friends or the user _#${user_id}_ has reached his limit on the number of friends or is no longer accepting new friends.`); + return; + default: + await interaction.editReply(`Unable to accept friend request for user _#${user_id}_. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + if (data == "ok"){ + await interaction.editReply(`The friend request for user _#${user_id}_ has been accepted.`); + }else{ + await interaction.editReply(`You have already accepted the friend request from user _#${user_id}_.`); + } + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this user.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +async function get_friend_ignore(interaction, client){ + await interaction.deferReply(); + + try{ + const user_id = interaction.options.get("user_id")?.value; + + const gobattle_token = database.get_gobattle_token_by_discord_user(interaction.user); + if (!gobattle_token){ + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + } + + const api_version = 1; + const platform = "Web"; + const request_info = { + method: "POST" + }; + const response = await fetch(`https://gobattle.io/api.php/v${api_version}/${gobattle_token}/friend/request/${user_id}/ignore?platform=${platform}&ud=`, request_info); + + const data = await response.json(); + if (!response.ok){ + switch (data?.error){ + case "Invalid token": + database.remove_gobattle_access_by_discord_user(interaction.user); + await interaction.editReply("Your user session is unknown to me or has expired. You must log in to your account with `/user login`."); + return; + default: + await interaction.editReply(`Unable to ignore friend request for user _#${user_id}_. There is a problem with the Gobattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + if (data == "ok"){ + await interaction.editReply(`The friend request for user _#${user_id}_ has been ignored.`); + }else{ + await interaction.editReply(`The friend request for user _#${user_id}_ has already been ignored.`); + } + }catch(error){ + await interaction.editReply(`Unable to retrieve information on this user.\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } +} + +exports.get_user = get_user; +exports.user_command = user_command; diff --git a/src/database/database.js b/src/database/database.js new file mode 100644 index 0000000..350887a --- /dev/null +++ b/src/database/database.js @@ -0,0 +1,142 @@ +"use strict"; + +const sqlite = require("node:sqlite"); +const fs = require("fs"); + +const database = new sqlite.DatabaseSync("database.sqlite"); + +let add_gobattle_access_statement; +let remove_gobattle_access_statement; +let get_gobattle_token_by_gobattle_user_id_statement; +let get_gobattle_token_by_discord_user_id_statement; +let discord_user_id_to_gobattle_user_id_statement; +let gobattle_user_id_to_discord_user_id_statement; + +function init(){ + fs.readFile(__dirname + "/schema.sql", "utf8", function (error, data){ + if (error){ + console.error(error); + return; + } + + database.exec(data); + + add_gobattle_access_statement = database.prepare(` + INSERT INTO user_session (discord_user_id, gobattle_user_id, gobattle_token) + VALUES ($discord_user_id, $gobattle_user_id, $gobattle_token) + ON CONFLICT (discord_user_id) + DO UPDATE SET + discord_user_id = excluded.discord_user_id, + gobattle_token = excluded.gobattle_token, + session_date = CURRENT_TIMESTAMP + ON CONFLICT (gobattle_user_id) + DO UPDATE SET + discord_user_id = excluded.discord_user_id, + gobattle_token = excluded.gobattle_token, + session_date = CURRENT_TIMESTAMP; + `); + + remove_gobattle_access_statement = database.prepare(` + DELETE FROM user_session WHERE gobattle_user_id = $gobattle_user_id; + `); + + get_gobattle_token_by_gobattle_user_id_statement = database.prepare(` + SELECT gobattle_token FROM user_session WHERE gobattle_user_id = $gobattle_user_id; + `); + + get_gobattle_token_by_discord_user_id_statement = database.prepare(` + SELECT gobattle_token FROM user_session WHERE discord_user_id = $discord_user_id; + `); + + discord_user_id_to_gobattle_user_id_statement = database.prepare(` + SELECT gobattle_user_id FROM user_session WHERE discord_user_id = $discord_user_id; + `); + + gobattle_user_id_to_discord_user_id_statement = database.prepare(` + SELECT discord_user_id FROM user_session WHERE gobattle_user_id = $gobattle_user_id; + `); + gobattle_user_id_to_discord_user_id_statement.setReadBigInts(true); + }); +} + +function add_gobattle_access(discord_user, gobattle_user_id, gobattle_token){ + const discord_user_prime = gobattle_user_id_to_discord_user_id(gobattle_user_id); + if (discord_user_prime?.toString() == discord_user.id){ + return false; + } + + add_gobattle_access_statement.run({ + $discord_user_id: BigInt(discord_user.id), + $gobattle_user_id: gobattle_user_id, + $gobattle_token: gobattle_token + }); + + return true; +} + +function remove_gobattle_access_by_discord_user(discord_user){ + const gobattle_user_id = discord_user_to_gobattle_user_id(discord_user); + if (!gobattle_user_id){ + return false; + } + + remove_gobattle_access_statement.run({ + $gobattle_user_id: gobattle_user_id, + }); + + return true; +} + +function remove_gobattle_access_by_gobattle_user_id(gobattle_user_id){ + const discord_user_id = gobattle_user_id_to_discord_user_id(gobattle_user_id); + if (!discord_user_id){ + return false; + } + + remove_gobattle_access_statement.run({ + $gobattle_user_id: gobattle_user_id, + }); + + return true; +} + +function get_gobattle_token_by_gobattle_user_id(gobattle_user_id){ + const data = get_gobattle_token_by_gobattle_user_id_statement.get({ + $gobattle_user_id: gobattle_user_id + }); + + return data?.gobattle_token; +} + +function get_gobattle_token_by_discord_user(discord_user){ + const data = get_gobattle_token_by_discord_user_id_statement.get({ + $discord_user_id: discord_user.id + }); + + return data?.gobattle_token; +} + +function discord_user_to_gobattle_user_id(discord_user){ + const data = discord_user_id_to_gobattle_user_id_statement.get({ + $discord_user_id: BigInt(discord_user.id) + }); + + return data?.gobattle_user_id; +} + +function gobattle_user_id_to_discord_user_id(gobattle_user_id){ + const data = gobattle_user_id_to_discord_user_id_statement.get({ + $gobattle_user_id: gobattle_user_id + }); + + return data?.discord_user_id; +} + +exports.init = init; +exports.add_gobattle_access = add_gobattle_access; +exports.remove_gobattle_access_by_discord_user = remove_gobattle_access_by_discord_user; +exports.remove_gobattle_access_by_gobattle_user_id = remove_gobattle_access_by_gobattle_user_id; +exports.get_gobattle_token_by_gobattle_id = get_gobattle_token_by_gobattle_user_id; +exports.get_gobattle_token_by_discord_user = get_gobattle_token_by_discord_user; +exports.discord_user_to_gobattle_user_id = discord_user_to_gobattle_user_id +exports.gobattle_user_id_to_discord_user_id = gobattle_user_id_to_discord_user_id; \ No newline at end of file diff --git a/src/database/schema.sql b/src/database/schema.sql new file mode 100644 index 0000000..6b24a30 --- /dev/null +++ b/src/database/schema.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS user_session ( + discord_user_id INTEGER PRIMARY KEY CHECK (discord_user_id > 0), + gobattle_user_id INTEGER UNIQUE CHECK (gobattle_user_id > 0), + session_date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK ( + -- Validating the date format (YYYY-MM-DD HH:MM:SS). + length(session_date) = 19 AND + substr(session_date, 1, 4) GLOB "[0-9][0-9][0-9][0-9]" AND + substr(session_date, 5, 1) = "-" AND + substr(session_date, 6, 2) GLOB "[0-9][0-9]" AND + substr(session_date, 8, 1) = "-" AND + substr(session_date, 9, 2) GLOB "[0-9][0-9]" AND + substr(session_date, 11, 1) = " " AND + substr(session_date, 12, 2) GLOB "[0-9][0-9]" AND + substr(session_date, 14, 1) = ":" AND + substr(session_date, 15, 2) GLOB "[0-9][0-9]" AND + substr(session_date, 17, 1) = ":" AND + substr(session_date, 18, 2) GLOB "[0-9][0-9]" + ), + gobattle_token TEXT NOT NULL UNIQUE CHECK (length(gobattle_token) = 64) +) WITHOUT ROWID; \ No newline at end of file diff --git a/src/dungeon_list.json b/src/dungeon_list.json new file mode 100644 index 0000000..3bc6925 --- /dev/null +++ b/src/dungeon_list.json @@ -0,0 +1,165 @@ +{ + "author_id": 1201923723662467082, + "dungeon_list": [ + { + "id": 8, + "name": "Neewe Caves", + "min_level": null + }, + { + "id": 9, + "name": "Daragorn's Lair", + "min_level": null + }, + { + "id": 10, + "name": "Enchanted Forest", + "min_level": null + }, + { + "id": 12, + "name": "Spider Nest", + "min_level": null + }, + { + "id": 13, + "name": "Sawmill", + "min_level": null + }, + { + "id": 14, + "name": "Magma Caves", + "min_level": null + }, + { + "id": 15, + "name": "FireBreath's Lair", + "min_level": null + }, + { + "id": 16, + "name": "Asterion's Labyrinth", + "min_level": null + }, + { + "id": 17, + "name": "Dragonblood", + "min_level": null + }, + { + "id": 18, + "name": "Sir Valkan's Stronghold", + "min_level": null + }, + { + "id": 20, + "name": "Ravanor's Land", + "min_level": null + }, + { + "id": 22, + "name": "Danma Caverns", + "min_level": null + }, + { + "id": 23, + "name": "Viasar Castle", + "min_level": null + }, + { + "id": 24, + "name": "Fairy Forest", + "min_level": null + }, + { + "id": 25, + "name": "Kron Waterfalls", + "min_level": null + }, + { + "id": 27, + "name": "Dark Cave", + "min_level": null + }, + { + "id": 28, + "name": "Underwater magma caves", + "min_level": null + }, + { + "id": 31, + "name": "Stahmite Mines", + "min_level": null + }, + { + "id": 32, + "name": "The path of the warrior", + "min_level": null + }, + { + "id": 33, + "name": "Feeble's Deathtrap", + "min_level": null + }, + { + "id": 35, + "name": "DanCie Spikes", + "min_level": null + }, + { + "id": 36, + "name": "Scale Caves", + "min_level": null + }, + { + "id": 41, + "name": "Sky's Prison", + "min_level": null + }, + { + "id": 45, + "name": "Tower Lair", + "min_level": null + }, + { + "id": 46, + "name": "Bloodmoon Castle", + "min_level": null + }, + { + "id": 48, + "name": "Ruins of Kalambria", + "min_level": null + }, + { + "id": 50, + "name": "AzureFlora Abyss", + "min_level": null + }, + { + "id": 51, + "name": "Toxic Caves", + "min_level": null + }, + { + "id": 52, + "name": "Cloudy Sky", + "min_level": null + }, + { + "id": 53, + "name": "Cave Under Water", + "min_level": null + }, + { + "id": 54, + "name": "Cursed Cave", + "min_level": null + }, + { + "id": 57, + "name": "Frostbite Dungeon", + "min_level": null + } + ] +} \ No newline at end of file diff --git a/src/head_list.json b/src/head_list.json new file mode 100644 index 0000000..6e58714 --- /dev/null +++ b/src/head_list.json @@ -0,0 +1,292 @@ +[ + { + "id": 0, + "name": "Grey Head", + "emoji": "heads_item_0" + }, + { + "id": 1, + "name": "Black Head", + "emoji": "heads_item_1" + }, + { + "id": 2, + "name": "Blue Head", + "emoji": "heads_item_2" + }, + { + "id": 3, + "name": "Dark Blue Head", + "emoji": "heads_item_3" + }, + { + "id": 4, + "name": "Emerald Head", + "emoji": "heads_item_4" + }, + { + "id": 5, + "name": "Fire Head", + "emoji": "heads_item_5" + }, + { + "id": 6, + "name": "Skin color 1 Head", + "emoji": "heads_item_6" + }, + { + "id": 7, + "name": "Skin color 2 Head", + "emoji": "heads_item_7" + }, + { + "id": 8, + "name": "Skin color 3 Head", + "emoji": "heads_item_8" + }, + { + "id": 9, + "name": "Skin color 4 Head", + "emoji": "heads_item_9" + }, + { + "id": 10, + "name": "Gold Head", + "emoji": "heads_item_10" + }, + { + "id": 11, + "name": "Green Head", + "emoji": "heads_item_11" + }, + { + "id": 12, + "name": "Light Blue Head", + "emoji": "heads_item_12" + }, + { + "id": 13, + "name": "Orange Head", + "emoji": "heads_item_13" + }, + { + "id": 14, + "name": "Pink Head", + "emoji": "heads_item_14" + }, + { + "id": 15, + "name": "Purple Head", + "emoji": "heads_item_15" + }, + { + "id": 16, + "name": "Red Head", + "emoji": "heads_item_16" + }, + { + "id": 17, + "name": "White Head", + "emoji": "heads_item_17" + }, + { + "id": 18, + "name": "Yellow Head", + "emoji": "heads_item_18" + }, + { + "id": 19, + "name": "Cat Knight", + "emoji": "heads_item_19" + }, + { + "id": 20, + "name": "Sophie", + "emoji": "heads_item_20" + }, + { + "id": 21, + "name": "Hiroto", + "emoji": "heads_item_21" + }, + { + "id": 22, + "name": "Mary", + "emoji": "heads_item_22" + }, + { + "id": 23, + "name": "Happy", + "emoji": "heads_item_23" + }, + { + "id": 24, + "name": "Rhino", + "emoji": "heads_item_24" + }, + { + "id": 25, + "name": "M.R. Dalmau", + "emoji": "heads_item_25" + }, + { + "id": 26, + "name": "Cyclops", + "emoji": "heads_item_26" + }, + { + "id": 27, + "name": "Monk", + "emoji": "heads_item_27" + }, + { + "id": 28, + "name": "Santa", + "emoji": "heads_item_28" + }, + { + "id": 29, + "name": "Fire Skull", + "emoji": "heads_item_29" + }, + { + "id": 30, + "name": "Halloween", + "emoji": "heads_item_30" + }, + { + "id": 31, + "name": "Mr. Chicken", + "emoji": "heads_item_31" + }, + { + "id": 32, + "name": "Karateka", + "emoji": "heads_item_32" + }, + { + "id": 33, + "name": "Mr. Chief", + "emoji": "heads_item_33" + }, + { + "id": 34, + "name": "Julie", + "emoji": "heads_item_34" + }, + { + "id": 35, + "name": "Gold", + "emoji": "heads_item_35" + }, + { + "id": 36, + "name": "Samantha", + "emoji": "heads_item_36" + }, + { + "id": 37, + "name": "Bruce", + "emoji": "heads_item_37" + }, + { + "id": 38, + "name": "Mr. Strong", + "emoji": "heads_item_38" + }, + { + "id": 39, + "name": "Neena", + "emoji": "heads_item_39" + }, + { + "id": 40, + "name": "Kira", + "emoji": "heads_item_40" + }, + { + "id": 41, + "name": "Scarlett", + "emoji": "heads_item_41" + }, + { + "id": 42, + "name": "Beatrice", + "emoji": "heads_item_42" + }, + { + "id": 43, + "name": "Sigrid", + "emoji": "heads_item_43" + }, + { + "id": 44, + "name": "Nira", + "emoji": "heads_item_44" + }, + { + "id": 45, + "name": "Kaneda", + "emoji": "heads_item_45" + }, + { + "id": 46, + "name": "Kiroto", + "emoji": "heads_item_46" + }, + { + "id": 47, + "name": "Ryo", + "emoji": "heads_item_47" + }, + { + "id": 48, + "name": "Storm", + "emoji": "heads_item_48" + }, + { + "id": 49, + "name": "Ra", + "emoji": "heads_item_49" + }, + { + "id": 50, + "name": "Ari", + "emoji": "heads_item_50" + }, + { + "id": 51, + "name": "Knight", + "emoji": "heads_item_51" + }, + { + "id": 52, + "name": "Luna", + "emoji": "heads_item_52" + }, + { + "id": 53, + "name": "Bea", + "emoji": "heads_item_53" + }, + { + "id": 54, + "name": "Atria", + "emoji": "heads_item_54" + }, + { + "id": 55, + "name": "Black Knight", + "emoji": "heads_item_55" + }, + { + "id": 56, + "name": "Demonic black knight", + "emoji": "heads_item_56" + }, + { + "id": 255, + "name": "Phamy", + "emoji": "heads_item_57" + } +] diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..56e4225 --- /dev/null +++ b/src/index.js @@ -0,0 +1,402 @@ +const {Client, Events, Partials, IntentsBitField, Routes, ActivityType, ButtonBuilder, ButtonStyle, ActionRowBuilder, ComponentType} = require("discord.js"); +const {get_first_chat_channel, is_my_developer, update_application_emoji_cache, get_level_to_emojis, application_emoji_cache, get_info_application} = require("./utils.js"); +const database = require("./database/database.js"); + +const {get_ranking, ranking_command} = require("./commands/ranking.js"); +const {get_help, help_command} = require("./commands/help.js"); +const {get_asset, asset_command} = require("./commands/asset.js"); +const {get_item, item_command} = require("./commands/item.js"); +const {get_user, user_command} = require("./commands/user.js"); +const {get_server, server_command} = require("./commands/server.js"); +const {get_info, info_command} = require("./commands/info.js"); +const {get_dungeon, dungeon_command} = require("./commands/dungeon.js"); +const {get_date_new_king, date_new_king_command} = require("./commands/date_new_king.js"); +const {get_ping, ping_command} = require("./commands/ping.js"); +const {get_leave, leave_command} = require("./commands/leave.js"); +const {get_setting, setting_command} = require("./commands/setting.js"); +const {get_echo, echo_command} = require("./commands/echo.js"); +const {get_ultrarare_drop_chance, ultrarare_drop_chance_command} = require("./commands/ultrarare_drop_chance.js"); +const {get_install_link, install_link_command} = require("./commands/install_link.js"); + +const global_commands = [ + ranking_command, + help_command, + asset_command, + item_command, + user_command, + server_command, + info_command, + dungeon_command, + date_new_king_command, + ping_command, + leave_command, + ultrarare_drop_chance_command, + install_link_command +]; + +const private_commands = [ + setting_command, + echo_command +]; + +const client = new Client({ + intents: [ + IntentsBitField.Flags.Guilds, + IntentsBitField.Flags.GuildMembers, + IntentsBitField.Flags.GuildMessages, + IntentsBitField.Flags.MessageContent, + IntentsBitField.Flags.GuildMessageReactions, + IntentsBitField.Flags.GuildEmojisAndStickers + ], + partials: [Partials.Message, Partials.Channel, Partials.Reaction], +}); + +client.login(process.env.TOKEN); + +client.on(Events.ClientReady, async (event) => { + database.init(); + + console.log(`${event.user.tag} ready.`); + + client.user.setActivity("GoBattle.io", {type: ActivityType.Playing}); + + await client.application.fetch(); + await update_application_emoji_cache(client.application); + + try{ + await client.rest.put( + Routes.applicationCommands(client.user.id), + {body: global_commands} + ); + }catch (error){ + console.error(error); + } + + const guilds_admin_id = JSON.parse(process.env.GUILD_ADMIN_ID); + for (const guild_id of guilds_admin_id){ + try{ + await client.rest.put( + Routes.applicationGuildCommands(client.user.id, guild_id), + {body: private_commands} + ); + }catch (error){ + console.error(error); + } + } +}); + +client.on(Events.GuildCreate, async (guild) => { + try { + const channel = get_first_chat_channel(guild, client); + + if (channel){ + await channel.send("Hello, thank you for inviting me to this guild!\nYou can use the `/help` command to find out what I can do!"); + } + }catch (error){ + console.error(error); + } +}); + +client.on(Events.InteractionCreate, async (interaction) => { + try{ + if (interaction.isChatInputCommand()){ + switch (interaction.commandName){ + case "item": + await get_item(interaction, client); + break; + case "user": + await get_user(interaction, client); + break; + case "server": + await get_server(interaction, client); + break; + case "ranking": + await get_ranking(interaction, client); + break; + case "get_date_new_king": + await get_date_new_king(interaction, client); + break; + case "info": + await get_info(interaction, client); + break; + case "help": + await get_help(interaction, client); + break; + case "ping": + await get_ping(interaction, client); + break; + case "leave": + await get_leave(interaction, client); + break; + case "asset": + await get_asset(interaction, client); + break; + case "dungeon": + await get_dungeon(interaction, client); + break; + case "setting": + await get_setting(interaction, client); + break; + case "echo": + await get_echo(interaction, client); + break; + case "get_ultrarare_drop_chance": + await get_ultrarare_drop_chance(interaction, client); + break; + case "install_link": + await get_install_link(interaction, client); + break; + default: + await interaction.reply({content: "This command no longer exists.", ephemeral: true}); + } + }else if (interaction.isModalSubmit()){ + switch (interaction.customId){ + case "create": + try{ + await interaction.deferReply({ephemeral: true}); + + const email = interaction.fields.getTextInputValue("email"); + const password = interaction.fields.getTextInputValue("password"); + + const platform = "Web"; + const request_info = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + "email": email, + "password": password, + "trackingId": "" + }) + }; + + const response = await fetch(`https://gobattle.io/api.php/register?platform=${platform}&ud=`, request_info); + const data = await response.json(); + + if (!response.ok){ + switch (data?.error){ + case "Invalid email": + await interaction.editReply("# âš ī¸ Account creation error\nThe email introduced is not valid. Please reenter your email."); + return; + case "Password short or too easy": + await interaction.editReply("# âš ī¸ Account creation error\nYour password is too easy to guess. Please use a better password."); + return; + case "Duplicated user": + await interaction.editReply("# âš ī¸ Account creation error\nEmail is already in use."); + return; + default: + await interaction.editReply(`Unable to create account. There is a problem with the GoBattle API.\nContact ${client.application.owner} to resolve this issue.`); + } + return; + } + + const info = await get_info_application(client.application); + const tos = info?.terms_of_service_url || "https://gobattle.io/tos.html"; + const pp = info?.privacy_policy_url || "https://www.iubenda.com/privacy-policy/8108614"; + interaction.editReply(`Thank you for register to GoBattle.io.\nReview your Email and follow the instructions to activate your account _#${data.id}_.\nBy continuing to create your account we assume that you accept our [Terms of Use](${tos}) and our [Privacy Policy](${pp}).`); + }catch (error){ + await interaction.editReply(`An internal error has occurred...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } + break; + case "login": + try{ + await interaction.deferReply({ephemeral: true}); + + const email = interaction.fields.getTextInputValue("email"); + const password = interaction.fields.getTextInputValue("password"); + + const platform = "Web"; + const request_info = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + "email": email, + "password": password + }) + }; + + const response = await fetch(`https://gobattle.io/api.php/login?platform=${platform}&ud=`, request_info); + const data = await response.json(); + + if (!response.ok){ + if (data?.error == "Invalid credentials"){ + await interaction.editReply("# âš ī¸ Login error\nThe email or the passworld are incorrect."); + return; + } + + await interaction.editReply(`Unable to connect. There is a problem with the GoBattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + const success = database.add_gobattle_access(interaction.user, data.id, data.token); + if (success){ + interaction.editReply("Your account has been successfully registered. You can log out at any time with `/user logout`."); + }else{ + interaction.editReply("Your account is already registered. You can log out at any time with `/user logout`."); + } + }catch (error){ + await interaction.editReply(`An internal error has occurred...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } + break; + case "recover": + try{ + await interaction.deferReply({ephemeral: true}); + + const email = interaction.fields.getTextInputValue("email"); + + const platform = "Web"; + const request_info = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + "email": email + }) + }; + + const response = await fetch(`https://gobattle.io/api.php/recover?platform=${platform}&ud=`, request_info); + const data = await response.json(); + + if (!response.ok){ + if (data?.error == "Error sending email"){ + await interaction.editReply("# âš ī¸ Recovery error\nThe email is incorrect."); + return; + } + + await interaction.editReply(`Unable to recover. There is a problem with the GoBattle API.\nContact ${client.application.owner} to resolve this issue.`); + return; + } + + interaction.editReply(`This is the Email of the user _#${data.id}_. Let's ckeck out your email and follow instructions fer next steps. Let's play GoBattle!`); + }catch (error){ + await interaction.editReply(`An internal error has occurred...\nContact ${client.application.owner} to resolve this issue.`); + console.error(error); + } + break; + default: + await interaction.reply({content: "The form submission was not processed successfully because I am not familiar with the form.", ephemeral: true}); + } + } + }catch (error){ + console.error(error); + } +}); + +client.on(Events.MessageCreate, async (msg) => { + if (msg.author.bot){ + return; + } + + try{ + const command_info = msg.content.trim().split(" "); + const command = command_info[0].toLowerCase(); + + // Quick commands for development. + let content = ""; + switch (command){ + case "!gb_bot_guilds": + if (!is_my_developer(client, msg.author)){ + await msg.reply("You do not have permission to use this command."); + return; + } + + const guilds = client.guilds.cache; + content = "# List of guilds I am in:\n\n"; + for (const guild of guilds.values()){ + content += `* ${guild.name}: \`${guild.id}\`,\n`; + } + content += `(${guilds.size} Guilds)`; + await msg.reply(content); + + break; + case "!get_off_this_server": + if (!is_my_developer(client, msg.author)){ + await msg.reply("You do not have permission to use this command."); + return; + } + + await msg.reply("Well, I'm leaving this guild because I seem to be disturbing..."); + await msg.guild.leave(); + + break; + case "!gb_emojis": + if (!is_my_developer(client, msg.author)){ + await msg.reply("You do not have permission to use this command."); + return; + } + + const nb_emoji = 60; + for (let i = 0; i < nb_emoji; i++){ + const emojis = Array.from(application_emoji_cache.values()); + const index = Math.floor(Math.random() * emojis.length); + + content += `${emojis[index]}`; + } + await msg.reply(content); + + break; + case "!gb_level_to_emojis": + if (!is_my_developer(client, msg.author)){ + await msg.reply("You do not have permission to use this command."); + return; + } + + const level = parseInt(command_info[1] || 0, 10); + await msg.reply(`Level ${level} is: (${get_level_to_emojis(level)})`); + + break; + case "!gb_emoji_name": + if (!is_my_developer(client, msg.author)){ + await msg.reply("You do not have permission to use this command."); + return; + } + + const emoji = application_emoji_cache.get(command_info[1]) || "_Unknown?_"; + await msg.reply(`The emoji is: ${emoji}`); + + break; + case "!gb_confirm": + if (!is_my_developer(client, msg.author)){ + await msg.reply("You do not have permission to use this command."); + return; + } + + const confirm_button = new ButtonBuilder(); + confirm_button.setCustomId("confirm"); + confirm_button.setLabel("Confirm"); + confirm_button.setStyle(ButtonStyle.Success); + + const cancel_button = new ButtonBuilder(); + cancel_button.setCustomId("cancel"); + cancel_button.setLabel("Cancel"); + cancel_button.setStyle(ButtonStyle.Danger); + + const row = new ActionRowBuilder(); + row.addComponents(confirm_button, cancel_button); + + const response_interaction = await msg.reply({ + content: "Hello dad, here is a confirmation message:", + components: [row] + }); + + const confirmation = await response_interaction.awaitMessageComponent({filter: () => {return true}, componentType: ComponentType.Button, time: 60_000}); + + if (confirmation.customId == "confirm"){ + await confirmation.update({content: "Confirm.", components: []}); + return; + } + + await confirmation.update({content: "Cancel.", components: []}); + + break; + } + }catch (error){ + console.error(error); + } +}); diff --git a/src/level_emojis.json b/src/level_emojis.json new file mode 100644 index 0000000..5995884 --- /dev/null +++ b/src/level_emojis.json @@ -0,0 +1,22 @@ +[ + "compass_item102", + "compass_item103", + "compass_item104", + "compass_item105", + "compass_item106", + "compass_item107", + "compass_item108", + "compass_item109", + "compass_item110", + "compass_item111", + "compass_item112", + "compass_item113", + "compass_item114", + "compass_item115", + "compass_item116", + "compass_item117", + "compass_item118", + "compass_item119", + "compass_item120", + "compass_item121" +] diff --git a/src/ultralist.json b/src/ultralist.json new file mode 100644 index 0000000..036a64e --- /dev/null +++ b/src/ultralist.json @@ -0,0 +1,478 @@ +{ + "author_id": 945345865176997989, + "ultra_list": [ + { + "name": "Restoration Ring", + "tickets": 500, + "drops": "Sky's Prison", + "id": 155, + "description": "It restores 50 points of your fatigue and boosts your defense by 50% for 40 seconds.", + "uses": 3, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_506" + }, + { + "name": "Gobattle Ring", + "tickets": 250, + "drops": "Stahmite Mines", + "id": 68, + "description": "Increases 50% of your attack, defense, speed, and jump for 30 seconds. Gives Invincibility for 30 seconds.", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_512" + }, + { + "name": "Firebreath Ring", + "tickets": 250, + "drops": "Firebreath's Lair", + "id": 114, + "description": "Invokes a fire attack, ring has 10 uses. The attack varies by the users attack. Sometimes, it will work with blue ring.", + "uses": 10, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_502" + }, + { + "name": "Blue Ring", + "tickets": 500, + "drops": "All", + "id": 2, + "description": "Increases your power attack 3x for 30 seconds", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_520" + }, + { + "name": "Red Ring", + "tickets": 500, + "drops": "All", + "id": 3, + "description": "Regenerates your health 8 times faster for 1 minute", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_515" + }, + { + "name": "Bloodmoon Ring", + "tickets": 1000, + "drops": "Bloodmoon Castle", + "id": 166, + "description": "A mystical ring that, when activated, conjures four fearsome bats with crimson eyes, serving as loyal companions and formidable allies under the blood moon's eerie light.", + "uses": 3, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_510" + }, + { + "name": "Anti Freezing Glove", + "tickets": 3000, + "drops": "Magma Caves", + "id": 45, + "description": "Freeze protection for 60 seconds, your speed decreases 20%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_431" + }, + { + "name": "Instant Strength Glove", + "tickets": 3000, + "drops": "Dark Cave", + "id": 14, + "description": "Increases your power attack 20% during 40 seconds, your defense decreases 20%", + "uses": 7, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_435" + }, + { + "name": "Epic Strength Glove", + "tickets": 2000, + "drops": "Asterion's Labyrinth", + "id": 15, + "description": "Increases your power attack 50% during 40 seconds, your defense decreases 30%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_441" + }, + { + "name": "Gravity Feather", + "tickets": 1000, + "drops": "Spider's Lair", + "id": 51, + "description": "Your gravity decreases 50% during 30 seconds, your defense decreases 20%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_754" + }, + { + "name": "Team Gravity Feather", + "tickets": 500, + "drops": "Cloudy Sky", + "id": 154, + "description": "For a duration of 40 seconds, the gravity affecting both you and your group members will decrease by 50%. Additionally, your attack with increase by 20%.", + "uses": 3, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_749" + }, + { + "name": "Hermes Boots", + "tickets": 4000, + "drops": "All", + "id": 6, + "description": "Increases your speed 50% for 40 seconds", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_133" + }, + { + "name": "Speed Boots", + "tickets": 3000, + "drops": "Ravanor's Land", + "id": 25, + "description": "Increases speed 20%, your defense decreases 20%", + "uses": 7, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_139" + }, + { + "name": "Maximum Speed Boots", + "tickets": 3000, + "drops": "Dragonblood", + "id": 26, + "description": "Increases speed 75% during 40 seconds, your defense decreases 30%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_140" + }, + { + "name": "Normal Invisibility Cloak", + "tickets": 3000, + "drops": "All", + "id": 4, + "description": "Makes you invisible for 20 seconds", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_235" + }, + { + "name": "Extreme Invisibility Cloak", + "tickets": 3000, + "drops": "All", + "id": 42, + "description": "Makes you invisible for 60 seconds, decreases your attack 20%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_264" + }, + { + "name": "Instant Defense Cloak", + "tickets": 3000, + "drops": "Sir Valkan's Stronghold", + "id": 20, + "description": "Increases your defense 50% during 40 seconds, your power attack decreases 20%", + "uses": 7, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_236" + }, + { + "name": "Epic Instant Defense Cloak", + "tickets": 3000, + "drops": "Enchanted Forest", + "id": 21, + "description": "Increases defense by 80%, attack decreases 40%.", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_262" + }, + { + "name": "Normal Health Regeneration Cloak", + "tickets": 3000, + "drops": "Sawmill", + "id": 30, + "description": "Regenerates health points 200% faster during 40", + "uses": 7, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_240" + }, + { + "name": "Maximum Health Regeneration Cloak", + "tickets": 2000, + "drops": "Asterion's Labyrinth", + "id": 31, + "description": "Regenerates health points 400% faster during 60 seconds, your power attack decreases 20%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_260" + }, + { + "name": "Normal Venom Protection Cloak", + "tickets": 3000, + "drops": "Enchanted Forest", + "id": 35, + "description": "Protects you from venom during 40 seconds", + "uses": 7, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_242" + }, + { + "name": "Extreme Venom Protection Cloak", + "tickets": 2000, + "drops": "Spider's Lair", + "id": 36, + "description": "Protects you from venom during 60 seconds, your power attack decreases 20%", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_261" + }, + { + "name": "Maximum Fire Protection Cloak", + "tickets": 2000, + "drops": "Magma Caves", + "id": 33, + "description": "Protects you from fire during 60 seconds.", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_268" + }, + { + "name": "Fire Protection Cloak", + "tickets": 3000, + "drops": "Dragonblood", + "id": 32, + "description": "It protects you from fire during 30 seconds.", + "uses": 7, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_249" + }, + { + "name": "Breath Helmet", + "tickets": 3000, + "drops": "Underwater Magma Caves", + "id": 74, + "description": "You can breathe underwater 4x the initial time (40 seconds total)", + "uses": 5, + "is_relic": false, + "max_in_inventory": null, + "emoji": "item_267" + }, + { + "name": "Lava Armor Protector", + "tickets": 2000, + "drops": "Sir Valkan's Stronghold", + "id": 48, + "description": "Protects you from lava damage during 60 seconds, decreases 20% of your velocity.", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_251" + }, + { + "name": "Anti Fire Enchantment", + "tickets": 100, + "drops": "All", + "id": 116, + "description": "Stops fire on people around you and yourself, not repairable", + "uses": 5, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_388" + }, + { + "name": "Epic Invicibility Potion", + "tickets": 5000, + "drops": "All", + "id": 39, + "description": "Makes you invincible (you don't take damage besides from debuffs) for 40 seconds", + "uses": 1, + "is_relic": false, + "max_in_inventory": null, + "emoji": "item_551" + }, + { + "name": "Firebreath's Blood", + "tickets": 2000, + "drops": "Firebreath's Lair", + "id": 115, + "description": "Invokes fire attack. Not repairable, one use.", + "uses": 1, + "is_relic": false, + "max_in_inventory": null, + "emoji": "item_745" + }, + { + "name": "Special Halloween Broom", + "tickets": null, + "drops": "Halloween World NPC", + "id": null, + "description": "You can use this broom to fly for 10 minutes. Linked to Halloween World. If you dismount or you leave Halloween world, the object will disappear.", + "uses": 1, + "is_relic": false, + "max_in_inventory": null, + "emoji": "item_761" + }, + { + "name": "Sleigh Enchantment", + "tickets": null, + "drops": "Santa's Gift", + "id": null, + "description": "This enchantment invokes a flying sleigh that you can use as a mount for 10 minutes. You cannot use this object inside a dungeon. Bound object.", + "uses": 1, + "is_relic": false, + "max_in_inventory": null, + "emoji": "item_395" + }, + { + "name": "Special Christmas Hat", + "tickets": null, + "drops": "Special Christmas Hat", + "id": null, + "description": "You can use this hat to fly with a sleigh like Santa for 10 minutes. This object is linked to Winter Wonderland. If you dismount or you leave Winterland, it will disappear.", + "uses": 1, + "is_relic": false, + "max_in_inventory": null, + "emoji": "item_764" + }, + { + "name": "Peppermint Strike", + "tickets": 1000, + "drops": "Frostbite Dungeon", + "id": 168, + "description": "Unleash the Peppermint Strike, a sweet and powerful attack that swirls through adversaries with the refreshing force of a candy cane breeze.", + "uses": 3, + "is_relic": false, + "max_in_inventory": 1, + "emoji": "item_801" + }, + { + "name": "Dice of Destiny", + "tickets": null, + "drops": null, + "id": 182, + "description": "Increases your luck 1%", + "uses": -1, + "is_relic": true, + "max_in_inventory": 16, + "emoji": "item_810" + }, + { + "name": "Iron Heart", + "tickets": null, + "drops": null, + "id": 183, + "description": "Increases max health 2%", + "uses": -1, + "is_relic": true, + "max_in_inventory": null, + "emoji": "item_811" + }, + { + "name": "Rejuvenation Gem", + "tickets": null, + "drops": null, + "id": 184, + "description": "Increases regeneration 2%", + "uses": -1, + "is_relic": true, + "max_in_inventory": null, + "emoji": "item_812" + }, + { + "name": "Inferno Touch", + "tickets": null, + "drops": null, + "id": 185, + "description": "1% chance of burning effect (PVP)", + "uses": -1, + "is_relic": true, + "max_in_inventory": null, + "emoji": "item_813" + }, + { + "name": "Greed's Grip", + "tickets": null, + "drops": null, + "id": 186, + "description": "Attract coins close to you", + "uses": -1, + "is_relic": true, + "max_in_inventory": null, + "emoji": "item_814" + }, + { + "name": "Dwarf's Strength", + "tickets": null, + "drops": null, + "id": 187, + "description": "Increases reach of Axes 8%", + "uses": -1, + "is_relic": true, + "max_in_inventory": 16, + "emoji": "item_815" + }, + { + "name": "Talisman of the Phoenix", + "tickets": null, + "drops": null, + "id": 161, + "description": null, + "uses": -1, + "is_relic": true, + "max_in_inventory": null, + "emoji": "item_798" + }, + { + "name": "Flying Skill", + "tickets": null, + "drops": null, + "id": 198, + "description": "Gives you the skill to dash vertically with your wings", + "uses": -1, + "is_relic": true, + "max_in_inventory": 1, + "emoji": "item_820" + }, + { + "name": "Dodge Charm", + "tickets": 100, + "drops": "All", + "id": 189, + "description": "1% chance of dodging any attack", + "uses": -1, + "is_relic": true, + "max_in_inventory": 1, + "emoji": "item_817" + }, + { + "name": "Gem Dust", + "tickets": 50, + "drops": "Stahmite Mines", + "id": 191, + "description": "Up to 5 dyams (+ Luck) per gem in chests", + "uses": -1, + "is_relic": true, + "max_in_inventory": 25, + "emoji": "item_819" + } + ] +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d4f585f --- /dev/null +++ b/src/utils.js @@ -0,0 +1,303 @@ +const {ChannelType, PermissionFlagsBits, User, Team, Emoji, ButtonBuilder, ButtonStyle, ActionRowBuilder, ComponentType} = require("discord.js"); +const level_emojis = require("./level_emojis.json"); + +const application_emoji_cache = new Map(); + +function restrict_text(str, nb){ + if (str.length > nb){ + str = str.substring(0, Math.max(0, nb - 3)) + "..."; + } + + return str; +} + +function format_score(score){ + score = score.toString(); + const number_digits = score.length; + let score_formated = score; + if (number_digits >= 7){ + score_formated = score.substring(0, number_digits - 6); + + if (number_digits >= 6 && score[number_digits - 6] != "0"){ + score_formated += "." + score[number_digits - 6]; + } + score_formated += "m"; + }else if (number_digits >= 4){ + score_formated = score.substring(0, number_digits - 3); + + if (number_digits >= 3 && score[number_digits - 3] != "0"){ + score_formated += "." + score[number_digits - 3]; + } + score_formated += "k"; + } + + return score_formated; +} + +function format_score_with_commas(score){ + const score_string = score.toString(); + let score_formated = ""; + for (let i = score_string.length - 1, count = 0; i >= 0; i--, count++){ + if (count && count % 3 === 0){ + score_formated = "," + score_formated; + } + score_formated = score_string[i] + score_formated; + } + + return score_formated; +} + +function format_speed_run_time(time){ + const total_milliseconds = Math.floor(time * 1000); + + const minutes = Math.floor(total_milliseconds / 60000); + const seconds = Math.floor((total_milliseconds % 60000) / 1000); + const milliseconds = total_milliseconds % 1000; + + const formatted_minutes = String(minutes).padStart(2, "0"); + const formatted_seconds = String(seconds).padStart(2, "0"); + const formatted_milliseconds = String(milliseconds).padStart(3, "0"); + + return `${formatted_minutes}:${formatted_seconds}.${formatted_milliseconds}`; +} + +function get_level(exp){ + return Math.floor(Math.pow(exp, 0.5) / 20); +} + +function get_level_adventurer(experience){ + return Math.min(20, Math.floor(Math.pow(experience / 100, 1 / 3)) + 1); +} + +function get_level_to_emojis(level){ + level = Math.floor(level); + + let emojis = ""; + + const landing = Math.floor(level / level_emojis.length); + for (let i = 0; i < landing; i++){ + emojis += application_emoji_cache.get(level_emojis.at(-1)).toString(); + } + + const rest = level % level_emojis.length; + if (rest){ + emojis += application_emoji_cache.get(level_emojis[rest - 1]).toString(); + } + + return emojis; +} + +function get_utc_date(){ + const today = new Date(); + return new Date(today.getTime() + (today.getTimezoneOffset() * 60000)); +} + +function get_utc_time_next_king(){ + const utc = get_utc_date(); + + const current_day = utc.getDay(); + const days_to_add = current_day == 0 ? 1 : (8 - current_day); + const next_monday = new Date(Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate() + days_to_add, 0, 0, 0)); + + return next_monday.getTime(); +} + +function sum(array){ + let result = 0; + for (const element of array){ + result += element; + } + return result; +} + +function get_first_chat_channel(guild, client){ + const everyone = guild.roles.everyone; + return guild.channels.cache.find((channel) => { + return channel.type == ChannelType.GuildText && + channel.permissionsFor(client.user).has(PermissionFlagsBits.SendMessages) && + channel.permissionsFor(everyone).has(PermissionFlagsBits.ViewChannel); + }); +} + +async function send_echo(client, message){ + const guilds = client.guilds.cache; + for (const guild of guilds.values()){ + const channel = get_first_chat_channel(guild, client); + + if (channel){ + try{ + await channel.send(message); + }catch(error){ + console.error(error); + } + } + } +} + +function is_my_developer(client, user){ + if (client.application.owner instanceof User){ + return user == client.application.owner; + }else if (client.application.owner instanceof Team){ + let is_member = false; + + const members_id = client.application.owner.members.keys(); + + for (const user_id of members_id){ + is_member = user_id == user.id; + + if (is_member){ + return true; + } + } + } + + return false; +} + +async function update_application_emoji_cache(application){ + const api_version = 10; + const headers = { + "Authorization": `Bot ${application.client.token}` + }; + + const response = await fetch(`https://discord.com/api/v${api_version}/applications/${application.id}/emojis`, {method: "GET", headers: headers}); + if (!response.ok){ + return; + } + + const data = await response.json(); + application_emoji_cache.clear(); + for (const item of data.items){ + const emoji = new Emoji(application.client, item); + application_emoji_cache.set(emoji.name, emoji); + } + + return application_emoji_cache; +} + +async function get_info_application(application){ + const api_version = 10; + const headers = { + "Authorization": `Bot ${application.client.token}` + }; + + const response = await fetch(`https://discord.com/api/v${api_version}/applications/${application.id}`, {method: "GET", headers: headers}); + if (!response.ok){ + return; + } + + const data = await response.json(); + return data; +} + +async function send_embed_layout(interaction, embed, pages, header_description = "", content_message){ + let current_page = 1; + + if (header_description){ + header_description += "\n"; + } + + embed.setDescription(header_description + (pages[current_page - 1] || "***There are no items to display on this page at the moment...***")); + + const nb_pages = pages.length || 1; + + embed.setFooter({text: `Page ${current_page}/${nb_pages}`}); + + const first_button = new ButtonBuilder(); + first_button.setCustomId("first"); + first_button.setEmoji("âĒ"); + first_button.setStyle(ButtonStyle.Primary); + first_button.setDisabled(current_page == 1); + + const previous_button = new ButtonBuilder(); + previous_button.setCustomId("previous"); + previous_button.setEmoji("â—€ī¸"); + previous_button.setStyle(ButtonStyle.Primary); + previous_button.setDisabled(current_page == 1); + + const next_button = new ButtonBuilder(); + next_button.setCustomId("next"); + next_button.setEmoji("â–ļī¸"); + next_button.setStyle(ButtonStyle.Primary); + next_button.setDisabled(current_page == nb_pages); + + const last_button = new ButtonBuilder(); + last_button.setCustomId("last"); + last_button.setEmoji("⏊"); + last_button.setStyle(ButtonStyle.Primary); + last_button.setDisabled(current_page == nb_pages); + + const row = new ActionRowBuilder(); + row.addComponents(first_button, previous_button, next_button, last_button); + + const response_interaction = await interaction.editReply({ + content: content_message, + embeds: [embed], + components: [row] + }); + + function collector_filter(m){ + const result = m.user.id == interaction.user.id || is_my_developer(m.user.client, m.user); + + if (!result){ + m.reply({content: "You cannot interact with a command that you did not initiate yourself.", ephemeral: true}).catch((error) => { + console.error(error); + }); + } + + return result; + } + + async function button_interaction_logic(response_interaction){ + try{ + const confirmation = await response_interaction.awaitMessageComponent({filter: collector_filter, componentType: ComponentType.Button, time: 60_000}); + + switch (confirmation.customId){ + case "first": + current_page = 1; + break; + case "previous": + current_page--; + break; + case "next": + current_page++; + break; + case "last": + current_page = nb_pages; + break; + } + + embed.setDescription(header_description + pages[current_page - 1]); + embed.setFooter({text: `Page ${current_page}/${nb_pages}`}); + first_button.setDisabled(current_page == 1); + previous_button.setDisabled(current_page == 1); + next_button.setDisabled(current_page == nb_pages); + last_button.setDisabled(current_page == nb_pages); + + response_interaction = await confirmation.update({embeds: [embed], components: [row]}); + await button_interaction_logic(response_interaction); + }catch (_error){ + await interaction.editReply({content: "-# ⓘ This interaction has timed out, please use the command again to be able to navigate from page to page.", components: []}); + } + } + + await button_interaction_logic(response_interaction); +} + +exports.restrict_text = restrict_text; +exports.format_score = format_score; +exports.format_score_with_commas = format_score_with_commas; +exports.format_speed_run_time = format_speed_run_time; +exports.get_level = get_level; +exports.get_level_adventurer = get_level_adventurer; +exports.get_level_to_emojis = get_level_to_emojis; +exports.get_utc_date = get_utc_date; +exports.get_utc_time_next_king = get_utc_time_next_king; +exports.sum = sum; +exports.send_echo = send_echo; +exports.get_first_chat_channel = get_first_chat_channel; +exports.is_my_developer = is_my_developer; +exports.update_application_emoji_cache = update_application_emoji_cache; +exports.send_embed_layout = send_embed_layout; +exports.get_info_application = get_info_application; +exports.application_emoji_cache = application_emoji_cache;