feat: link command basic functionaility

This commit is contained in:
TrèsAbhi committed 2023-10-10 20:33:24 -05:00
1 parent 97d427a6a8
commit 19298b9ad6
19 files changed
+225 -166

No files matched your search

+1 -1
View File
@@ -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
+1
View File
@@ -5,6 +5,7 @@
"bepaint",
"blitzstars",
"cleandist",
"cockroackdb",
"customperiod",
"deburr",
"diacriticless",
+3 -1
View File
@@ -8,5 +8,7 @@
"sklld_peasant_role": "896878529512370228",
"tres_guild_id": "734786591205359697",
"tres_log_channel": "1109262265850544148"
"tres_log_channel": "1109262265850544148",
"test_guild_id": "1124729294829142116"
}
+1 -1
View File
@@ -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"
}
}
+3 -1
View File
@@ -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"
}
}
@@ -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")
);
+3
View File
@@ -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"
+14
View File
@@ -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
}
+2 -2
View File
@@ -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);
+98
View File
@@ -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<AccountInfo>(
// `https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
// );
// const clanData = await getWargamingResponse<PlayerClanData>(
// `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,
};
-82
View File
@@ -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<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${id}`,
);
const clanData = await getWargamingResponse<PlayerClanData>(
`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,
};
+23
View File
@@ -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<null | { region: Region; blitz: number }>;
}
+7 -33
View File
@@ -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<CacheType>,
) {
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.`,
);
}
}
}
+29
View File
@@ -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));
}
+5 -1
View File
@@ -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
-40
View File
@@ -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')),
);
}
}
@@ -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<CacheType>,
@@ -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);
}
}
+1 -1
View File
@@ -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';
+24
View File
@@ -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/[email protected]":
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/[email protected]":
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"