feat: better subcommand organiztion

This commit is contained in:
TresAbhi committed 2023-08-04 02:13:21 +00:00
1 parent 2cabcc57f4
commit 23dd088466
15 files changed
+544 -662

No files matched your search

+1 -1
View File
@@ -29,4 +29,4 @@ jobs:
unzip dist.zip unzip dist.zip
- name: Run bot - name: Run bot
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.cjs run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.js
+3 -2
View File
@@ -1,6 +1,7 @@
{ {
"main": "server.cjs", "main": "server.js",
"type": "module",
"scripts": { "scripts": {
"start": "node --enable-source-maps --trace-warnings server.cjs" "start": "node --enable-source-maps --trace-warnings server.js"
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@
"@radix-ui/colors": "^0.1.8", "@radix-ui/colors": "^0.1.8",
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"discord.js": "^14.9.0", "discord.js": "^14.12.0",
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"express": "^4.18.2", "express": "^4.18.2",
+5 -4
View File
@@ -1,5 +1,5 @@
import { BuildOptions, build } from 'esbuild'; import { BuildOptions, build } from 'esbuild';
import { copyFile, mkdir, rm } from 'fs/promises'; import { copyFile, mkdirSync, rmSync } from 'fs';
import { argv } from 'process'; import { argv } from 'process';
const isProd = !argv.includes('--dev'); const isProd = !argv.includes('--dev');
@@ -10,15 +10,15 @@ const buildServer = argv.includes('--build=server') || buildAll;
if (isProd) { if (isProd) {
// only remove in production to ensure no time is wasted in dev // only remove in production to ensure no time is wasted in dev
console.log('Removing dist...'); console.log('Removing dist...');
await rm('dist', { recursive: true, force: true }); rmSync('dist', { recursive: true, force: true });
console.log('Dist removed'); console.log('Dist removed');
console.log('Creating dist...'); console.log('Creating dist...');
await mkdir('dist'); mkdirSync('dist');
console.log('Dist created'); console.log('Dist created');
console.log('Copying package.json...'); console.log('Copying package.json...');
copyFile('package.dist.json', 'dist/package.json').then(() => copyFile('package.dist.json', 'dist/package.json', () =>
console.log('package.json copied'), console.log('package.json copied'),
); );
} }
@@ -29,6 +29,7 @@ const commonOptions: BuildOptions = {
'.node': 'copy', '.node': 'copy',
'.ttf': 'file', '.ttf': 'file',
}, },
format: 'cjs',
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
+14 -8
View File
@@ -13,21 +13,25 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { secrets } from '../core/node/secrets'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import evolution from '../renderers/evolution'; import evolution from '../renderers/evolution';
import { StatType } from '../renderers/fullStats'; import { StatType } from '../renderers/fullStats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const evolutionCommand: CommandRegistry = { export const evolutionCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
);
resolve({
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: false,
inPublic: true, inPublic: true,
command: addStatTypeSubCommandGroups( command,
new SlashCommandBuilder()
.setName('evolution')
.setDescription('Evolution of statistics'),
),
async handler(interaction) { async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup( const commandGroup = interaction.options.getSubcommandGroup(
@@ -84,4 +88,6 @@ export const evolutionCommand: CommandRegistry = {
parseInt(url.searchParams.get('tankId')!), parseInt(url.searchParams.get('tankId')!),
); );
}, },
}; });
},
);
+14 -8
View File
@@ -13,20 +13,24 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { secrets } from '../core/node/secrets'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import fullStats, { StatType } from '../renderers/fullStats'; import fullStats, { StatType } from '../renderers/fullStats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const fullStatsCommand: CommandRegistry = { export const fullStatsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const command = await addStatTypeSubCommandGroups(
new SlashCommandBuilder()
.setName('fullstats')
.setDescription('Full in-game statistics'),
);
resolve({
inProduction: true, inProduction: true,
inDevelopment: false, inDevelopment: false,
inPublic: true, inPublic: true,
command: addStatTypeSubCommandGroups( command,
new SlashCommandBuilder()
.setName('fullstats')
.setDescription('Full in-game statistics'),
),
async handler(interaction) { async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup( const commandGroup = interaction.options.getSubcommandGroup(
@@ -81,4 +85,6 @@ export const fullStatsCommand: CommandRegistry = {
parseInt(url.searchParams.get('tankId')!), parseInt(url.searchParams.get('tankId')!),
); );
}, },
}; });
},
);
+1 -2
View File
@@ -121,7 +121,7 @@ export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
export const ratingsCommand: CommandRegistry = { export const ratingsCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true, inDevelopment: false,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -165,7 +165,6 @@ export const ratingsCommand: CommandRegistry = {
), ),
), ),
), ),
async handler(interaction) { async handler(interaction) {
const subcommandGroup = interaction.options.getSubcommandGroup(true); const subcommandGroup = interaction.options.getSubcommandGroup(true);
+13 -9
View File
@@ -13,21 +13,24 @@ import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { secrets } from '../core/node/secrets'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import { StatType } from '../renderers/fullStats'; import { StatType } from '../renderers/fullStats';
import stats from '../renderers/stats'; import stats from '../renderers/stats';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
export const statsCommand: CommandRegistry = { export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
inProduction: true, const command = await addStatTypeSubCommandGroups(
inDevelopment: false,
inPublic: true,
command: addStatTypeSubCommandGroups(
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('stats') .setName('stats')
.setDescription('Regular battles statistics'), .setDescription('Regular battles statistics'),
), );
resolve({
inProduction: true,
inDevelopment: true,
inPublic: true,
command,
async handler(interaction) { async handler(interaction) {
const commandGroup = interaction.options.getSubcommandGroup( const commandGroup = interaction.options.getSubcommandGroup(
@@ -82,4 +85,5 @@ export const statsCommand: CommandRegistry = {
parseInt(url.searchParams.get('tankId')!), parseInt(url.searchParams.get('tankId')!),
); );
}, },
}; });
});
+19
View File
@@ -1,3 +1,6 @@
import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse';
export interface TankopediaEntry { export interface TankopediaEntry {
name: string; name: string;
nation: string; nation: string;
@@ -32,6 +35,22 @@ export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
resolve((await entries).map(({ name }) => name)); resolve((await entries).map(({ name }) => name));
}); });
export interface TankopediaInfo {
achievement_sections: Record<string, { name: string; order: number }>;
tanks_updated_at: number;
languages: Record<string, string>;
vehicle_types: Record<string, string>;
vehicle_nations: Record<string, string>;
game_version: string;
}
// this is blocking because info is needed for command creation
console.log('Caching tankopedia info...');
export const tankopediaInfo = getWargamingResponse<TankopediaInfo>(
`https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}`,
);
console.log('Cached tankopedia info');
export const TANK_ICONS: Record<string, string> = { export const TANK_ICONS: Record<string, string> = {
'AT-SPG': 'https://i.imgur.com/BIHSEH0.png', 'AT-SPG': 'https://i.imgur.com/BIHSEH0.png',
lightTank: 'https://i.imgur.com/CSNha5V.png', lightTank: 'https://i.imgur.com/CSNha5V.png',
+2 -2
View File
@@ -4,13 +4,13 @@ import getTimeDaysAgo from './getTimeDaysAgo';
export default function getPeriodStart( export default function getPeriodStart(
server: Region, server: Region,
period: Exclude<Period, 'custom'>, period: Exclude<Period, 'customperiod'>,
) { ) {
if (period === 'career') { if (period === 'career') {
return -Infinity; return -Infinity;
} else if (period === 'today') { } else if (period === 'today') {
return getTimeDaysAgo(server, 1); return getTimeDaysAgo(server, 1);
} else { } else {
return getTimeDaysAgo(server, parseInt(period)); return getTimeDaysAgo(server, parseInt(period.match(/\d+/)?.[0]!));
} }
} }
+20 -15
View File
@@ -4,7 +4,7 @@ import {
} from 'discord.js'; } from 'discord.js';
import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand'; import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand';
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom'; export type Period = 'period' | 'customperiod';
export default function addPeriodSubCommands( export default function addPeriodSubCommands(
option: SlashCommandSubcommandGroupBuilder, option: SlashCommandSubcommandGroupBuilder,
@@ -14,25 +14,30 @@ export default function addPeriodSubCommands(
) { ) {
return option return option
.addSubcommand((option) => .addSubcommand((option) =>
extra(option.setName('today').setDescription(PERIOD_NAMES.today)), extra(
option
.setName('period' satisfies Period)
.setDescription(PERIOD_NAMES.period)
.addStringOption((option) =>
option
.setName('days')
.setDescription('A preset period')
.setChoices(
{ name: 'Today', value: '1' },
{ name: '30 days', value: '30' },
{ name: '60 days', value: '60' },
{ name: '90 days', value: '90' },
{ name: 'Career', value: 'Infinity' },
) )
.addSubcommand((option) => .setRequired(true),
extra(option.setName('30').setDescription(PERIOD_NAMES[30])), ),
) ),
.addSubcommand((option) =>
extra(option.setName('60').setDescription(PERIOD_NAMES[60])),
)
.addSubcommand((option) =>
extra(option.setName('90').setDescription(PERIOD_NAMES[90])),
)
.addSubcommand((option) =>
extra(option.setName('career').setDescription(PERIOD_NAMES.career)),
) )
.addSubcommand((option) => .addSubcommand((option) =>
extra( extra(
option option
.setName('custom') .setName('customperiod' satisfies Period)
.setDescription(PERIOD_NAMES.custom) .setDescription(PERIOD_NAMES.customperiod)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('start') .setName('start')
@@ -1,18 +1,21 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands'; import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices'; import addTankChoices from './addTankChoices';
import addUsernameChoices from './addUsernameChoices'; import addUsernameChoices from './addUsernameChoices';
export default function addStatTypeSubCommandGroups( export default async function addStatTypeSubCommandGroups(
option: SlashCommandBuilder, option: SlashCommandBuilder,
) { ) {
const awaitedTankopediaInfo = await tankopediaInfo;
return option return option
.addSubcommandGroup((option) => .addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) => addPeriodSubCommands(option, (option) =>
option.addStringOption(addUsernameChoices), option.addStringOption(addUsernameChoices),
) )
.setName('player') .setName('player')
.setDescription("Player's statistics"), .setDescription('Player statistics'),
) )
.addSubcommandGroup((option) => .addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) => addPeriodSubCommands(option, (option) =>
@@ -21,6 +24,58 @@ export default function addStatTypeSubCommandGroups(
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
) )
.setName('tank') .setName('tank')
.setDescription("Tank's statistics"), .setDescription('Tank statistics'),
)
.addSubcommandGroup((option) =>
addPeriodSubCommands(option, (option) =>
option
.addStringOption((option) => {
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
);
return option;
})
.addStringOption((option) =>
option
.setName('tanktype')
.setDescription('Type')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.addIntegerOption((option) =>
option
.setName('tier')
.setDescription('Tier')
.setMinValue(1)
.setMaxValue(10)
.setRequired(false),
)
.addStringOption((option) =>
option
.setName('treetype')
.setDescription('Tech tree type')
.addChoices(
{ name: 'Tech Tree', value: 'techtree' },
{ name: 'Premium', value: 'premium' },
{ name: 'Collector', value: 'collector' },
)
.setRequired(false),
),
)
.setName('tanks')
.setDescription('Multiple tanks'),
); );
} }
+5 -9
View File
@@ -7,20 +7,16 @@ import { Period } from '../discord/addPeriodSubCommands';
export const PERIOD_NAMES: Record<Period, string> = { export const PERIOD_NAMES: Record<Period, string> = {
today: "Today's statistics", today: "Today's statistics",
30: "30 days' statistics", period: "Set number of days' statistics",
60: "60 days' statistics",
90: "90 days' statistics",
career: 'Career statistics', career: 'Career statistics',
custom: 'Custom period', customperiod: 'Custom period',
}; };
export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = { export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = {
today: "Today's evolution", today: "Today's evolution",
30: "30 days' evolution", period: "Set number of days' evolution",
60: "60 days' evolution",
90: "90 days' evolution",
career: 'Career evolution', career: 'Career evolution',
custom: 'Custom period', customperiod: 'Custom period',
}; };
export interface ResolvedPeriod { export interface ResolvedPeriod {
@@ -40,7 +36,7 @@ export default function resolvePeriodFromCommand(
let end: number; let end: number;
const period = interaction.options.getSubcommand() as Period; const period = interaction.options.getSubcommand() as Period;
if (period === 'custom') { if (period === 'customperiod') {
const startRaw = interaction.options.getInteger('start')!; const startRaw = interaction.options.getInteger('start')!;
const endRaw = interaction.options.getInteger('end')!; const endRaw = interaction.options.getInteger('end')!;
const startMin = Math.min(startRaw, endRaw); const startMin = Math.min(startRaw, endRaw);
+26 -9
View File
@@ -54,22 +54,30 @@ export interface Registry {
inProduction: boolean; inProduction: boolean;
} }
export interface CommandRegistry<HandlesInteraction extends boolean = boolean> export type CommandRegistryRaw = Registry & {
extends Registry {
inPublic: boolean; inPublic: boolean;
inPreview?: boolean; inPreview?: boolean;
handlesInteraction?: HandlesInteraction;
command: command:
| SlashCommandBuilder | SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>; | Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
) => HandlesInteraction extends true ? void : InteractionReturnable;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void; autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable; button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
} & (
| {
handlesInteraction: true;
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
} }
| {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
}
);
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>;
const rest = new REST().setToken(secrets.DISCORD_TOKEN); const rest = new REST().setToken(secrets.DISCORD_TOKEN);
@@ -92,12 +100,19 @@ export const COMMANDS_RAW: CommandRegistry[] = [
statsCommand, statsCommand,
ratingsCommand, ratingsCommand,
]; ];
export const commands: Record<string, CommandRegistry> = COMMANDS_RAW.reduce(
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
rawCommands.reduce<Record<string, CommandRegistryRaw>>(
(accumulator, registry) => { (accumulator, registry) => {
if (isDev()) registry.command.setDefaultMemberPermissions(0); if (isDev()) registry.command.setDefaultMemberPermissions(0);
return { ...accumulator, [registry.command.name]: registry };
return {
...accumulator,
[registry.command.name]: registry,
};
}, },
{}, {},
),
); );
export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
@@ -105,7 +120,8 @@ export const guildCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
[]; [];
Object.entries(commands).forEach(([, registry]) => { commands.then((awaitedCommands) => {
Object.entries(awaitedCommands).forEach(([, registry]) => {
if (isDev() ? registry.inDevelopment : registry.inProduction) { if (isDev() ? registry.inDevelopment : registry.inProduction) {
const json = registry.command.toJSON(); const json = registry.command.toJSON();
@@ -146,6 +162,7 @@ try {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
});
export default async function interactionCreate( export default async function interactionCreate(
interaction: Interaction<CacheType>, interaction: Interaction<CacheType>,
+362 -589
View File
File diff suppressed because it is too large. Load diff