Compare commits

...
12 Commits
29 changed files with 338 additions and 221 deletions

No files matched your search

+1
View File
@@ -39,6 +39,7 @@
"ownedtanks", "ownedtanks",
"playerachievements", "playerachievements",
"playerinfo", "playerinfo",
"Promisable",
"ratingsleaderboard", "ratingsleaderboard",
"ratingsplot", "ratingsplot",
"researchable", "researchable",
+23
View File
@@ -0,0 +1,23 @@
# Blitzkrieg 1.15.10
The 10th patch for the `1.15` update is here, breaking the record of the most patches for a single update!
## Features
- Added `/today`
- This is a direct alias for `/breakdown today`
- Drastically simplified ratings command
- `/ratings current` and `/ratings archive` have been now merged
- Where the data is fetched from is now decided directly from the chosen season number
- Simplified many error messages
- Title bar description text is now centered when there is no image
- It always felt awkward when there was no image on the left but the left still aligned to the right
- Removed `/link`'s requirement to select a username explicitly from the search results
- Full names for regions are now used in the `/link` command
- So "NA" turns into "North America"
## Fixes
- Fixed contrast issues with WN8 percentile colors behind white text
- Archived ratings neighbors now functions once again
- Disabled tankopedia; this feature isn't finished yet and the changes were accidentally leaked
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.15.9", "version": "1.15.10",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
+7 -7
View File
@@ -13,7 +13,7 @@ import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import { tankAverages } from '../core/blitzstars/tankAverages'; import { tankAverages } from '../core/blitzstars/tankAverages';
import addFilterOptions from '../core/discord/addFilterOptions'; import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -33,12 +33,12 @@ import resolvePlayerFromCommand, {
import calculateWN8 from '../core/statistics/calculateWN8'; import calculateWN8 from '../core/statistics/calculateWN8';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import getWN8Percentile from '../core/statistics/getWN8Percentile'; import getWN8Percentile from '../core/statistics/getWN8Percentile';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryPromisable } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8; const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10; const MAX_PAGES = 10;
async function render( export async function renderBreakdown(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
filters: StatFilters, filters: StatFilters,
@@ -290,9 +290,9 @@ async function render(
} }
} }
export const breakdownCommand = new Promise<CommandRegistryRaw>( export const breakdownCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => { async (resolve) => {
const command = await addFilterOptions( const command = await addPeriodicFilterOptions(
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('breakdown') .setName('breakdown')
.setDescription("A period's breakdown by tanks played"), .setDescription("A period's breakdown by tanks played"),
@@ -316,7 +316,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
}); });
return [ return [
...(await render(player, period, filters)), ...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'), buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id), await getBlitzStarsLinkButton(player.region, player.id),
]; ];
@@ -332,7 +332,7 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
const period = resolvePeriodFromButton(player.region, interaction); const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction); const filters = getFiltersFromButton(interaction);
return await render(player, period, filters); return await renderBreakdown(player, period, filters);
}, },
}); });
}, },
+1 -1
View File
@@ -13,7 +13,7 @@ export const debugCommand: CommandRegistry = {
.setName('debug') .setName('debug')
.setDescription('Debug information about the bot'), .setDescription('Debug information about the bot'),
async handler(interaction) { async handler() {
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const uptime = currentTime - executionStart; const uptime = currentTime - executionStart;
const list = [ const list = [
+2 -2
View File
@@ -29,7 +29,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand, { import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryPromisable } from '../events/interactionCreate';
type EvolutionStatType = 'player' | 'tank'; type EvolutionStatType = 'player' | 'tank';
@@ -107,7 +107,7 @@ async function render(
); );
} }
export const evolutionCommand = new Promise<CommandRegistryRaw>( export const evolutionCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => { async (resolve) => {
let playerGroup: SlashCommandSubcommandGroupBuilder; let playerGroup: SlashCommandSubcommandGroupBuilder;
let tankGroup: SlashCommandSubcommandGroupBuilder; let tankGroup: SlashCommandSubcommandGroupBuilder;
+4 -4
View File
@@ -10,7 +10,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import addFilterOptions from '../core/discord/addFilterOptions'; import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -29,7 +29,7 @@ import resolvePlayerFromCommand, {
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { getTierWeights } from '../core/statistics/getTierWeights'; import { getTierWeights } from '../core/statistics/getTierWeights';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryPromisable } from '../events/interactionCreate';
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
@@ -64,9 +64,9 @@ async function render(
); );
} }
export const fullStatsCommand = new Promise<CommandRegistryRaw>( export const fullStatsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => { async (resolve) => {
const command = await addFilterOptions( const command = await addPeriodicFilterOptions(
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('full-stats') .setName('full-stats')
.setDescription('Full in-game statistics'), .setDescription('Full in-game statistics'),
+15 -29
View File
@@ -1,25 +1,21 @@
import { teal } from '@radix-ui/colors'; import { teal } from '@radix-ui/colors';
import { import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js';
AttachmentBuilder,
GuildMemberRoleManager,
SlashCommandBuilder,
} from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' }; import discord from '../../discord.json' assert { type: 'json' };
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow'; import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_NAMES_SHORT, Region } from '../constants/regions'; import { REGION_NAMES } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz'; import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedInfo from '../core/discord/embedInfo';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export const verifyCommand: CommandRegistry = { export const verifyCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
@@ -30,18 +26,12 @@ export const verifyCommand: CommandRegistry = {
.addStringOption((option) => addUsernameChoices(option).setRequired(true)), .addStringOption((option) => addUsernameChoices(option).setRequired(true)),
async handler(interaction) { async handler(interaction) {
const username = interaction.options.getString('username', true); const { id, region } = await resolvePlayerFromCommand(interaction);
if (serverAndIdPattern.test(username)) {
const [region, idString] = username.split('/');
const id = parseInt(idString);
const discordId = parseInt(interaction.user.id); const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region as Region, id); const accountInfo = await getAccountInfo(region, id);
const clanAccountInfo = await getClanAccountInfo(region as Region, id, [ const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']);
'clan',
]);
await linkBlitzAndDiscord(discordId, region as Region, id); await linkBlitzAndDiscord(discordId, region, id);
if (interaction.guildId === discord.sklld_guild_id) { if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
@@ -59,7 +49,7 @@ export const verifyCommand: CommandRegistry = {
); );
} }
return ( return [
<Wrapper fat> <Wrapper fat>
<div <div
style={{ style={{
@@ -198,7 +188,7 @@ export const verifyCommand: CommandRegistry = {
{clanAccountInfo?.clan {clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] - ` ? `[${clanAccountInfo?.clan?.tag}] - `
: ''} : ''}
{REGION_NAMES_SHORT[region as Region]} {REGION_NAMES[region]}
</span> </span>
</div> </div>
</div> </div>
@@ -206,17 +196,13 @@ export const verifyCommand: CommandRegistry = {
<Glow color={teal.teal9} rotation={-90} /> <Glow color={teal.teal9} rotation={-90} />
</div> </div>
</Wrapper> </Wrapper>,
);
} else { embedInfo(
return [ 'Incorrect account?',
embedNegative( 'Use the command again and select a username from the search results.',
'Please select from the options',
'Follow the procedure like the video above. The search results may take a while to show up.',
), ),
new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif'),
]; ];
}
}, },
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
+70 -98
View File
@@ -2,7 +2,6 @@ import {
APIApplicationCommandOptionChoice, APIApplicationCommandOptionChoice,
SlashCommandBuilder, SlashCommandBuilder,
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { range } from 'lodash'; import { range } from 'lodash';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
@@ -17,17 +16,17 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo';
import { getRatingsLeague } from '../core/blitz/getRatingsLeague'; import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors'; import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber'; import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard'; import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard';
import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard'; import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard';
import { UserError } from '../core/blitzkrieg/userError'; import { UserError } from '../core/blitzkrieg/userError';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryPromisable } from '../events/interactionCreate';
export interface RatingsPlayer { export interface RatingsPlayer {
spa_id: number; spa_id: number;
@@ -116,18 +115,22 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[]; neighbors: RatingsPlayer[];
} }
function addSubcommands( const noOngoingSeason = embedNegative(
option: SlashCommandSubcommandGroupBuilder, 'No ongoing season',
addOptions = (option: SlashCommandSubcommandBuilder) => option, "Wargaming didn't provide any data for this season.",
) { );
export const ratingsCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const onGoingSeason = await isOnGoingRatingsSeason();
const seasonNumbers = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addSubcommands(option: SlashCommandSubcommandBuilder) {
return option return option
.addSubcommand((option) =>
addOptions(
option
.setName('neighbors')
.setDescription('Your position and neighbors'),
)
.addStringOption(addUsernameChoices)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName('limit')
@@ -135,10 +138,42 @@ function addSubcommands(
.setRequired(false) .setRequired(false)
.setMinValue(5) .setMinValue(5)
.setMaxValue(30), .setMaxValue(30),
)
.addStringOption((option) =>
option
.setName('season')
.setDescription('Season number')
.setRequired(false)
.addChoices(
...seasonNumbers
.map(
(number, index) =>
({
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
value: `${number}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
)
.reverse(),
),
);
}
const command = new SlashCommandBuilder()
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommand((option) =>
addSubcommands(
option
.setName('neighbors')
.setDescription('Shows neighboring players on the leaderboard'),
), ),
) )
.addSubcommand((option) => .addSubcommand((option) =>
addOptions(option.setName('league').setDescription('Top league players')) addSubcommands(
option
.setName('league')
.setDescription('Shows top players from a league')
.addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('league') .setName('league')
@@ -153,59 +188,7 @@ function addSubcommands(
), ),
) )
.setRequired(true), .setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
), ),
);
}
const noOngoingSeason = embedNegative(
'No ongoing season',
"Wargaming didn't provide any data for this season.",
);
export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
const seasonChoices = range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + 1,
).map(
(number) =>
({
name: `Season ${number}`,
value: `${number}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
const command = new SlashCommandBuilder()
.setName('ratings')
.setDescription('Ratings battles statistics')
.addSubcommandGroup((option) =>
addSubcommands(
option.setName('archive').setDescription('Any previous season'),
(option) =>
option.addStringOption((option) =>
option
.setName('season')
.setDescription('The season number')
.setChoices(...seasonChoices)
.setRequired(true),
),
),
)
.addSubcommandGroup((option) =>
addSubcommands(
option
.setName('current')
.setDescription('The current running Ratings season'),
), ),
); );
@@ -224,18 +207,19 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
nickname: string; nickname: string;
} }
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| 'current';
const subcommand = interaction.options.getSubcommand(true) as const subcommand = interaction.options.getSubcommand(true) as
| 'league' | 'league'
| 'neighbors'; | 'neighbors';
const limit = interaction.options.getInteger('limit') ?? 10; const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season'); const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined; const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
? latestArchivedSeasonNumber + 1
: latestArchivedSeasonNumber;
const isCurrentSeason = season === latestArchivedSeasonNumber + 1;
let titleName: string; let titleName: string;
let titleImage: string | undefined; let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined;
let titleDescription: string; let titleDescription: string;
let playersBefore: number; let playersBefore: number;
let playersAfter: number; let playersAfter: number;
@@ -249,16 +233,14 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
interaction.options.getString('league')!, interaction.options.getString('league')!,
); );
const region = interaction.options.getString('region') as Region; const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await (subcommandGroup === 'current' regionRatingsInfo = await (isCurrentSeason
? getRatingsInfo(region) ? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season!)); : getArchivedRatingsInfo(region, season!));
if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; if (regionRatingsInfo.detail !== undefined) return noOngoingSeason;
const result = const result = isCurrentSeason
subcommandGroup === 'current' ? await getRatingsLeague(region, leagueIndex).then(async (result) =>
? await getRatingsLeague(region, leagueIndex).then(
async (result) =>
result.slice(0, limit).map( result.slice(0, limit).map(
(player) => (player) =>
({ ({
@@ -305,8 +287,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
}, },
); );
const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; const leagueInfo = regionRatingsInfo.leagues[leagueIndex];
midnightLeaderboard = midnightLeaderboard = isCurrentSeason
subcommandGroup === 'current'
? await getArchivedRatingsMidnightLeaderboard( ? await getArchivedRatingsMidnightLeaderboard(
region, region,
regionRatingsInfo.current_season, regionRatingsInfo.current_season,
@@ -326,8 +307,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
titleDescription = `Top ${limit} players`; titleDescription = `Top ${limit} players`;
} else { } else {
const { region, id } = await resolvePlayerFromCommand(interaction); const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = regionRatingsInfo = isCurrentSeason
subcommandGroup === 'current'
? await getRatingsInfo(region) ? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!); : await getArchivedRatingsInfo(region, season!);
@@ -335,13 +315,9 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = const neighbors = isCurrentSeason
subcommandGroup === 'current' ? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then(
? await getRatingsNeighbors( (players) =>
region,
id,
Math.round(limit / 2),
).then((players) =>
players.neighbors.map( players.neighbors.map(
(player) => (player) =>
({ ({
@@ -361,11 +337,8 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
if (playerIndex === -1) { if (playerIndex === -1) {
throw new UserError( throw new UserError(
`${accountInfo.nickname} didn't player ratings in season ${season}`, `${accountInfo.nickname} didn't play ratings in season ${season}`,
{
cause:
'This player did not participate in this season or did not get past calibration.', 'This player did not participate in this season or did not get past calibration.',
},
); );
} }
@@ -384,21 +357,20 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
id: player.id, id: player.id,
score: player.score, score: player.score,
position: playerIndex + index + 1, position: playerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag, clan: clanData[index]?.clan?.tag,
nickname: clanData[player.id]!.account_name, nickname: clanData[index]
? clanData[index]!.account_name
: `Deleted ${player.id}`,
})); }));
}, },
); );
midnightLeaderboard = midnightLeaderboard = isCurrentSeason
subcommandGroup === 'current'
? await getArchivedRatingsMidnightLeaderboard( ? await getArchivedRatingsMidnightLeaderboard(
region, region,
regionRatingsInfo.current_season, regionRatingsInfo.current_season,
) )
: await getArchivedRatingsMidnightLeaderboard(region, season!); : await getArchivedRatingsMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
players = neighbors; players = neighbors;
titleImage = clan titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
+7 -5
View File
@@ -9,7 +9,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL';
import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod';
import addFilterOptions from '../core/discord/addFilterOptions'; import addPeriodicFilterOptions from '../core/discord/addPeriodicFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -27,7 +27,7 @@ import resolvePlayerFromCommand, {
ResolvedPlayer, ResolvedPlayer,
} from '../core/discord/resolvePlayerFromCommand'; } from '../core/discord/resolvePlayerFromCommand';
import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { StatFilters, filterStats } from '../core/statistics/filterStats';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryPromisable } from '../events/interactionCreate';
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
@@ -57,8 +57,9 @@ async function render(
); );
} }
export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => { export const statsCommand = new Promise<CommandRegistryPromisable>(
const command = await addFilterOptions( async (resolve) => {
const command = await addPeriodicFilterOptions(
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('stats') .setName('stats')
.setDescription('Regular battles statistics'), .setDescription('Regular battles statistics'),
@@ -101,4 +102,5 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
return await render(player, period, filters); return await render(player, period, filters);
}, },
}); });
}); },
);
+71
View File
@@ -0,0 +1,71 @@
import { SlashCommandBuilder } from 'discord.js';
import { encyclopediaInfo } from '../core/blitz/encyclopediaInfo';
import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton';
import { addFilterOptions } from '../core/discord/addFilterOptions';
import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteTanks from '../core/discord/autocompleteTanks';
import autocompleteUsername from '../core/discord/autocompleteUsername';
import buttonPrimary from '../core/discord/buttonPrimary';
import commandToURL from '../core/discord/commandToURL';
import { getCustomPeriodParams } from '../core/discord/getCustomPeriodParams';
import { getFiltersFromButton } from '../core/discord/getFiltersFromButton';
import { getFiltersFromCommand } from '../core/discord/getFiltersFromCommand';
import resolvePeriodFromButton from '../core/discord/resolvePeriodFromButton';
import resolvePeriodFromCommand from '../core/discord/resolvePeriodFromCommand';
import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { CommandRegistryPromisable } from '../events/interactionCreate';
import { renderBreakdown } from './breakdown';
export const todayCommand = new Promise<CommandRegistryPromisable>(
async (resolve) => {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
const command = addFilterOptions(
new SlashCommandBuilder()
.setName('today')
.setDescription("Today's played tanks and statistics"),
awaitedEncyclopediaInfo,
).addStringOption(addUsernameChoices);
resolve({
inProduction: true,
inPublic: true,
command,
async handler(interaction) {
const player = await resolvePlayerFromCommand(interaction);
const period = resolvePeriodFromCommand(
player.region,
interaction,
'today',
);
const filters = await getFiltersFromCommand(interaction);
const path = commandToURL(interaction, {
...player,
...getCustomPeriodParams(interaction, true),
...filters,
});
return [
...(await renderBreakdown(player, period, filters)),
buttonPrimary(path, 'Refresh'),
await getBlitzStarsLinkButton(player.region, player.id),
];
},
autocomplete: (interaction) => {
autocompleteUsername(interaction);
autocompleteTanks(interaction);
},
async button(interaction) {
const player = await resolvePlayerFromButton(interaction);
const period = resolvePeriodFromButton(player.region, interaction);
const filters = getFiltersFromButton(interaction);
return await renderBreakdown(player, period, filters);
},
});
},
);
@@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
gap: -16, gap: -16,
}} }}
> >
<Glow color={PERCENTILE_COLORS[percentile]} /> <Glow color={color} />
<div <div
style={{ style={{
@@ -71,7 +71,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) {
</span> </span>
</div> </div>
<Glow color={PERCENTILE_COLORS[percentile]} direction="reverse" /> <Glow color={color} direction="reverse" />
</div> </div>
); );
} }
+3 -3
View File
@@ -1,4 +1,5 @@
import { import {
amber,
blue, blue,
green, green,
lime, lime,
@@ -8,7 +9,6 @@ import {
red, red,
slate, slate,
teal, teal,
yellow,
} from '@radix-ui/colors'; } from '@radix-ui/colors';
import { Percentile } from '../constants/percentiles'; import { Percentile } from '../constants/percentiles';
@@ -20,8 +20,8 @@ export const PERCENTILE_COLORS = {
[Percentile.VeryBad]: slate.slate9, [Percentile.VeryBad]: slate.slate9,
[Percentile.Bad]: red.red9, [Percentile.Bad]: red.red9,
[Percentile.BelowAverage]: orange.orange9, [Percentile.BelowAverage]: orange.orange9,
[Percentile.Average]: yellow.yellow9, [Percentile.Average]: amber.amber10,
[Percentile.AboveAverage]: lime.lime9, [Percentile.AboveAverage]: lime.lime8,
[Percentile.Good]: green.green9, [Percentile.Good]: green.green9,
[Percentile.VeryGood]: teal.teal9, [Percentile.VeryGood]: teal.teal9,
[Percentile.Great]: blue.blue9, [Percentile.Great]: blue.blue9,
-1
View File
@@ -18,7 +18,6 @@ export const TANK_TYPES = [
'heavy', 'heavy',
] as const; ] as const;
// TODO: remove hyphen for underscore
export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const;
export type TankType = (typeof TANK_TYPES)[number]; export type TankType = (typeof TANK_TYPES)[number];
+15 -1
View File
@@ -36,6 +36,8 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
style={{ style={{
display: 'flex', display: 'flex',
gap: 4, gap: 4,
alignItems: 'center',
justifyContent: 'center',
}} }}
> >
<span <span
@@ -51,11 +53,23 @@ export default function TitleBar({ name, image, description }: TitleBarProps) {
</span> </span>
</div> </div>
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}> <div
style={{
display: 'flex',
justifyContent: image ? 'flex-start' : 'center',
}}
>
<span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
}}
>
{description} {description}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }
+1
View File
@@ -20,6 +20,7 @@ export const TOOLS: Tool[] = [
id: 'tankopedia', id: 'tankopedia',
title: 'Tankopedia', title: 'Tankopedia',
description: 'Blitz tank encyclopedia', description: 'Blitz tank encyclopedia',
disabled: true,
}, },
{ {
id: 'inactive', id: 'inactive',
+8 -1
View File
@@ -4,7 +4,11 @@ import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch'; import { patientFetch } from '../blitzkrieg/patientFetch';
import regionToRegionSubdomain from './regionToRegionSubdomain'; import regionToRegionSubdomain from './regionToRegionSubdomain';
const ratingsInfoCache: Partial<Record<Region, RatingsInfo>> = {};
export default async function getRatingsInfo(region: Region) { export default async function getRatingsInfo(region: Region) {
if (ratingsInfoCache[region]) return ratingsInfoCache[region]!;
const response = await patientFetch( const response = await patientFetch(
context === 'website' context === 'website'
? `/api/${region}/ratings/current/info` ? `/api/${region}/ratings/current/info`
@@ -14,6 +18,9 @@ export default async function getRatingsInfo(region: Region) {
undefined, undefined,
{ cache: 'no-store' }, { cache: 'no-store' },
); );
const data = (await response.json()) as RatingsInfo;
return (await response.json()) as Promise<RatingsInfo>; ratingsInfoCache[region] = data;
return data;
} }
+4 -4
View File
@@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) {
); );
if (tankStats[id] === null) { if (tankStats[id] === null) {
throw new UserError('No tank stats available', { throw new UserError(
cause: 'No tank stats available',
"This player doesn't have any stats for tanks available. This may not be the player you are looking for.", "This player doesn't have any stats available. This may not be the player you are looking for.",
}); );
} }
return tankStats[id]; return tankStats[id];
+6
View File
@@ -0,0 +1,6 @@
import getRatingsInfo from './getRatingsInfo';
export async function isOnGoingRatingsSeason() {
const ratingsInfo = await getRatingsInfo('com');
return ratingsInfo.detail === undefined;
}
+8 -6
View File
@@ -15,9 +15,10 @@ export default async function resolveTankId(tank: string | number) {
}); });
if (searchResult.length === 0) { if (searchResult.length === 0) {
throw new UserError('Tank not found', { throw new UserError(
cause: `Could not find tank by the name "${tank}".`, 'Tank not found',
}); `Could not find tank by the name "${tank}".`,
);
} else { } else {
return searchResult[0].obj.id; return searchResult[0].obj.id;
} }
@@ -25,9 +26,10 @@ export default async function resolveTankId(tank: string | number) {
if ((await tankDefinitions)[number]) { if ((await tankDefinitions)[number]) {
return number; return number;
} else { } else {
throw new UserError('Tank not found', { throw new UserError(
cause: `Could not find tank by the ID "${number}".`, 'Tank not found',
}); `Could not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`,
);
} }
} }
} }
+6 -1
View File
@@ -1,4 +1,9 @@
/** /**
* Discriminator for user caused errors to not report in logs * Discriminator for user caused errors to not report in logs
*/ */
export class UserError extends Error {} export class UserError {
constructor(
public message: string,
public cause: string,
) {}
}
+10 -26
View File
@@ -1,34 +1,19 @@
import { import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js';
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks'; import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
import { encyclopediaInfo } from '../blitz/encyclopediaInfo'; import { EncyclopediaInfo } from '../blitz/encyclopediaInfo';
import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices'; import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices'; import addTierChoices from './addTierChoices';
export default async function addFilterOptions< export function addFilterOptions<
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder, Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder,
>( >(option: Option, encyclopediaInfo: EncyclopediaInfo) {
option: OptionType,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
): Promise<OptionType> {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
addPeriodSubCommands(option, (option) => {
extra(option);
return option return option
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('nation') .setName('nation')
.setDescription('Nation') .setDescription('Nation')
.addChoices( .addChoices(
...Object.entries(awaitedEncyclopediaInfo.vehicle_nations).map( ...Object.entries(encyclopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }), ([type, name]) => ({ name, value: type }),
), ),
) )
@@ -40,7 +25,7 @@ export default async function addFilterOptions<
.setName('tank-type') .setName('tank-type')
.setDescription('Tank type') .setDescription('Tank type')
.addChoices( .addChoices(
...Object.entries(awaitedEncyclopediaInfo.vehicle_types).map( ...Object.entries(encyclopediaInfo.vehicle_types).map(
([type, name]) => ({ ([type, name]) => ({
name: name.replace(' Tank', ''), name: name.replace(' Tank', ''),
value: type, value: type,
@@ -69,8 +54,7 @@ export default async function addFilterOptions<
) )
.setRequired(false), .setRequired(false),
) )
.addStringOption((option) => addTankChoices(option).setRequired(false)); .addStringOption((option) =>
}); addTankChoices(option).setRequired(false),
) as Option;
return option;
} }
@@ -0,0 +1,25 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { encyclopediaInfo } from '../blitz/encyclopediaInfo';
import { addFilterOptions } from './addFilterOptions';
import addPeriodSubCommands from './addPeriodSubCommands';
export default async function addPeriodicFilterOptions<
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
>(
option: OptionType,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
): Promise<OptionType> {
const awaitedEncyclopediaInfo = await encyclopediaInfo;
addPeriodSubCommands(option, (option) => {
return addFilterOptions(extra(option), awaitedEncyclopediaInfo);
});
return option;
}
+6 -1
View File
@@ -3,12 +3,17 @@ import { PeriodType } from './addPeriodSubCommands';
export function getCustomPeriodParams( export function getCustomPeriodParams(
interaction: ChatInputCommandInteraction, interaction: ChatInputCommandInteraction,
forcedNonCustomPeriod = false,
) {
if (
!forcedNonCustomPeriod &&
(interaction.options.getSubcommand() as PeriodType) === 'custom'
) { ) {
if ((interaction.options.getSubcommand() as PeriodType) === 'custom')
return { return {
start: interaction.options.getInteger('start', true), start: interaction.options.getInteger('start', true),
end: interaction.options.getInteger('end', true), end: interaction.options.getInteger('end', true),
}; };
}
return undefined; return undefined;
} }
+4 -3
View File
@@ -18,9 +18,10 @@ export default async function resolveClanFromCommand(
if (accounts[0]) { if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id }; return { region: accounts[0].region, id: accounts[0].clan_id };
} else { } else {
throw new UserError('Could not find clan', { throw new UserError(
cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`, 'Could not find clan',
}); `\`${clan}\` was not found. Try selecting a clan from the search result.`,
);
} }
} }
} }
+4 -1
View File
@@ -20,11 +20,14 @@ export interface ResolvedPeriod {
export default function resolvePeriodFromCommand( export default function resolvePeriodFromCommand(
region: Region, region: Region,
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType,
) { ) {
let name: string; let name: string;
let start: number; let start: number;
let end: number; let end: number;
const periodSubcommand = interaction.options.getSubcommand() as PeriodType; const periodSubcommand = forcedPeriod
? forcedPeriod
: (interaction.options.getSubcommand() as PeriodType);
if (periodSubcommand === 'custom') { if (periodSubcommand === 'custom') {
const startOption = interaction.options.getInteger('start', true); const startOption = interaction.options.getInteger('start', true);
+8 -6
View File
@@ -34,11 +34,12 @@ export default async function resolvePlayerFromCommand(
id: accounts[0].account_id, id: accounts[0].account_id,
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
throw new UserError('Could not find user', { throw new UserError(
cause: `I couldn't find user "${markdownEscape( 'Could not find user',
`I couldn't find user "${markdownEscape(
commandUsername, commandUsername,
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`, )}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
}); );
} }
} }
} else { } else {
@@ -47,9 +48,10 @@ export default async function resolvePlayerFromCommand(
if (account) { if (account) {
return { region: account.region, id: account.blitz }; return { region: account.region, id: account.blitz };
} else { } else {
throw new UserError('Use the `/link` command', { throw new UserError(
cause: 'Link your Blitz and Discord accounts to get started.', "You're account isn't linked with Blitzkrieg yet",
}); 'Use the `/link` command to get started.',
);
} }
} }
} }
@@ -19,7 +19,8 @@ import { psa } from '../../../core/discord/psa';
export default async function handleChatInputCommand( export default async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) { ) {
const registry = (await commands)[interaction.commandName]; const awaitedCommands = await commands;
const registry = awaitedCommands[interaction.commandName];
await interaction.deferReply(); await interaction.deferReply();
+24 -17
View File
@@ -31,6 +31,7 @@ import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers'; import { searchPlayersCommand } from '../../commands/searchPlayers';
import { searchTanksCommand } from '../../commands/searchTanks'; import { searchTanksCommand } from '../../commands/searchTanks';
import { statsCommand } from '../../commands/stats'; import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today';
import getClientId from '../../core/blitzkrieg/getClientId'; import getClientId from '../../core/blitzkrieg/getClientId';
import isDev from '../../core/blitzkrieg/isDev'; import isDev from '../../core/blitzkrieg/isDev';
import { secrets } from '../../core/blitzkrieg/secrets'; import { secrets } from '../../core/blitzkrieg/secrets';
@@ -52,34 +53,39 @@ export type InteractionReturnable =
| InteractionIterableReturnable | InteractionIterableReturnable
| Promise<InteractionIterableReturnable>; | Promise<InteractionIterableReturnable>;
export interface Registry { type CommandRegistryBase = {
inProduction: boolean;
}
export type CommandRegistryRaw = Registry & {
inPublic: boolean;
inPreview?: boolean;
command: command:
| SlashCommandBuilder | SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>; | Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
};
type CommandRegistryDefinitionBase = {
inProduction: boolean;
inPublic: boolean;
inPreview?: boolean;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void; autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable; button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
} & ( };
| { type CommandRegistryDefinitionHandlesInteraction = {
handlesInteraction: true; handlesInteraction: true;
handler: (interaction: ChatInputCommandInteraction<CacheType>) => void; handler: (interaction: ChatInputCommandInteraction<CacheType>) => void;
} };
| { type CommandRegistryDefinitionDoesHandleInteraction = {
handlesInteraction?: false; handlesInteraction?: false;
handler: ( handler: (
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable; ) => InteractionReturnable;
} };
); export type CommandRegistryPromisable = CommandRegistryBase &
export type CommandRegistry = CommandRegistryRaw | Promise<CommandRegistryRaw>; (CommandRegistryDefinitionBase &
(
| CommandRegistryDefinitionHandlesInteraction
| CommandRegistryDefinitionDoesHandleInteraction
));
export type CommandRegistry =
| CommandRegistryPromisable
| Promise<CommandRegistryPromisable>;
const rest = new REST().setToken(secrets.DISCORD_TOKEN); const rest = new REST().setToken(secrets.DISCORD_TOKEN);
@@ -101,10 +107,11 @@ export const COMMANDS_RAW: CommandRegistry[] = [
evolutionCommand, evolutionCommand,
statsCommand, statsCommand,
ratingsCommand, ratingsCommand,
todayCommand,
]; ];
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) => export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
rawCommands.reduce<Record<string, CommandRegistryRaw>>( rawCommands.reduce<Record<string, CommandRegistryPromisable>>(
(accumulator, registry) => { (accumulator, registry) => {
if (isDev()) registry.command.setDefaultMemberPermissions(0); if (isDev()) registry.command.setDefaultMemberPermissions(0);
@@ -123,7 +130,7 @@ export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
[]; [];
commands.then((awaitedCommands) => { commands.then((awaitedCommands) => {
Object.entries(awaitedCommands).forEach(([, registry]) => { Object.values(awaitedCommands).forEach((registry) => {
const json = registry.command.toJSON(); const json = registry.command.toJSON();
if (registry.inPublic) { if (registry.inPublic) {