From 19298b9ad6c737b969e75956ed1c9702aa0c1edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:33:24 -0500 Subject: [PATCH] feat: link command basic functionaility --- .github/workflows/cd-host.yml | 2 +- .vscode/settings.json | 1 + discord.json | 4 +- package.dist.json | 2 +- package.json | 4 +- .../20231009152251_use_big_int/migration.sql | 8 ++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 14 +++ src/bot.ts | 4 +- src/commands/link.ts | 98 +++++++++++++++++++ src/commands/verify.ts | 82 ---------------- src/core/cockroackdb/discordBlitz.ts | 23 +++++ src/core/discord/resolvePlayerFromCommand.ts | 40 ++------ src/core/node/processes.ts | 29 ++++++ src/core/node/secrets.ts | 6 +- src/events/error.ts | 40 -------- .../handlers/chatInputCommand.ts | 5 +- src/events/interactionCreate/index.ts | 2 +- yarn.lock | 24 +++++ 19 files changed, 225 insertions(+), 166 deletions(-) create mode 100644 prisma/migrations/20231009152251_use_big_int/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/commands/link.ts delete mode 100644 src/commands/verify.ts create mode 100644 src/core/cockroackdb/discordBlitz.ts create mode 100644 src/core/node/processes.ts delete mode 100644 src/events/error.ts diff --git a/.github/workflows/cd-host.yml b/.github/workflows/cd-host.yml index bda094d6d..33834e705 100644 --- a/.github/workflows/cd-host.yml +++ b/.github/workflows/cd-host.yml @@ -29,4 +29,4 @@ jobs: unzip bot.zip - name: Run bot - run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps --trace-warnings index.cjs + run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps index.cjs diff --git a/.vscode/settings.json b/.vscode/settings.json index e63c24f60..30edbe1b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "bepaint", "blitzstars", "cleandist", + "cockroackdb", "customperiod", "deburr", "diacriticless", diff --git a/discord.json b/discord.json index d169bf4c9..d3ffa5749 100644 --- a/discord.json +++ b/discord.json @@ -8,5 +8,7 @@ "sklld_peasant_role": "896878529512370228", "tres_guild_id": "734786591205359697", - "tres_log_channel": "1109262265850544148" + "tres_log_channel": "1109262265850544148", + + "test_guild_id": "1124729294829142116" } diff --git a/package.dist.json b/package.dist.json index a4aa082e6..9e205ed74 100644 --- a/package.dist.json +++ b/package.dist.json @@ -2,6 +2,6 @@ "main": "server.js", "type": "module", "scripts": { - "start": "node --enable-source-maps --trace-warnings server.cjs" + "start": "node --enable-source-maps server.cjs" } } diff --git a/package.json b/package.json index 49a527f52..97c151197 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dev:bot": "yarn build:bot && yarn start:bot", "dev:docs": "vitepress dev docs", "dev:website": "next dev", - "start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs", + "start:bot": "node --enable-source-maps dist/bot/index.cjs", "start:website": "next start", "host:docs": "vitepress preview docs", "publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish", @@ -26,6 +26,7 @@ "license": "MIT", "dependencies": { "@octokit/rest": "^20.0.1", + "@prisma/client": "^5.4.1", "@radix-ui/colors": "^3.0.0-rc.4", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^1.1.2", @@ -62,6 +63,7 @@ "compare-versions": "^6.1.0", "knip": "^2.27.1", "prettier": "^3.0.3", + "prisma": "^5.4.1", "typescript": "^5.2.2" } } diff --git a/prisma/migrations/20231009152251_use_big_int/migration.sql b/prisma/migrations/20231009152251_use_big_int/migration.sql new file mode 100644 index 000000000..2fdbde59a --- /dev/null +++ b/prisma/migrations/20231009152251_use_big_int/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "discord_blitz" ( + "discord" INT8 NOT NULL, + "region" STRING NOT NULL, + "blitz" INT4 NOT NULL, + + CONSTRAINT "discord_blitz_pkey" PRIMARY KEY ("discord") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..7106c8a84 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "cockroachdb" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 000000000..20719dbc2 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,14 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "cockroachdb" + url = env("DISCORD_BLITZ_DB") +} + +model discord_blitz { + discord BigInt @id + region String + blitz Int +} diff --git a/src/bot.ts b/src/bot.ts index 55f661aa1..4506ce218 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,6 +1,6 @@ import { client } from './core/discord/client'; +import { registerProcesses } from './core/node/processes'; import { secrets } from './core/node/secrets'; -import { registerErrorHandlers } from './events/error'; -registerErrorHandlers(); +registerProcesses(); client.login(secrets.DISCORD_TOKEN); diff --git a/src/commands/link.ts b/src/commands/link.ts new file mode 100644 index 000000000..e1ca47b5e --- /dev/null +++ b/src/commands/link.ts @@ -0,0 +1,98 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { Region } from '../constants/regions'; +import { linkBlitzAndDiscord } from '../core/cockroackdb/discordBlitz'; +import addUsernameChoices from '../core/discord/addUsernameChoices'; +import autocompleteUsername from '../core/discord/autocompleteUsername'; +import embedNegative from '../core/discord/embedNegative'; +import { CommandRegistry } from '../events/interactionCreate'; + +const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; + +export const verifyCommand: CommandRegistry = { + inProduction: true, + inPublic: true, + + command: new SlashCommandBuilder() + .setName('link') + .setDescription('Links your Blitz and Discord account') + .addStringOption((option) => addUsernameChoices(option).setRequired(true)), + + async handler(interaction) { + // const blitzAccount = await resolvePlayerFromCommand(interaction); + // const { id, region: server } = blitzAccount; + // const accountInfo = await getWargamingResponse( + // `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, + // ); + // const clanData = await getWargamingResponse( + // `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, + // ); + // const clan = clanData[id]?.clan; + // const clanTag = clan ? ` [${clan!.tag}]` : ''; + // const member = + // interaction.member && + // interaction.guild?.members.cache.get(interaction.member?.user.id); + + // if (!interaction.guild?.members.me?.permissions.has('ManageNicknames')) { + // return embedNegative( + // `${markdownEscape(interaction.user.username)} failed to verify`, + // "I don't have the permission to change your nickname.", + // ); + // } + // if (!interaction.member || !member) { + // return embedNegative( + // `${interaction.user.username} is not in this server`, + // "I couldn't find this person in this server. Did they leave?", + // ); + // } + + // try { + // await member.setNickname(`${accountInfo[id].nickname}${clanTag}`); + + // if (interaction.guildId === discord.sklld_guild_id) { + // if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { + // return embedNegative( + // `${markdownEscape(interaction.user.username)} failed to verify`, + // "I don't have the permission to change your manage roles.", + // ); + // } + + // await member.roles.remove(discord.sklld_verify_role); + // await member.roles.add(discord.sklld_peasant_role); + // } + + // return embedPositive( + // `${interaction.user.username} is verified`, + // `The user is now verified as ${markdownEscape( + // accountInfo[id].nickname, + // )}${markdownEscape(clanTag)}`, + // ); + // } catch (error) { + // return embedNegative( + // `${interaction.user.username} failed to verify`, + // "I can't change your nickname because you have higher permissions than me. Try setting your nickname to your Blitz username manually.", + // ); + // } + + const username = interaction.options.getString('username', true); + + if (serverAndIdPattern.test(username)) { + const [region, idString] = username.split('/'); + const id = parseInt(idString); + const discordId = parseInt(interaction.user.id); + + await linkBlitzAndDiscord(discordId, region as Region, id); + + return 'success'; + } else { + return [ + embedNegative( + 'Please select from the options', + 'Click on the user you desire after the search results show up (like the image below).', + ), + 'https://i.imgur.com/ncJyV1F.png', + ]; + } + }, + + autocomplete: autocompleteUsername, +}; diff --git a/src/commands/verify.ts b/src/commands/verify.ts deleted file mode 100644 index 70524c432..000000000 --- a/src/commands/verify.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import markdownEscape from 'markdown-escape'; -import discord from '../../discord.json' assert { type: 'json' }; -import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; -import getWargamingResponse from '../core/blitz/getWargamingResponse'; -import addUsernameChoices from '../core/discord/addUsernameChoices'; -import autocompleteUsername from '../core/discord/autocompleteUsername'; -import embedNegative from '../core/discord/embedNegative'; -import embedPositive from '../core/discord/embedPositive'; -import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; -import { CommandRegistry } from '../events/interactionCreate'; -import { AccountInfo } from '../types/accountInfo'; -import { PlayerClanData } from '../types/playerClanData'; - -export const verifyCommand: CommandRegistry = { - inProduction: true, - inPublic: true, - - command: new SlashCommandBuilder() - .setName('verify') - .setDescription("Set's the user's username to their in-game name") - .addStringOption((option) => addUsernameChoices(option).setRequired(true)), - - async handler(interaction) { - const blitzAccount = await resolvePlayerFromCommand(interaction); - const { id, region: server } = blitzAccount; - const accountInfo = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`, - ); - const clanData = await getWargamingResponse( - `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, - ); - const clan = clanData[id]?.clan; - const clanTag = clan ? ` [${clan!.tag}]` : ''; - const member = - interaction.member && - interaction.guild?.members.cache.get(interaction.member?.user.id); - - if (!interaction.guild?.members.me?.permissions.has('ManageNicknames')) { - return embedNegative( - `${markdownEscape(interaction.user.username)} failed to verify`, - "I don't have the permission to change your nickname.", - ); - } - if (!interaction.member || !member) { - return embedNegative( - `${interaction.user.username} is not in this server`, - "I couldn't find this person in this server. Did they leave?", - ); - } - - try { - await member.setNickname(`${accountInfo[id].nickname}${clanTag}`); - - if (interaction.guildId === discord.sklld_guild_id) { - if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { - return embedNegative( - `${markdownEscape(interaction.user.username)} failed to verify`, - "I don't have the permission to change your manage roles.", - ); - } - - await member.roles.remove(discord.sklld_verify_role); - await member.roles.add(discord.sklld_peasant_role); - } - - return embedPositive( - `${interaction.user.username} is verified`, - `The user is now verified as ${markdownEscape( - accountInfo[id].nickname, - )}${markdownEscape(clanTag)}`, - ); - } catch (error) { - return embedNegative( - `${interaction.user.username} failed to verify`, - "I can't change your nickname because you have higher permissions than me. Try setting your nickname to your Blitz username manually.", - ); - } - }, - - autocomplete: autocompleteUsername, -}; diff --git a/src/core/cockroackdb/discordBlitz.ts b/src/core/cockroackdb/discordBlitz.ts new file mode 100644 index 000000000..fd644e944 --- /dev/null +++ b/src/core/cockroackdb/discordBlitz.ts @@ -0,0 +1,23 @@ +import { PrismaClient } from '@prisma/client'; +import { Region } from '../../constants/regions'; + +export const discordBlitzDB = new PrismaClient(); + +export function linkBlitzAndDiscord( + discord: number, + region: Region, + blitz: number, +) { + return discordBlitzDB.discord_blitz.upsert({ + where: { discord }, + update: { region, blitz }, + create: { discord, region, blitz }, + }); +} + +export function getBlitzFromDiscord(discord: number) { + return discordBlitzDB.discord_blitz.findUnique({ + where: { discord }, + select: { region: true, blitz: true }, + }) as Promise; +} diff --git a/src/core/discord/resolvePlayerFromCommand.ts b/src/core/discord/resolvePlayerFromCommand.ts index 38e4efa2f..4958bee03 100644 --- a/src/core/discord/resolvePlayerFromCommand.ts +++ b/src/core/discord/resolvePlayerFromCommand.ts @@ -1,8 +1,6 @@ import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { Region } from '../../constants/regions'; -import listPlayers, { - usernamePatternWithoutPosition, -} from '../blitz/listPlayers'; +import { getBlitzFromDiscord } from '../cockroackdb/discordBlitz'; import throwError from '../node/throwError'; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; @@ -15,38 +13,14 @@ export interface ResolvedPlayer { export default async function resolvePlayerFromCommand( interaction: ChatInputCommandInteraction, ) { - const commandUsername = interaction.options.getString('username'); - const displayName = interaction.guild?.members.cache - .get(interaction.user.id) - ?.displayName.match(usernamePatternWithoutPosition)?.[0]; - const username = commandUsername ?? displayName; + const account = await getBlitzFromDiscord(parseInt(interaction.user.id)); - if (username === undefined) { + if (account) { + return { region: account.region, id: account.blitz }; + } else { throw throwError( - 'Username could not be inferred', - "You didn't provide me a username in the command nor was I able to infer your Blitz username from your Discord nickname. Try providing a username manually in the command or using the `/verify` command to set your Discord nickname to your Blitz username.", + "You're not linked", + 'Use the `/link` command to link your Discord and Blitz accounts.', ); } - - if (serverAndIdPattern.test(username)) { - const [server, accountId] = username.split('/'); - return { - region: server as Region, - id: Number(accountId), - } satisfies ResolvedPlayer; - } else { - const accounts = await listPlayers(username); - - if (accounts[0]) { - return { - region: accounts[0].region, - id: accounts[0].account_id, - } satisfies ResolvedPlayer; - } else { - throw throwError( - 'Could not find user', - `I couldn't find user \`${username}\`. Try providing a valid username manually or using the \`/verify\` command to set your Discord nickname to your Blitz username.`, - ); - } - } } diff --git a/src/core/node/processes.ts b/src/core/node/processes.ts new file mode 100644 index 000000000..f7fee3548 --- /dev/null +++ b/src/core/node/processes.ts @@ -0,0 +1,29 @@ +import { discordBlitzDB } from '../cockroackdb/discordBlitz'; +import { client } from '../discord/client'; + +const EXIT_EVENTS = [ + 'beforeExit', + 'exit', + 'SIGINT', + 'SIGTERM', + 'SIGUSR1', + 'SIGUSR2', +]; + +async function cleanup() { + console.log('Cleaning up...'); + + await Promise.all([discordBlitzDB.$disconnect(), client.destroy()]); + + console.log('Gracefully exiting...'); + + EXIT_EVENTS.forEach((event) => process.off(event, cleanup)); + process.exit(0); +} + +export function registerProcesses() { + process.on('uncaughtException', console.error); + client.on('error', console.error); + + EXIT_EVENTS.forEach((event) => process.on(event, cleanup)); +} diff --git a/src/core/node/secrets.ts b/src/core/node/secrets.ts index 9836fec85..264382263 100644 --- a/src/core/node/secrets.ts +++ b/src/core/node/secrets.ts @@ -2,7 +2,11 @@ import { config } from 'dotenv'; config(); -export const SECRET_KEYS = ['DISCORD_TOKEN', 'GH_TOKEN'] as const; +export const SECRET_KEYS = [ + 'DISCORD_TOKEN', + 'GH_TOKEN', + 'DISCORD_BLITZ_DB', +] as const; export type SECRET = typeof SECRET_KEYS extends readonly (infer T)[] ? T diff --git a/src/events/error.ts b/src/events/error.ts deleted file mode 100644 index 537e06b29..000000000 --- a/src/events/error.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { EmbedBuilder, TextChannel } from 'discord.js'; -import discord from '../../discord.json' assert { type: 'json' }; -import { NEGATIVE_COLOR } from '../constants/colors'; -import { client } from '../core/discord/client'; -import isDev from '../core/node/isDev'; - -const PROCESS_ERROR_EVENTS = ['uncaughtException']; -const CLIENT_ERROR_EVENTS = ['error']; - -export function handleError(error: Error, command: string) { - console.error(error); - - ( - client.guilds.cache - .get(discord.tres_guild_id) - ?.channels.cache.get(discord.tres_log_channel) as TextChannel - ).send({ - embeds: [ - new EmbedBuilder() - .setTitle(`${client.user?.username} ran into an error`) - .setColor(NEGATIVE_COLOR) - .setDescription( - `\`\`\`${[command, error.name, error.message, error.stack] - .filter((item) => item !== undefined) - .join('\n\n')}\`\`\``, - ), - ], - }); -} - -export function registerErrorHandlers() { - if (!isDev()) { - PROCESS_ERROR_EVENTS.forEach((name) => - process.on(name, (error) => handleError(error, 'process error')), - ); - CLIENT_ERROR_EVENTS.forEach((name) => - client.on(name, (error) => handleError(error, 'client error')), - ); - } -} diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index e603b4048..985ed45c8 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -11,7 +11,6 @@ import embedNegative from '../../../core/discord/embedNegative'; import embedWarning from '../../../core/discord/embedWarning'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import { psa } from '../../../core/discord/psa'; -import { handleError } from '../../error'; export default async function handleChatInputCommand( interaction: ChatInputCommandInteraction, @@ -55,6 +54,8 @@ export default async function handleChatInputCommand( .setStyle(ButtonStyle.Link), ); + console.error(interaction.commandName, error); + await interaction.editReply({ embeds: [ embedNegative( @@ -64,7 +65,5 @@ export default async function handleChatInputCommand( ], components: [actionRow], }); - - handleError(error as Error, interaction.commandName); } } diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index db65bf71d..4a7389abf 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -30,7 +30,7 @@ import { searchPlayersCommand } from '../../commands/searchPlayers'; import { searchTanksCommand } from '../../commands/searchTanks'; import { statsCommand } from '../../commands/stats'; import { todayCommand } from '../../commands/today'; -import { verifyCommand } from '../../commands/verify'; +import { verifyCommand } from '../../commands/link'; import getClientId from '../../core/node/getClientId'; import isDev from '../../core/node/isDev'; import { secrets } from '../../core/node/secrets'; diff --git a/yarn.lock b/yarn.lock index d85a8246c..a5eb64006 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1060,6 +1060,23 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@prisma/client@^5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.4.1.tgz#f891d1bc2739d303afaf4406b673f57bf84b226c" + integrity sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ== + dependencies: + "@prisma/engines-version" "5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f" + +"@prisma/engines-version@5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f": + version "5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f.tgz#347534906f2a9d6fcf02aeeb911103c5ebfe4384" + integrity sha512-+nUQM/y8C+1GG5Ioeqcu6itFslCfxvQSAUVSMC9XM2G2Fcq0F4Afnp6m0pXF6X6iUBWen7jZBPmM9Qlq4Nr3/A== + +"@prisma/engines@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.4.1.tgz#80b854c87ba30d02ce8df1578cc955ac1c97216a" + integrity sha512-vJTdY4la/5V3N7SFvWRmSMUh4mIQnyb/MNoDjzVbh9iLmEC+uEykj/1GPviVsorvfz7DbYSQC4RiwmlEpTEvGA== + "@radix-ui/colors@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-2.1.0.tgz#8aa6f233f68076a92c57db93d7235172e88fda44" @@ -3636,6 +3653,13 @@ pretty-ms@^8.0.0: dependencies: parse-ms "^3.0.0" +prisma@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.4.1.tgz#2abbd2ae446519654330d67acd81e7bafbac67a2" + integrity sha512-op9PmU8Bcw5dNAas82wBYTG0yHnpq9/O3bhxbDBrNzwZTwBqsVCxxYRLf6wHNh9HVaDGhgjjHlu1+BcW8qdnBg== + dependencies: + "@prisma/engines" "5.4.1" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"