From 3f264d6a4964d416bd31be93691f2073e1847b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:31:45 -0600 Subject: [PATCH 01/27] feat: auto restart every 5 hours --- .github/workflows/cd-host.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-host.yml b/.github/workflows/cd-host.yml index 9fb0336e5..d9fd59e25 100644 --- a/.github/workflows/cd-host.yml +++ b/.github/workflows/cd-host.yml @@ -2,7 +2,7 @@ name: CD Host on: schedule: - - cron: '0 */6 * * *' + - cron: '0 */5 * * *' workflow_dispatch: concurrency: From 90ee71c9ca35b03c00ffe992de1461af51d44a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 1 Jan 2024 14:19:13 -0600 Subject: [PATCH 02/27] fix: archived season number --- .../getArchivedLatestSeasonNumber.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts b/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts index ba4a1a808..3e7cca876 100644 --- a/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts +++ b/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts @@ -1,17 +1,28 @@ import getRatingsInfo from '../blitz/getRatingsInfo'; +interface GitHubTrees { + tree: { path: string; url: string }[]; +} + let cachedLatestArchivedSeasonNumber: number | null = null; export async function getArchivedLatestSeasonNumber() { + if (cachedLatestArchivedSeasonNumber !== null) { + return cachedLatestArchivedSeasonNumber; + } + const ratingsInfo = await getRatingsInfo('com'); if (ratingsInfo.detail) { - const comURL = ( + const regionsURL = ( (await fetch( - 'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main/regions', - ).then((response) => response.json())) as { - tree: { path: string; url: string }[]; - } + 'https://api.github.com/repos/tresabhi/blitzkrieg-assets/git/trees/main', + ).then((response) => response.json())) as GitHubTrees + ).tree.find(({ path }) => path === 'regions')!.url; + const comURL = ( + (await fetch(regionsURL).then((response) => + response.json(), + )) as GitHubTrees ).tree.find(({ path }) => path === 'com')!.url; const ratingsURL = ( (await fetch(comURL).then((response) => response.json())) as { @@ -20,14 +31,16 @@ export async function getArchivedLatestSeasonNumber() { ).tree[0].url; const latestSeason = Math.max( ...( - (await fetch(ratingsURL).then((response) => { - response.json(); - })) as { tree: { path: string }[] } - ).tree.map(({ path }) => parseInt(path)), + (await fetch(ratingsURL).then((response) => response.json())) as { + tree: { path: string }[]; + } + ).tree.map(({ path }) => Number(path)), ); - return latestSeason; + cachedLatestArchivedSeasonNumber = latestSeason; } else { - return ratingsInfo.current_season - 1; + cachedLatestArchivedSeasonNumber = ratingsInfo.current_season - 1; } + + return cachedLatestArchivedSeasonNumber; } From fff42b61e1d81c842c412db711555383b7292d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 1 Jan 2024 14:22:39 -0600 Subject: [PATCH 03/27] fix: deleted ratings players --- src/commands/ratings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 7fe6a5b23..f807ef7ff 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -298,8 +298,8 @@ export const ratingsCommand = new Promise( position: firstPlayerIndex + index + 1, clan: clanData[player.id]?.clan?.tag, nickname: - clanData[player.id]?.account_name ?? - `Unknown Player ${player.id}`, + clanData[index]?.account_name ?? + `Deleted ${player.id}`, }) satisfies SimplifiedPlayer, ); }, From fe562915cbe175ce4329ff4edee7e8c3bc0c7edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 1 Jan 2024 14:24:45 -0600 Subject: [PATCH 04/27] feat: docs --- docs/changelogs/1.15.9.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 docs/changelogs/1.15.9.md diff --git a/docs/changelogs/1.15.9.md b/docs/changelogs/1.15.9.md new file mode 100644 index 000000000..f3c58e41e --- /dev/null +++ b/docs/changelogs/1.15.9.md @@ -0,0 +1,13 @@ +# Blitzkrieg 1.15.9 + +~~Curiosity killed the cat~~ Ratings killed the Blitzkrieg + +## Fixes + +- Deleted ratings players are now handled properly + +## Technical Fixes + +- Latest archived season number + - Is now cached properly + - Is now fetched in a chaining fashion correctly diff --git a/package.json b/package.json index 8a933ceb9..d7d12bb84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.15.8", + "version": "1.15.9", "description": "🎉 Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", From c804a3d56419e5ef955e6e9d75da684659433d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:53:42 -0600 Subject: [PATCH 05/27] feat: update version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7d12bb84..59c5e31fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.15.9", + "version": "1.15.10", "description": "🎉 Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", From 5ca9e29f3d85ab21a4231ad90015a5fd23bff5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:22:53 -0600 Subject: [PATCH 06/27] fix: percentile indicator contrast --- .../AllStatsOverview/components/WN8Display/index.tsx | 4 ++-- src/components/PercentileIndicator.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AllStatsOverview/components/WN8Display/index.tsx b/src/components/AllStatsOverview/components/WN8Display/index.tsx index 6e8fe8aba..3e33af43b 100644 --- a/src/components/AllStatsOverview/components/WN8Display/index.tsx +++ b/src/components/AllStatsOverview/components/WN8Display/index.tsx @@ -37,7 +37,7 @@ export function WN8Display({ WN8 }: WN8DisplayProps) { gap: -16, }} > - +
- + ); } diff --git a/src/components/PercentileIndicator.tsx b/src/components/PercentileIndicator.tsx index 548a46039..25fe22e6c 100644 --- a/src/components/PercentileIndicator.tsx +++ b/src/components/PercentileIndicator.tsx @@ -1,4 +1,5 @@ import { + amber, blue, green, lime, @@ -8,7 +9,6 @@ import { red, slate, teal, - yellow, } from '@radix-ui/colors'; import { Percentile } from '../constants/percentiles'; @@ -20,8 +20,8 @@ export const PERCENTILE_COLORS = { [Percentile.VeryBad]: slate.slate9, [Percentile.Bad]: red.red9, [Percentile.BelowAverage]: orange.orange9, - [Percentile.Average]: yellow.yellow9, - [Percentile.AboveAverage]: lime.lime9, + [Percentile.Average]: amber.amber10, + [Percentile.AboveAverage]: lime.lime8, [Percentile.Good]: green.green9, [Percentile.VeryGood]: teal.teal9, [Percentile.Great]: blue.blue9, From ff9bda29cf06822535e383d7bac295073905e8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:28:02 -0600 Subject: [PATCH 07/27] fix: archive league neighbors --- src/commands/ratings.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index f807ef7ff..197a55c21 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -384,8 +384,10 @@ export const ratingsCommand = new Promise( id: player.id, score: player.score, position: playerIndex + index + 1, - clan: clanData[player.id]!.clan?.tag, - nickname: clanData[player.id]!.account_name, + clan: clanData[index]?.clan?.tag, + nickname: clanData[index] + ? clanData[index]!.account_name + : `Deleted ${player.id}`, })); }, ); From 890a4d142b3fd8f8bfad02e929e57bd8453b3d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:02:03 -0600 Subject: [PATCH 08/27] feat: simplified /ratings command --- src/commands/ratings.tsx | 359 +++++++++++------------ src/core/blitz/getRatingsInfo.ts | 9 +- src/core/blitz/isOnGoingRatingsSeason.ts | 6 + 3 files changed, 180 insertions(+), 194 deletions(-) create mode 100644 src/core/blitz/isOnGoingRatingsSeason.ts diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 197a55c21..68ab00559 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -2,7 +2,6 @@ import { APIApplicationCommandOptionChoice, SlashCommandBuilder, SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, } from 'discord.js'; import { range } from 'lodash'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; @@ -17,13 +16,13 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo'; import { getRatingsLeague } from '../core/blitz/getRatingsLeague'; import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors'; +import { isOnGoingRatingsSeason } from '../core/blitz/isOnGoingRatingsSeason'; import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber'; import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; import { getArchivedRatingsLeaderboard } from '../core/blitzkrieg/getArchivedRatingsLeaderboard'; import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchivedRatingsMidnightLeaderboard'; import { UserError } from '../core/blitzkrieg/userError'; import addRegionChoices from '../core/discord/addRegionChoices'; -import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; import embedNegative from '../core/discord/embedNegative'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; @@ -116,56 +115,6 @@ export interface RatingsNeighbors { neighbors: RatingsPlayer[]; } -function addSubcommands( - option: SlashCommandSubcommandGroupBuilder, - addOptions = (option: SlashCommandSubcommandBuilder) => option, -) { - return option - .addSubcommand((option) => - addOptions( - option - .setName('neighbors') - .setDescription('Your position and neighbors'), - ) - .addStringOption(addUsernameChoices) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('How many neighbors to display (default: 10)') - .setRequired(false) - .setMinValue(5) - .setMaxValue(30), - ), - ) - .addSubcommand((option) => - addOptions(option.setName('league').setDescription('Top league players')) - .addStringOption((option) => - option - .setName('league') - .setDescription('The league to display') - .addChoices( - ...LEAGUES.map( - ({ name }, value) => - ({ - name, - value: `${value}`, - }) satisfies APIApplicationCommandOptionChoice, - ), - ) - .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.", @@ -174,38 +123,72 @@ const noOngoingSeason = embedNegative( export const ratingsCommand = new Promise( async (resolve) => { const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); - const seasonChoices = range( + const onGoingSeason = await isOnGoingRatingsSeason(); + const seasonNumbers = range( FIRST_ARCHIVED_RATINGS_SEASON, - latestArchivedSeasonNumber + 1, - ).map( - (number) => - ({ - name: `Season ${number}`, - value: `${number}`, - }) satisfies APIApplicationCommandOptionChoice, + latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, ); + function addSubcommands(option: SlashCommandSubcommandBuilder) { + return option + .addIntegerOption((option) => + option + .setName('limit') + .setDescription('How many neighbors to display (default: 10)') + .setRequired(false) + .setMinValue(5) + .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, + ) + .reverse(), + ), + ); + } + 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) => + .addSubcommand((option) => addSubcommands( option - .setName('current') - .setDescription('The current running Ratings season'), + .setName('neighbors') + .setDescription('Shows neighboring players on the leaderboard'), + ), + ) + .addSubcommand((option) => + addSubcommands( + option + .setName('league') + .setDescription('Shows top players from a league') + .addStringOption(addRegionChoices) + .addStringOption((option) => + option + .setName('league') + .setDescription('The league to display') + .addChoices( + ...LEAGUES.map( + ({ name }, value) => + ({ + name, + value: `${value}`, + }) satisfies APIApplicationCommandOptionChoice, + ), + ) + .setRequired(true), + ), ), ); @@ -224,18 +207,19 @@ export const ratingsCommand = new Promise( nickname: string; } - const subcommandGroup = interaction.options.getSubcommandGroup(true) as - | 'archive' - | 'current'; const subcommand = interaction.options.getSubcommand(true) as | 'league' | 'neighbors'; const limit = interaction.options.getInteger('limit') ?? 10; 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 titleImage: string | undefined; - let titleNameDiscriminator: string | undefined; let titleDescription: string; let playersBefore: number; let playersAfter: number; @@ -249,69 +233,66 @@ export const ratingsCommand = new Promise( interaction.options.getString('league')!, ); const region = interaction.options.getString('region') as Region; - regionRatingsInfo = await (subcommandGroup === 'current' + regionRatingsInfo = await (isCurrentSeason ? getRatingsInfo(region) : getArchivedRatingsInfo(region, season!)); if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; - const result = - subcommandGroup === 'current' - ? await getRatingsLeague(region, leagueIndex).then( - async (result) => - result.slice(0, limit).map( - (player) => - ({ - id: player.spa_id, - score: player.score, - position: player.number, - clan: player.clan_tag, - nickname: player.nickname, - }) satisfies SimplifiedPlayer, - ), - ) - : await getArchivedRatingsLeaderboard(region, season!).then( - async (players) => { - const firstPlayerIndex = - leagueIndex === 0 - ? 0 - : players.findIndex( - (player) => - player.score < LEAGUES[leagueIndex - 1].minScore, - ); - const lastPlayerIndex = firstPlayerIndex + limit - 1; - const trimmed = players.slice( - firstPlayerIndex, - lastPlayerIndex + 1, - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map(({ id }) => id), - ['clan'], - ); + const result = isCurrentSeason + ? await getRatingsLeague(region, leagueIndex).then(async (result) => + result.slice(0, limit).map( + (player) => + ({ + id: player.spa_id, + score: player.score, + position: player.number, + clan: player.clan_tag, + nickname: player.nickname, + }) satisfies SimplifiedPlayer, + ), + ) + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { + const firstPlayerIndex = + leagueIndex === 0 + ? 0 + : players.findIndex( + (player) => + player.score < LEAGUES[leagueIndex - 1].minScore, + ); + const lastPlayerIndex = firstPlayerIndex + limit - 1; + const trimmed = players.slice( + firstPlayerIndex, + lastPlayerIndex + 1, + ); + const clanData = await getClanAccountInfo( + region, + trimmed.map(({ id }) => id), + ['clan'], + ); - return trimmed.map( - (player, index) => - ({ - id: player.id, - score: player.score, - position: firstPlayerIndex + index + 1, - clan: clanData[player.id]?.clan?.tag, - nickname: - clanData[index]?.account_name ?? - `Deleted ${player.id}`, - }) satisfies SimplifiedPlayer, - ); - }, - ); + return trimmed.map( + (player, index) => + ({ + id: player.id, + score: player.score, + position: firstPlayerIndex + index + 1, + clan: clanData[player.id]?.clan?.tag, + nickname: + clanData[index]?.account_name ?? + `Deleted ${player.id}`, + }) satisfies SimplifiedPlayer, + ); + }, + ); const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; - midnightLeaderboard = - subcommandGroup === 'current' - ? await getArchivedRatingsMidnightLeaderboard( - region, - regionRatingsInfo.current_season, - ) - : await getArchivedRatingsMidnightLeaderboard(region, season!); + midnightLeaderboard = isCurrentSeason + ? await getArchivedRatingsMidnightLeaderboard( + region, + regionRatingsInfo.current_season, + ) + : await getArchivedRatingsMidnightLeaderboard(region, season!); players = result; playersBefore = result ? result[0].position - 1 : 0; @@ -326,22 +307,17 @@ export const ratingsCommand = new Promise( titleDescription = `Top ${limit} players`; } else { const { region, id } = await resolvePlayerFromCommand(interaction); - regionRatingsInfo = - subcommandGroup === 'current' - ? await getRatingsInfo(region) - : await getArchivedRatingsInfo(region, season!); + regionRatingsInfo = isCurrentSeason + ? await getRatingsInfo(region) + : await getArchivedRatingsInfo(region, season!); if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; const accountInfo = await getAccountInfo(region, id); const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; - const neighbors = - subcommandGroup === 'current' - ? await getRatingsNeighbors( - region, - id, - Math.round(limit / 2), - ).then((players) => + const neighbors = isCurrentSeason + ? await getRatingsNeighbors(region, id, Math.round(limit / 2)).then( + (players) => players.neighbors.map( (player) => ({ @@ -352,54 +328,51 @@ export const ratingsCommand = new Promise( nickname: player.nickname, }) satisfies SimplifiedPlayer, ), - ) - : await getArchivedRatingsLeaderboard(region, season!).then( - async (players) => { - const playerIndex = players.findIndex( - (player) => player.id === id, + ) + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { + const playerIndex = players.findIndex( + (player) => player.id === id, + ); + + if (playerIndex === -1) { + throw new UserError( + `${accountInfo.nickname} didn't player ratings in season ${season}`, + { + cause: + 'This player did not participate in this season or did not get past calibration.', + }, ); + } - if (playerIndex === -1) { - throw new UserError( - `${accountInfo.nickname} didn't player ratings in season ${season}`, - { - cause: - 'This player did not participate in this season or did not get past calibration.', - }, - ); - } + const halfRange = Math.round(limit / 2); + const trimmed = players.slice( + playerIndex - halfRange, + playerIndex + halfRange + 1, + ); + const clanData = await getClanAccountInfo( + region, + trimmed.map((player) => player.id), + ['clan'], + ); - const halfRange = Math.round(limit / 2); - const trimmed = players.slice( - playerIndex - halfRange, - playerIndex + halfRange + 1, - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map((player) => player.id), - ['clan'], - ); - - return trimmed.map((player, index) => ({ - id: player.id, - score: player.score, - position: playerIndex + index + 1, - clan: clanData[index]?.clan?.tag, - nickname: clanData[index] - ? clanData[index]!.account_name - : `Deleted ${player.id}`, - })); - }, - ); - midnightLeaderboard = - subcommandGroup === 'current' - ? await getArchivedRatingsMidnightLeaderboard( - region, - regionRatingsInfo.current_season, - ) - : await getArchivedRatingsMidnightLeaderboard(region, season!); - - if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`; + return trimmed.map((player, index) => ({ + id: player.id, + score: player.score, + position: playerIndex + index + 1, + clan: clanData[index]?.clan?.tag, + nickname: clanData[index] + ? clanData[index]!.account_name + : `Deleted ${player.id}`, + })); + }, + ); + midnightLeaderboard = isCurrentSeason + ? await getArchivedRatingsMidnightLeaderboard( + region, + regionRatingsInfo.current_season, + ) + : await getArchivedRatingsMidnightLeaderboard(region, season!); players = neighbors; titleImage = clan diff --git a/src/core/blitz/getRatingsInfo.ts b/src/core/blitz/getRatingsInfo.ts index 6c77df9f7..ef1616f12 100644 --- a/src/core/blitz/getRatingsInfo.ts +++ b/src/core/blitz/getRatingsInfo.ts @@ -4,7 +4,11 @@ import { context } from '../blitzkrieg/context'; import { patientFetch } from '../blitzkrieg/patientFetch'; import regionToRegionSubdomain from './regionToRegionSubdomain'; +const ratingsInfoCache: Partial> = {}; + export default async function getRatingsInfo(region: Region) { + if (ratingsInfoCache[region]) return ratingsInfoCache[region]!; + const response = await patientFetch( context === 'website' ? `/api/${region}/ratings/current/info` @@ -14,6 +18,9 @@ export default async function getRatingsInfo(region: Region) { undefined, { cache: 'no-store' }, ); + const data = (await response.json()) as RatingsInfo; - return (await response.json()) as Promise; + ratingsInfoCache[region] = data; + + return data; } diff --git a/src/core/blitz/isOnGoingRatingsSeason.ts b/src/core/blitz/isOnGoingRatingsSeason.ts new file mode 100644 index 000000000..ef6f1e2d5 --- /dev/null +++ b/src/core/blitz/isOnGoingRatingsSeason.ts @@ -0,0 +1,6 @@ +import getRatingsInfo from './getRatingsInfo'; + +export async function isOnGoingRatingsSeason() { + const ratingsInfo = await getRatingsInfo('com'); + return ratingsInfo.detail === undefined; +} From 4c746cbb864bd6967ea634aac7f4fb27658c426f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:07:15 -0600 Subject: [PATCH 09/27] feat: improved error messages --- src/commands/ratings.tsx | 7 ++----- src/core/blitz/getTankStats.ts | 8 ++++---- src/core/blitz/resolveTankId.ts | 14 ++++++++------ src/core/blitzkrieg/userError.ts | 7 ++++++- src/core/discord/resolveClanFromCommand.ts | 7 ++++--- src/core/discord/resolvePlayerFromCommand.ts | 14 ++++++++------ 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 68ab00559..e238e2063 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -337,11 +337,8 @@ export const ratingsCommand = new Promise( if (playerIndex === -1) { throw new UserError( - `${accountInfo.nickname} didn't player ratings in season ${season}`, - { - cause: - 'This player did not participate in this season or did not get past calibration.', - }, + `${accountInfo.nickname} didn't play ratings in season ${season}`, + 'This player did not participate in this season or did not get past calibration.', ); } diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts index 4965c43d7..763d720cf 100644 --- a/src/core/blitz/getTankStats.ts +++ b/src/core/blitz/getTankStats.ts @@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) { ); if (tankStats[id] === null) { - throw new UserError('No tank stats available', { - cause: - "This player doesn't have any stats for tanks available. This may not be the player you are looking for.", - }); + throw new UserError( + 'No tank stats available', + "This player doesn't have any stats available. This may not be the player you are looking for.", + ); } return tankStats[id]; diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index c110ee3a2..813f5e51a 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -15,9 +15,10 @@ export default async function resolveTankId(tank: string | number) { }); if (searchResult.length === 0) { - throw new UserError('Tank not found', { - cause: `Could not find tank by the name "${tank}".`, - }); + throw new UserError( + 'Tank not found', + `Could not find tank by the name "${tank}".`, + ); } else { return searchResult[0].obj.id; } @@ -25,9 +26,10 @@ export default async function resolveTankId(tank: string | number) { if ((await tankDefinitions)[number]) { return number; } else { - throw new UserError('Tank not found', { - cause: `Could not find tank by the ID "${number}".`, - }); + throw new UserError( + 'Tank not found', + `Could not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`, + ); } } } diff --git a/src/core/blitzkrieg/userError.ts b/src/core/blitzkrieg/userError.ts index 06b0ee65c..e4eb7cc27 100644 --- a/src/core/blitzkrieg/userError.ts +++ b/src/core/blitzkrieg/userError.ts @@ -1,4 +1,9 @@ /** * 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, + ) {} +} diff --git a/src/core/discord/resolveClanFromCommand.ts b/src/core/discord/resolveClanFromCommand.ts index a43bb78fc..f332ca22e 100644 --- a/src/core/discord/resolveClanFromCommand.ts +++ b/src/core/discord/resolveClanFromCommand.ts @@ -18,9 +18,10 @@ export default async function resolveClanFromCommand( if (accounts[0]) { return { region: accounts[0].region, id: accounts[0].clan_id }; } else { - throw new UserError('Could not find clan', { - cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`, - }); + throw new UserError( + 'Could not find clan', + `\`${clan}\` was not found. Try selecting a clan from the search result.`, + ); } } } diff --git a/src/core/discord/resolvePlayerFromCommand.ts b/src/core/discord/resolvePlayerFromCommand.ts index a766f2161..21ca22347 100644 --- a/src/core/discord/resolvePlayerFromCommand.ts +++ b/src/core/discord/resolvePlayerFromCommand.ts @@ -34,11 +34,12 @@ export default async function resolvePlayerFromCommand( id: accounts[0].account_id, } satisfies ResolvedPlayer; } else { - throw new UserError('Could not find user', { - cause: `I couldn't find user "${markdownEscape( + throw new UserError( + 'Could not find user', + `I couldn't find user "${markdownEscape( commandUsername, )}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`, - }); + ); } } } else { @@ -47,9 +48,10 @@ export default async function resolvePlayerFromCommand( if (account) { return { region: account.region, id: account.blitz }; } else { - throw new UserError('Use the `/link` command', { - cause: 'Link your Blitz and Discord accounts to get started.', - }); + throw new UserError( + "You're account isn't linked with Blitzkrieg yet", + 'Use the `/link` command to get started.', // TODO: add shortcut button too + ); } } } From ae07b8de4cd2eaeb5bed5de2a630a751fc278f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:47:46 -0600 Subject: [PATCH 10/27] feat: center align text when no image --- src/components/Tanks/components/Item.tsx | 1 - src/components/TitleBar.tsx | 20 +++++++++++++++++--- src/core/discord/resolvePlayerFromCommand.ts | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/Tanks/components/Item.tsx b/src/components/Tanks/components/Item.tsx index 4363e0a80..4bf8766d1 100644 --- a/src/components/Tanks/components/Item.tsx +++ b/src/components/Tanks/components/Item.tsx @@ -18,7 +18,6 @@ export const TANK_TYPES = [ 'heavy', ] as const; -// TODO: remove hyphen for underscore export const TREE_TYPES = ['researchable', 'premium', 'collector'] as const; export type TankType = (typeof TANK_TYPES)[number]; diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index b3e4da675..7449da129 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -36,6 +36,8 @@ export default function TitleBar({ name, image, description }: TitleBarProps) { style={{ display: 'flex', gap: 4, + alignItems: 'center', + justifyContent: 'center', }} > - - {description} - +
+ + {description} + +
diff --git a/src/core/discord/resolvePlayerFromCommand.ts b/src/core/discord/resolvePlayerFromCommand.ts index 21ca22347..fa6d156d1 100644 --- a/src/core/discord/resolvePlayerFromCommand.ts +++ b/src/core/discord/resolvePlayerFromCommand.ts @@ -50,7 +50,7 @@ export default async function resolvePlayerFromCommand( } else { throw new UserError( "You're account isn't linked with Blitzkrieg yet", - 'Use the `/link` command to get started.', // TODO: add shortcut button too + 'Use the `/link` command to get started.', ); } } From 0480445bae93281e5afcfabe6dac033918b9f517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:53:27 -0600 Subject: [PATCH 11/27] feat: easier /link command --- src/commands/link.tsx | 294 ++++++++++++++++++++---------------------- 1 file changed, 140 insertions(+), 154 deletions(-) diff --git a/src/commands/link.tsx b/src/commands/link.tsx index 9db82aaae..1e44da607 100644 --- a/src/commands/link.tsx +++ b/src/commands/link.tsx @@ -1,25 +1,21 @@ import { teal } from '@radix-ui/colors'; -import { - AttachmentBuilder, - GuildMemberRoleManager, - SlashCommandBuilder, -} from 'discord.js'; +import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js'; import markdownEscape from 'markdown-escape'; import discord from '../../discord.json' assert { type: 'json' }; import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow'; import Wrapper from '../components/Wrapper'; -import { REGION_NAMES_SHORT, Region } from '../constants/regions'; +import { REGION_NAMES_SHORT } from '../constants/regions'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz'; import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; +import embedInfo from '../core/discord/embedInfo'; import embedNegative from '../core/discord/embedNegative'; +import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import { CommandRegistry } from '../events/interactionCreate'; import { theme } from '../stitches.config'; -const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; - export const verifyCommand: CommandRegistry = { inProduction: true, inPublic: true, @@ -30,193 +26,183 @@ export const verifyCommand: CommandRegistry = { .addStringOption((option) => addUsernameChoices(option).setRequired(true)), async handler(interaction) { - const username = interaction.options.getString('username', true); + const { id, region } = await resolvePlayerFromCommand(interaction); + const discordId = parseInt(interaction.user.id); + const accountInfo = await getAccountInfo(region, id); + const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); - if (serverAndIdPattern.test(username)) { - const [region, idString] = username.split('/'); - const id = parseInt(idString); - const discordId = parseInt(interaction.user.id); - const accountInfo = await getAccountInfo(region as Region, id); - const clanAccountInfo = await getClanAccountInfo(region as Region, id, [ - 'clan', - ]); + await linkBlitzAndDiscord(discordId, region, id); - await linkBlitzAndDiscord(discordId, region as Region, id); - - 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 (interaction.member!.roles as GuildMemberRoleManager).remove( - discord.sklld_verify_role, - ); - await (interaction.member!.roles as GuildMemberRoleManager).add( - discord.sklld_peasant_role, + 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.", ); } - return ( - + await (interaction.member!.roles as GuildMemberRoleManager).remove( + discord.sklld_verify_role, + ); + await (interaction.member!.roles as GuildMemberRoleManager).add( + discord.sklld_peasant_role, + ); + } + + return [ + +
+ +
- +
+ + +
+ + {interaction.user.displayName} + + + @{interaction.user.username} + +
+
-
+ /> + Accounts linked +
+ +
+ {clanAccountInfo?.clan ? ( - -
- - {interaction.user.displayName} - - - @{interaction.user.username} - -
-
- + ) : null}
- - Accounts linked -
- -
- {clanAccountInfo?.clan ? ( - - ) : null} -
- - {accountInfo.nickname} - - - {clanAccountInfo?.clan - ? `[${clanAccountInfo?.clan?.tag}] - ` - : ''} - {REGION_NAMES_SHORT[region as Region]} - -
+ {accountInfo.nickname} + + + {clanAccountInfo?.clan + ? `[${clanAccountInfo?.clan?.tag}] - ` + : ''} + {REGION_NAMES_SHORT[region]} +
- -
- - ); - } else { - return [ - embedNegative( - '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'), - ]; - } + + +
+
, + + embedInfo( + 'Incorrect account?', + 'Use the command again and select a username from the search results.', + ), + ]; }, autocomplete: autocompleteUsername, From 23a23e36bb79ba2699334d43fce5c49ef1eed65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:32:46 -0600 Subject: [PATCH 12/27] feat: /today alias for /breakdown today --- .vscode/settings.json | 1 + src/commands/breakdown.tsx | 14 +- src/commands/debug.ts | 2 +- src/commands/evolution.tsx | 4 +- src/commands/fullStats.tsx | 8 +- src/commands/ratings.tsx | 4 +- src/commands/stats.tsx | 82 ++++++------ src/commands/today.ts | 71 ++++++++++ src/core/discord/addFilterOptions.ts | 126 ++++++++---------- src/core/discord/addPeriodicFilterOptions.ts | 25 ++++ src/core/discord/getCustomPeriodParams.ts | 7 +- src/core/discord/resolvePeriodFromCommand.ts | 5 +- .../handlers/chatInputCommand.ts | 3 +- src/events/interactionCreate/index.ts | 53 ++++---- 14 files changed, 252 insertions(+), 153 deletions(-) create mode 100644 src/commands/today.ts create mode 100644 src/core/discord/addPeriodicFilterOptions.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4482684a6..91a3cb534 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,7 @@ "ownedtanks", "playerachievements", "playerinfo", + "Promisable", "ratingsleaderboard", "ratingsplot", "researchable", diff --git a/src/commands/breakdown.tsx b/src/commands/breakdown.tsx index fe05523db..fc6308cc9 100644 --- a/src/commands/breakdown.tsx +++ b/src/commands/breakdown.tsx @@ -13,7 +13,7 @@ import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; import getStatsInPeriod from '../core/blitzstars/getStatsInPeriod'; 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 autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteUsername from '../core/discord/autocompleteUsername'; @@ -33,12 +33,12 @@ import resolvePlayerFromCommand, { import calculateWN8 from '../core/statistics/calculateWN8'; import { StatFilters, filterStats } from '../core/statistics/filterStats'; import getWN8Percentile from '../core/statistics/getWN8Percentile'; -import { CommandRegistryRaw } from '../events/interactionCreate'; +import { CommandRegistryPromisable } from '../events/interactionCreate'; const ROWS_PER_PAGE = 8; const MAX_PAGES = 10; -async function render( +export async function renderBreakdown( { region, id }: ResolvedPlayer, { start, end, name }: ResolvedPeriod, filters: StatFilters, @@ -290,9 +290,9 @@ async function render( } } -export const breakdownCommand = new Promise( +export const breakdownCommand = new Promise( async (resolve) => { - const command = await addFilterOptions( + const command = await addPeriodicFilterOptions( new SlashCommandBuilder() .setName('breakdown') .setDescription("A period's breakdown by tanks played"), @@ -316,7 +316,7 @@ export const breakdownCommand = new Promise( }); return [ - ...(await render(player, period, filters)), + ...(await renderBreakdown(player, period, filters)), buttonPrimary(path, 'Refresh'), await getBlitzStarsLinkButton(player.region, player.id), ]; @@ -332,7 +332,7 @@ export const breakdownCommand = new Promise( const period = resolvePeriodFromButton(player.region, interaction); const filters = getFiltersFromButton(interaction); - return await render(player, period, filters); + return await renderBreakdown(player, period, filters); }, }); }, diff --git a/src/commands/debug.ts b/src/commands/debug.ts index 70ac62ad9..ef768d4d6 100644 --- a/src/commands/debug.ts +++ b/src/commands/debug.ts @@ -13,7 +13,7 @@ export const debugCommand: CommandRegistry = { .setName('debug') .setDescription('Debug information about the bot'), - async handler(interaction) { + async handler() { const currentTime = new Date().getTime(); const uptime = currentTime - executionStart; const list = [ diff --git a/src/commands/evolution.tsx b/src/commands/evolution.tsx index b495076e4..5b8cbd213 100644 --- a/src/commands/evolution.tsx +++ b/src/commands/evolution.tsx @@ -29,7 +29,7 @@ import resolvePlayerFromButton from '../core/discord/resolvePlayerFromButton'; import resolvePlayerFromCommand, { ResolvedPlayer, } from '../core/discord/resolvePlayerFromCommand'; -import { CommandRegistryRaw } from '../events/interactionCreate'; +import { CommandRegistryPromisable } from '../events/interactionCreate'; type EvolutionStatType = 'player' | 'tank'; @@ -107,7 +107,7 @@ async function render( ); } -export const evolutionCommand = new Promise( +export const evolutionCommand = new Promise( async (resolve) => { let playerGroup: SlashCommandSubcommandGroupBuilder; let tankGroup: SlashCommandSubcommandGroupBuilder; diff --git a/src/commands/fullStats.tsx b/src/commands/fullStats.tsx index f9262602a..01ca868a8 100644 --- a/src/commands/fullStats.tsx +++ b/src/commands/fullStats.tsx @@ -10,7 +10,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; 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 autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteUsername from '../core/discord/autocompleteUsername'; @@ -29,7 +29,7 @@ import resolvePlayerFromCommand, { } from '../core/discord/resolvePlayerFromCommand'; import { StatFilters, filterStats } from '../core/statistics/filterStats'; import { getTierWeights } from '../core/statistics/getTierWeights'; -import { CommandRegistryRaw } from '../events/interactionCreate'; +import { CommandRegistryPromisable } from '../events/interactionCreate'; async function render( { region, id }: ResolvedPlayer, @@ -64,9 +64,9 @@ async function render( ); } -export const fullStatsCommand = new Promise( +export const fullStatsCommand = new Promise( async (resolve) => { - const command = await addFilterOptions( + const command = await addPeriodicFilterOptions( new SlashCommandBuilder() .setName('full-stats') .setDescription('Full in-game statistics'), diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index e238e2063..9e344bdf9 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -26,7 +26,7 @@ import addRegionChoices from '../core/discord/addRegionChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; import embedNegative from '../core/discord/embedNegative'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; -import { CommandRegistryRaw } from '../events/interactionCreate'; +import { CommandRegistryPromisable } from '../events/interactionCreate'; export interface RatingsPlayer { spa_id: number; @@ -120,7 +120,7 @@ const noOngoingSeason = embedNegative( "Wargaming didn't provide any data for this season.", ); -export const ratingsCommand = new Promise( +export const ratingsCommand = new Promise( async (resolve) => { const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); const onGoingSeason = await isOnGoingRatingsSeason(); diff --git a/src/commands/stats.tsx b/src/commands/stats.tsx index 3f2e8cf60..bf6146259 100644 --- a/src/commands/stats.tsx +++ b/src/commands/stats.tsx @@ -9,7 +9,7 @@ import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { filtersToDescription } from '../core/blitzkrieg/filtersToDescription'; import { getBlitzStarsLinkButton } from '../core/blitzstars/getBlitzStarsLinkButton'; 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 autocompleteTanks from '../core/discord/autocompleteTanks'; import autocompleteUsername from '../core/discord/autocompleteUsername'; @@ -27,7 +27,7 @@ import resolvePlayerFromCommand, { ResolvedPlayer, } from '../core/discord/resolvePlayerFromCommand'; import { StatFilters, filterStats } from '../core/statistics/filterStats'; -import { CommandRegistryRaw } from '../events/interactionCreate'; +import { CommandRegistryPromisable } from '../events/interactionCreate'; async function render( { region, id }: ResolvedPlayer, @@ -57,48 +57,50 @@ async function render( ); } -export const statsCommand = new Promise(async (resolve) => { - const command = await addFilterOptions( - new SlashCommandBuilder() - .setName('stats') - .setDescription('Regular battles statistics'), - (option) => option.addStringOption(addUsernameChoices), - ); +export const statsCommand = new Promise( + async (resolve) => { + const command = await addPeriodicFilterOptions( + new SlashCommandBuilder() + .setName('stats') + .setDescription('Regular battles statistics'), + (option) => option.addStringOption(addUsernameChoices), + ); - resolve({ - inProduction: true, - inPublic: true, + resolve({ + inProduction: true, + inPublic: true, - command, + command, - async handler(interaction) { - const player = await resolvePlayerFromCommand(interaction); - const period = resolvePeriodFromCommand(player.region, interaction); - const filters = await getFiltersFromCommand(interaction); - const path = commandToURL(interaction, { - ...player, - ...getCustomPeriodParams(interaction), - ...filters, - }); + async handler(interaction) { + const player = await resolvePlayerFromCommand(interaction); + const period = resolvePeriodFromCommand(player.region, interaction); + const filters = await getFiltersFromCommand(interaction); + const path = commandToURL(interaction, { + ...player, + ...getCustomPeriodParams(interaction), + ...filters, + }); - return Promise.all([ - render(player, period, filters), - buttonRefresh(interaction, path), - getBlitzStarsLinkButton(player.region, player.id), - ]); - }, + return Promise.all([ + render(player, period, filters), + buttonRefresh(interaction, path), + getBlitzStarsLinkButton(player.region, player.id), + ]); + }, - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, + autocomplete: (interaction) => { + autocompleteUsername(interaction); + autocompleteTanks(interaction); + }, - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); + async button(interaction) { + const player = await resolvePlayerFromButton(interaction); + const period = resolvePeriodFromButton(player.region, interaction); + const filters = getFiltersFromButton(interaction); - return await render(player, period, filters); - }, - }); -}); + return await render(player, period, filters); + }, + }); + }, +); diff --git a/src/commands/today.ts b/src/commands/today.ts new file mode 100644 index 000000000..81e82405b --- /dev/null +++ b/src/commands/today.ts @@ -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( + 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); + }, + }); + }, +); diff --git a/src/core/discord/addFilterOptions.ts b/src/core/discord/addFilterOptions.ts index 2c45a612d..1f00e9942 100644 --- a/src/core/discord/addFilterOptions.ts +++ b/src/core/discord/addFilterOptions.ts @@ -1,76 +1,60 @@ -import { - SlashCommandBuilder, - SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, -} from 'discord.js'; +import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from 'discord.js'; import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks'; -import { encyclopediaInfo } from '../blitz/encyclopediaInfo'; -import addPeriodSubCommands from './addPeriodSubCommands'; +import { EncyclopediaInfo } from '../blitz/encyclopediaInfo'; import addTankChoices from './addTankChoices'; import addTierChoices from './addTierChoices'; -export default async function addFilterOptions< - OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder, ->( - option: OptionType, - extra: ( - option: SlashCommandSubcommandBuilder, - ) => SlashCommandSubcommandBuilder = (option) => option, -): Promise { - const awaitedEncyclopediaInfo = await encyclopediaInfo; - - addPeriodSubCommands(option, (option) => { - extra(option); - - return option - .addStringOption((option) => - option - .setName('nation') - .setDescription('Nation') - .addChoices( - ...Object.entries(awaitedEncyclopediaInfo.vehicle_nations).map( - ([type, name]) => ({ name, value: type }), - ), - ) - .setRequired(false), - ) - .addStringOption((option) => addTierChoices(option).setRequired(false)) - .addStringOption((option) => - option - .setName('tank-type') - .setDescription('Tank type') - .addChoices( - ...Object.entries(awaitedEncyclopediaInfo.vehicle_types).map( - ([type, name]) => ({ - name: name.replace(' Tank', ''), - value: type, - }), - ), - ) - .setRequired(false), - ) - .addStringOption((option) => - option - .setName('tree-type') - .setDescription('Tech tree type') - .addChoices( - { - name: TREE_TYPE_NAMES['researchable'], - value: 'researchable' satisfies TreeTypeString, - }, - { - name: TREE_TYPE_NAMES.premium, - value: 'premium' satisfies TreeTypeString, - }, - { - name: TREE_TYPE_NAMES.collector, - value: 'collector' satisfies TreeTypeString, - }, - ) - .setRequired(false), - ) - .addStringOption((option) => addTankChoices(option).setRequired(false)); - }); - - return option; +export function addFilterOptions< + Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder, +>(option: Option, encyclopediaInfo: EncyclopediaInfo) { + return option + .addStringOption((option) => + option + .setName('nation') + .setDescription('Nation') + .addChoices( + ...Object.entries(encyclopediaInfo.vehicle_nations).map( + ([type, name]) => ({ name, value: type }), + ), + ) + .setRequired(false), + ) + .addStringOption((option) => addTierChoices(option).setRequired(false)) + .addStringOption((option) => + option + .setName('tank-type') + .setDescription('Tank type') + .addChoices( + ...Object.entries(encyclopediaInfo.vehicle_types).map( + ([type, name]) => ({ + name: name.replace(' Tank', ''), + value: type, + }), + ), + ) + .setRequired(false), + ) + .addStringOption((option) => + option + .setName('tree-type') + .setDescription('Tech tree type') + .addChoices( + { + name: TREE_TYPE_NAMES['researchable'], + value: 'researchable' satisfies TreeTypeString, + }, + { + name: TREE_TYPE_NAMES.premium, + value: 'premium' satisfies TreeTypeString, + }, + { + name: TREE_TYPE_NAMES.collector, + value: 'collector' satisfies TreeTypeString, + }, + ) + .setRequired(false), + ) + .addStringOption((option) => + addTankChoices(option).setRequired(false), + ) as Option; } diff --git a/src/core/discord/addPeriodicFilterOptions.ts b/src/core/discord/addPeriodicFilterOptions.ts new file mode 100644 index 000000000..7840258d3 --- /dev/null +++ b/src/core/discord/addPeriodicFilterOptions.ts @@ -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 { + const awaitedEncyclopediaInfo = await encyclopediaInfo; + + addPeriodSubCommands(option, (option) => { + return addFilterOptions(extra(option), awaitedEncyclopediaInfo); + }); + + return option; +} diff --git a/src/core/discord/getCustomPeriodParams.ts b/src/core/discord/getCustomPeriodParams.ts index 55b7b8aa0..1b35e96e8 100644 --- a/src/core/discord/getCustomPeriodParams.ts +++ b/src/core/discord/getCustomPeriodParams.ts @@ -3,12 +3,17 @@ import { PeriodType } from './addPeriodSubCommands'; export function getCustomPeriodParams( interaction: ChatInputCommandInteraction, + forcedNonCustomPeriod = false, ) { - if ((interaction.options.getSubcommand() as PeriodType) === 'custom') + if ( + !forcedNonCustomPeriod && + (interaction.options.getSubcommand() as PeriodType) === 'custom' + ) { return { start: interaction.options.getInteger('start', true), end: interaction.options.getInteger('end', true), }; + } return undefined; } diff --git a/src/core/discord/resolvePeriodFromCommand.ts b/src/core/discord/resolvePeriodFromCommand.ts index a09da237d..fc09987aa 100644 --- a/src/core/discord/resolvePeriodFromCommand.ts +++ b/src/core/discord/resolvePeriodFromCommand.ts @@ -20,11 +20,14 @@ export interface ResolvedPeriod { export default function resolvePeriodFromCommand( region: Region, interaction: ChatInputCommandInteraction, + forcedPeriod?: PeriodType, ) { let name: string; let start: number; let end: number; - const periodSubcommand = interaction.options.getSubcommand() as PeriodType; + const periodSubcommand = forcedPeriod + ? forcedPeriod + : (interaction.options.getSubcommand() as PeriodType); if (periodSubcommand === 'custom') { const startOption = interaction.options.getInteger('start', true); diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index adb323b07..7d2699725 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -19,7 +19,8 @@ import { psa } from '../../../core/discord/psa'; export default async function handleChatInputCommand( interaction: ChatInputCommandInteraction, ) { - const registry = (await commands)[interaction.commandName]; + const awaitedCommands = await commands; + const registry = awaitedCommands[interaction.commandName]; await interaction.deferReply(); diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 0b7aeadcb..3e7393199 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -31,6 +31,7 @@ import { searchClansCommand } from '../../commands/searchClans'; import { searchPlayersCommand } from '../../commands/searchPlayers'; import { searchTanksCommand } from '../../commands/searchTanks'; import { statsCommand } from '../../commands/stats'; +import { todayCommand } from '../../commands/today'; import getClientId from '../../core/blitzkrieg/getClientId'; import isDev from '../../core/blitzkrieg/isDev'; import { secrets } from '../../core/blitzkrieg/secrets'; @@ -52,34 +53,39 @@ export type InteractionReturnable = | InteractionIterableReturnable | Promise; -export interface Registry { - inProduction: boolean; -} - -export type CommandRegistryRaw = Registry & { - inPublic: boolean; - inPreview?: boolean; - +type CommandRegistryBase = { command: | SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder | Omit; +}; +type CommandRegistryDefinitionBase = { + inProduction: boolean; + inPublic: boolean; + inPreview?: boolean; autocomplete?: (interaction: AutocompleteInteraction) => void; button?: (interaction: ButtonInteraction) => InteractionReturnable; -} & ( - | { - handlesInteraction: true; - handler: (interaction: ChatInputCommandInteraction) => void; - } - | { - handlesInteraction?: false; - handler: ( - interaction: ChatInputCommandInteraction, - ) => InteractionReturnable; - } - ); -export type CommandRegistry = CommandRegistryRaw | Promise; +}; +type CommandRegistryDefinitionHandlesInteraction = { + handlesInteraction: true; + handler: (interaction: ChatInputCommandInteraction) => void; +}; +type CommandRegistryDefinitionDoesHandleInteraction = { + handlesInteraction?: false; + handler: ( + interaction: ChatInputCommandInteraction, + ) => InteractionReturnable; +}; +export type CommandRegistryPromisable = CommandRegistryBase & + (CommandRegistryDefinitionBase & + ( + | CommandRegistryDefinitionHandlesInteraction + | CommandRegistryDefinitionDoesHandleInteraction + )); +export type CommandRegistry = + | CommandRegistryPromisable + | Promise; const rest = new REST().setToken(secrets.DISCORD_TOKEN); @@ -101,10 +107,11 @@ export const COMMANDS_RAW: CommandRegistry[] = [ evolutionCommand, statsCommand, ratingsCommand, + todayCommand, ]; export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) => - rawCommands.reduce>( + rawCommands.reduce>( (accumulator, registry) => { if (isDev()) registry.command.setDefaultMemberPermissions(0); @@ -123,7 +130,7 @@ export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; commands.then((awaitedCommands) => { - Object.entries(awaitedCommands).forEach(([, registry]) => { + Object.values(awaitedCommands).forEach((registry) => { const json = registry.command.toJSON(); if (registry.inPublic) { From 8cf50126021f62dd6d239e22c7bc415377d06185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:35:46 -0600 Subject: [PATCH 13/27] feat: full region names in /link --- src/commands/link.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/link.tsx b/src/commands/link.tsx index 1e44da607..46110c6ed 100644 --- a/src/commands/link.tsx +++ b/src/commands/link.tsx @@ -4,7 +4,7 @@ import markdownEscape from 'markdown-escape'; import discord from '../../discord.json' assert { type: 'json' }; import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow'; import Wrapper from '../components/Wrapper'; -import { REGION_NAMES_SHORT } from '../constants/regions'; +import { REGION_NAMES } from '../constants/regions'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz'; @@ -188,7 +188,7 @@ export const verifyCommand: CommandRegistry = { {clanAccountInfo?.clan ? `[${clanAccountInfo?.clan?.tag}] - ` : ''} - {REGION_NAMES_SHORT[region]} + {REGION_NAMES[region]}
From 8f4e33fd5a2f2fff256c185cfa0274f498784e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:45:23 -0600 Subject: [PATCH 14/27] feat: changelogs --- docs/changelogs/1.15.10.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/changelogs/1.15.10.md diff --git a/docs/changelogs/1.15.10.md b/docs/changelogs/1.15.10.md new file mode 100644 index 000000000..9d2f3753a --- /dev/null +++ b/docs/changelogs/1.15.10.md @@ -0,0 +1,22 @@ +# 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 From c91ab29a234f67e051df19dd1836d08e547d4751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:46:36 -0600 Subject: [PATCH 15/27] feat: disabled tankopedia --- docs/changelogs/1.15.10.md | 1 + src/constants/tools.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/changelogs/1.15.10.md b/docs/changelogs/1.15.10.md index 9d2f3753a..231d0f2f2 100644 --- a/docs/changelogs/1.15.10.md +++ b/docs/changelogs/1.15.10.md @@ -20,3 +20,4 @@ The 10th patch for the `1.15` update is here, breaking the record of the most pa - 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 diff --git a/src/constants/tools.ts b/src/constants/tools.ts index 4211c50a9..a531edd8e 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -20,6 +20,7 @@ export const TOOLS: Tool[] = [ id: 'tankopedia', title: 'Tankopedia', description: 'Blitz tank encyclopedia', + disabled: true, }, { id: 'inactive', From 6094875ffbf5e145ec5449e13cc6c23077cf6e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:52:50 -0600 Subject: [PATCH 16/27] discord tokens... for some reason? --- .github/workflows/cd-ratings-asia.yml | 2 +- .github/workflows/cd-ratings-eu.yml | 2 +- .github/workflows/cd-ratings-na.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-ratings-asia.yml b/.github/workflows/cd-ratings-asia.yml index 1c237bcb2..339680037 100644 --- a/.github/workflows/cd-ratings-asia.yml +++ b/.github/workflows/cd-ratings-asia.yml @@ -35,4 +35,4 @@ jobs: run: yarn - name: Generate leaderboard - run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} + run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=asia --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} diff --git a/.github/workflows/cd-ratings-eu.yml b/.github/workflows/cd-ratings-eu.yml index 1f699cfa4..dec750984 100644 --- a/.github/workflows/cd-ratings-eu.yml +++ b/.github/workflows/cd-ratings-eu.yml @@ -35,4 +35,4 @@ jobs: run: yarn - name: Generate leaderboard - run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} + run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=eu --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} diff --git a/.github/workflows/cd-ratings-na.yml b/.github/workflows/cd-ratings-na.yml index 89b801ce0..12d0b2d55 100644 --- a/.github/workflows/cd-ratings-na.yml +++ b/.github/workflows/cd-ratings-na.yml @@ -35,4 +35,4 @@ jobs: run: yarn - name: Generate leaderboard - run: GH_TOKEN=${{ secrets.GH_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} + run: GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} yarn publish:ratingsleaderboard --region=na --target=${{ github.event.schedule == '0 0 * * *' && 'latest' || 'midnight' }} From 8946f115c0bfa72f5beab9189154afd41670c6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:29:32 -0600 Subject: [PATCH 17/27] feat: no embed errors --- package.json | 2 +- src/commands/ratings.tsx | 4 ++-- src/core/blitz/getTankStats.ts | 3 +-- src/core/blitz/resolveTankId.ts | 7 +++---- src/core/blitzkrieg/userError.ts | 5 +---- src/core/discord/resolveClanFromCommand.ts | 4 ++-- src/core/discord/resolvePlayerFromCommand.ts | 6 ++---- .../handlers/chatInputCommand.ts | 17 +++-------------- 8 files changed, 15 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 59c5e31fd..e33550113 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.15.10", + "version": "1.15.11", "description": "🎉 Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 9e344bdf9..ecbdc1453 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -4,6 +4,7 @@ import { SlashCommandSubcommandBuilder, } from 'discord.js'; import { range } from 'lodash'; +import markdownEscape from 'markdown-escape'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; import * as Leaderboard from '../components/Leaderboard'; import TitleBar from '../components/TitleBar'; @@ -337,8 +338,7 @@ export const ratingsCommand = new Promise( if (playerIndex === -1) { throw new UserError( - `${accountInfo.nickname} didn't play ratings in season ${season}`, - 'This player did not participate in this season or did not get past calibration.', + `# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`, ); } diff --git a/src/core/blitz/getTankStats.ts b/src/core/blitz/getTankStats.ts index 763d720cf..c35df14b5 100644 --- a/src/core/blitz/getTankStats.ts +++ b/src/core/blitz/getTankStats.ts @@ -11,8 +11,7 @@ export default async function getTankStats(region: Region, id: number) { if (tankStats[id] === null) { throw new UserError( - 'No tank stats available', - "This player doesn't have any stats available. This may not be the player you are looking for.", + "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.", ); } diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index 813f5e51a..e175b96fc 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -1,4 +1,5 @@ import { go } from 'fuzzysort'; +import markdownEscape from 'markdown-escape'; import { tankDefinitions, tanksDefinitionsArray, @@ -16,8 +17,7 @@ export default async function resolveTankId(tank: string | number) { if (searchResult.length === 0) { throw new UserError( - 'Tank not found', - `Could not find tank by the name "${tank}".`, + `# Tank not found\nCould not find tank by the name "${markdownEscape(tank)}".`, ); } else { return searchResult[0].obj.id; @@ -27,8 +27,7 @@ export default async function resolveTankId(tank: string | number) { return number; } else { throw new UserError( - 'Tank not found', - `Could not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`, + `# Tank not found\nCould not find tank by the ID "${number}". Try typing the name of the tank instead of a number.`, ); } } diff --git a/src/core/blitzkrieg/userError.ts b/src/core/blitzkrieg/userError.ts index e4eb7cc27..44a647f15 100644 --- a/src/core/blitzkrieg/userError.ts +++ b/src/core/blitzkrieg/userError.ts @@ -2,8 +2,5 @@ * Discriminator for user caused errors to not report in logs */ export class UserError { - constructor( - public message: string, - public cause: string, - ) {} + constructor(public message: string) {} } diff --git a/src/core/discord/resolveClanFromCommand.ts b/src/core/discord/resolveClanFromCommand.ts index f332ca22e..773723449 100644 --- a/src/core/discord/resolveClanFromCommand.ts +++ b/src/core/discord/resolveClanFromCommand.ts @@ -1,4 +1,5 @@ import { ChatInputCommandInteraction } from 'discord.js'; +import markdownEscape from 'markdown-escape'; import { Region } from '../../constants/regions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; import { UserError } from '../blitzkrieg/userError'; @@ -19,8 +20,7 @@ export default async function resolveClanFromCommand( return { region: accounts[0].region, id: accounts[0].clan_id }; } else { throw new UserError( - 'Could not find clan', - `\`${clan}\` was not found. Try selecting a clan from the search result.`, + `# Could not find clan\nCouldn't find "${markdownEscape(clan)}". Try selecting a clan from the search result.`, ); } } diff --git a/src/core/discord/resolvePlayerFromCommand.ts b/src/core/discord/resolvePlayerFromCommand.ts index fa6d156d1..132994f31 100644 --- a/src/core/discord/resolvePlayerFromCommand.ts +++ b/src/core/discord/resolvePlayerFromCommand.ts @@ -35,8 +35,7 @@ export default async function resolvePlayerFromCommand( } satisfies ResolvedPlayer; } else { throw new UserError( - 'Could not find user', - `I couldn't find user "${markdownEscape( + `# Could not find user\nI couldn't find user "${markdownEscape( commandUsername, )}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`, ); @@ -49,8 +48,7 @@ export default async function resolvePlayerFromCommand( return { region: account.region, id: account.blitz }; } else { throw new UserError( - "You're account isn't linked with Blitzkrieg yet", - 'Use the `/link` command to get started.', + "# I don't know who you are\nUse the `/link` command to get started.", ); } } diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index 7d2699725..de02a84cf 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -11,7 +11,6 @@ import { commands } from '..'; import discord from '../../../../discord.json' assert { type: 'json' }; import { UserError } from '../../../core/blitzkrieg/userError'; import buttonLink from '../../../core/discord/buttonLink'; -import embedNegative from '../../../core/discord/embedNegative'; import embedWarning from '../../../core/discord/embedWarning'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import { psa } from '../../../core/discord/psa'; @@ -119,25 +118,15 @@ export default async function handleChatInputCommand( if (error instanceof UserError) { interaction.editReply({ - embeds: [ - embedNegative( - error.message, - (error.cause as string | undefined) ?? - 'No further information is available.', - ), - ], + content: error.message, components, }); } else { console.error(interaction.commandName, error); interaction.editReply({ - embeds: [ - embedNegative( - 'Blitzkrieg ran into an error!', - "We're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", - ), - ], + content: + "# Blitzkrieg ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", components, }); } From e45f055493926198210b2df54115d137e5f0bf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:43:07 -0600 Subject: [PATCH 18/27] feat: permissions command --- docs/guide/permissions.md | 7 ----- src/commands/about.ts | 2 -- src/commands/permissions.ts | 42 +++++++++++++++++++++++++++ src/events/interactionCreate/index.ts | 2 ++ 4 files changed, 44 insertions(+), 9 deletions(-) delete mode 100644 docs/guide/permissions.md create mode 100644 src/commands/permissions.ts diff --git a/docs/guide/permissions.md b/docs/guide/permissions.md deleted file mode 100644 index 5a21542e3..000000000 --- a/docs/guide/permissions.md +++ /dev/null @@ -1,7 +0,0 @@ -# Permissions - -The following Discord permissions are required by the bot to ensure full functionality: - -- Manage nicknames -- Read messages / view channels -- Attach files diff --git a/src/commands/about.ts b/src/commands/about.ts index 94b60fbf0..fd97d6ed7 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -6,7 +6,6 @@ import { COMMANDS_RAW, CommandRegistry } from '../events/interactionCreate'; const RAW_PATH = `https://raw.githubusercontent.com/tresabhi/blitzkrieg/main/docs/`; const DOCS = { - permissions: 'guide/permissions', embeds: 'guide/embeds', introduction: 'guide/introduction', invite: 'guide/invite', @@ -14,7 +13,6 @@ const DOCS = { creators: 'guide/creators', }; const DOC_DESCRIPTIONS: Record = { - permissions: 'The permissions needed for the bot to function', embeds: 'How embeds work', introduction: 'About the bot', invite: 'How to invite the bot', diff --git a/src/commands/permissions.ts b/src/commands/permissions.ts new file mode 100644 index 000000000..b8f29f563 --- /dev/null +++ b/src/commands/permissions.ts @@ -0,0 +1,42 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { CommandRegistry } from '../events/interactionCreate'; + +export const permissionsCommand: CommandRegistry = { + inProduction: true, + inPublic: true, + + command: new SlashCommandBuilder() + .setName('permissions') + .setDescription('Checks wether the bot has the required permissions'), + + async handler(interaction) { + // friday the 13th permissions easter egg + const isFridayThe13th = + new Date().getDay() === 5 && new Date().getDate() === 13; + const permissions = [ + [ + interaction.appPermissions?.has('ViewChannel'), + 'View channels: needed for the refresh button to work', + ], + [ + interaction.appPermissions?.has('ReadMessageHistory'), + 'Read message history: also needed for the refresh button to work', + ], + [ + interaction.appPermissions?.has('AttachFiles'), + 'Attach files: needed for image-based commands to work', + ], + ]; + const body = permissions + .map(([hasPermission, description]) => { + if (hasPermission) { + return `${isFridayThe13th ? '😇' : '✅'} ${description}`; + } else { + return `${isFridayThe13th ? '💀' : '❌'} ${description}`; + } + }) + .join('\n'); + + return `# Permissions\n${body}`; + }, +}; diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 3e7393199..390055141 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -23,6 +23,7 @@ import { fullStatsCommand } from '../../commands/fullStats'; import { inactiveCommand } from '../../commands/inactive'; import { verifyCommand } from '../../commands/link'; import { ownedTanksCommand } from '../../commands/ownedTanks'; +import { permissionsCommand } from '../../commands/permissions'; import { pingCommand } from '../../commands/ping'; import { playerAchievementsCommand } from '../../commands/playerAchievements'; import { playerInfoCommand } from '../../commands/playerInfo'; @@ -90,6 +91,7 @@ export type CommandRegistry = const rest = new REST().setToken(secrets.DISCORD_TOKEN); export const COMMANDS_RAW: CommandRegistry[] = [ + permissionsCommand, debugCommand, eligibleCommand, aboutCommand, From f7fac5ad26788c4407e8db6d1e78d575ab9fefe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:44:58 -0600 Subject: [PATCH 19/27] feat: chagelogs --- docs/changelogs/1.15.11.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/changelogs/1.15.11.md diff --git a/docs/changelogs/1.15.11.md b/docs/changelogs/1.15.11.md new file mode 100644 index 000000000..fdebb263e --- /dev/null +++ b/docs/changelogs/1.15.11.md @@ -0,0 +1,11 @@ +# Blitzkrieg 1.15.11 + +There's an easter egg somewhere in this update 🤔 + +## Features + +- Added `/permissions` which lists all permissions required to run the bot and checks if the bot has them or not + +## Changes + +- Error messages now do no use embeds, instead utilize simple headings and paragraphs From 1d65ff3e0ccb82e458f270debb87925f2df89c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:46:38 -0600 Subject: [PATCH 20/27] fix: type --- src/core/blitz/resolveTankId.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/blitz/resolveTankId.ts b/src/core/blitz/resolveTankId.ts index e175b96fc..d6720847c 100644 --- a/src/core/blitz/resolveTankId.ts +++ b/src/core/blitz/resolveTankId.ts @@ -17,7 +17,7 @@ export default async function resolveTankId(tank: string | number) { if (searchResult.length === 0) { throw new UserError( - `# Tank not found\nCould not find tank by the name "${markdownEscape(tank)}".`, + `# Tank not found\nCould not find tank by the name "${markdownEscape(`${tank}`)}".`, ); } else { return searchResult[0].obj.id; From b52c3c6c6818eeada897e564f66ce55e554b2f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:36:03 -0600 Subject: [PATCH 21/27] feat: updates --- docs/changelogs/1.15.12.md | 8 ++++++++ src/app/embeds/session/page.tsx | 21 +++++++++++++++++++-- src/app/tools/session/page.tsx | 2 +- src/components/Breakdown/components/Row.tsx | 14 +++++++++++--- 4 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 docs/changelogs/1.15.12.md diff --git a/docs/changelogs/1.15.12.md b/docs/changelogs/1.15.12.md new file mode 100644 index 000000000..422d90329 --- /dev/null +++ b/docs/changelogs/1.15.12.md @@ -0,0 +1,8 @@ +# Blitzkrieg 1.15.12 + +Sometimes embeds need love too. + +## Changes + +- Session embeds are now translucent by default + - Session embed titles are now the username of the player by default diff --git a/src/app/embeds/session/page.tsx b/src/app/embeds/session/page.tsx index 2f65393f7..eee4472f9 100644 --- a/src/app/embeds/session/page.tsx +++ b/src/app/embeds/session/page.tsx @@ -14,14 +14,21 @@ import { tankAverages } from '../../../core/blitzstars/tankAverages'; import calculateWN8 from '../../../core/statistics/calculateWN8'; import { deltaTankStats } from '../../../core/statistics/deltaTankStats'; import getWN8Percentile from '../../../core/statistics/getWN8Percentile'; -import { resetSession, useSession } from '../../../stores/session'; +import mutateSession, { + resetSession, + useSession, +} from '../../../stores/session'; import { IndividualTankStats, TanksStats } from '../../../types/tanksStats'; import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn'; import { Menu } from '../../tools/session/components/Menu'; const REFRESH_RATE = 1 / 5; -export default function SessionPage() { +interface SessionPageProps { + naked?: boolean; +} + +export default function SessionPage({ naked = true }: SessionPageProps) { const session = useSession(); const since = session.isTracking ? new Date(session.time) : undefined; const [diff, setDiff] = useState< @@ -72,6 +79,14 @@ export default function SessionPage() { 0, ) / careerBattles; + useEffect(() => { + if (session.title === 'Untitled session') { + mutateSession((draft) => { + if (draft.isTracking) draft.title = draft.nickname; + }); + } + }, [session.title]); + useEffect(() => { async function recalculateDiff() { if (session.isTracking) { @@ -207,6 +222,7 @@ export default function SessionPage() { since !== undefined && diff !== undefined && ( - + ); } diff --git a/src/components/Breakdown/components/Row.tsx b/src/components/Breakdown/components/Row.tsx index 13da1e0f2..246609a47 100644 --- a/src/components/Breakdown/components/Row.tsx +++ b/src/components/Breakdown/components/Row.tsx @@ -18,6 +18,7 @@ export interface RowStatItem { interface RowProps { type?: 'tank' | 'summary'; + naked?: boolean; title: string; minimized?: boolean; treeType?: TreeTypeEnum; @@ -33,18 +34,23 @@ export function Row({ minimized, tankType, treeType, + naked = false, type, }: RowProps) { - const theme = createColors(PALETTES[`${color}Dark`]); + const palette = PALETTES[`${color}Dark`]; + const theme = createColors(palette); return (
@@ -55,7 +61,9 @@ export function Row({ justifyContent: 'center', padding: 8, gap: 4, - backgroundColor: theme.componentInteractive, + backgroundColor: naked + ? `${theme.componentInteractive}c0` + : theme.componentInteractive, }} > {type === 'tank' && tankType !== undefined && ( From 13651650ab4e85ce5efbfa41cba4329fe4af60b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:24:29 -0600 Subject: [PATCH 22/27] fix: delta by time --- package.json | 2 +- src/app/embeds/session/page.tsx | 2 +- src/core/statistics/deltaTankStats.ts | 14 +++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e33550113..0368b0c47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.15.11", + "version": "1.15.13", "description": "🎉 Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", diff --git a/src/app/embeds/session/page.tsx b/src/app/embeds/session/page.tsx index eee4472f9..160de7ac3 100644 --- a/src/app/embeds/session/page.tsx +++ b/src/app/embeds/session/page.tsx @@ -129,7 +129,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) { list: ( Object.values( - deltaTankStats(session.tankStats, career), + deltaTankStats(session.tankStats, career, session.time / 1000), ) as IndividualTankStats[] ).map((stats) => { return { diff --git a/src/core/statistics/deltaTankStats.ts b/src/core/statistics/deltaTankStats.ts index 83c017a43..7480a6971 100644 --- a/src/core/statistics/deltaTankStats.ts +++ b/src/core/statistics/deltaTankStats.ts @@ -4,13 +4,17 @@ import { emptyIndividualTankStats, } from '../../types/tanksStats'; -export function deltaTankStats(a: NormalizedTankStats, b: NormalizedTankStats) { +export function deltaTankStats( + a: NormalizedTankStats, + b: NormalizedTankStats, + time?: number, +) { const bTanks = Object.keys(b); const changedTanks = bTanks.filter((id) => { - return ( - a[id as unknown as number]?.all.battles !== - b[id as unknown as number].all.battles - ); + return time === undefined + ? a[id as unknown as number]?.all.battles !== + b[id as unknown as number].all.battles + : b[id as unknown as number].last_battle_time > time; }); const diff = changedTanks.map((id) => { const aTank = a[id as unknown as number] ?? emptyIndividualTankStats; From d8a5f577876977bf420323edfa489b458e0daf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:26:15 -0600 Subject: [PATCH 23/27] feat: changelogs --- .vscode/settings.json | 1 + docs/changelogs/1.15.13.md | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 docs/changelogs/1.15.13.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 91a3cb534..eb6090c58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,6 +24,7 @@ "fullstats", "fuzzysort", "gamemode", + "haha", "immer", "JOINTINDEX", "JOINTWEIGHT", diff --git a/docs/changelogs/1.15.13.md b/docs/changelogs/1.15.13.md new file mode 100644 index 000000000..cb930846c --- /dev/null +++ b/docs/changelogs/1.15.13.md @@ -0,0 +1,7 @@ +# Blitzkrieg 1.15.13 + +I have run out of things to say here. This patch fixes like 1 thing haha. + +# Technical Fixes + +- Tank deltas are now filtered by last battle time From eaa67ba1fe6864f5ec2f62c298946f244eface49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:29:12 -0600 Subject: [PATCH 24/27] fix: use and --- src/core/statistics/deltaTankStats.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/statistics/deltaTankStats.ts b/src/core/statistics/deltaTankStats.ts index 7480a6971..08306533a 100644 --- a/src/core/statistics/deltaTankStats.ts +++ b/src/core/statistics/deltaTankStats.ts @@ -11,10 +11,13 @@ export function deltaTankStats( ) { const bTanks = Object.keys(b); const changedTanks = bTanks.filter((id) => { - return time === undefined - ? a[id as unknown as number]?.all.battles !== - b[id as unknown as number].all.battles - : b[id as unknown as number].last_battle_time > time; + return ( + a[id as unknown as number]?.all.battles !== + b[id as unknown as number].all.battles && + (time === undefined + ? true + : b[id as unknown as number].last_battle_time > time) + ); }); const diff = changedTanks.map((id) => { const aTank = a[id as unknown as number] ?? emptyIndividualTankStats; From 19783ccbd6149bdb7cbd4f5375fa31f449929a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:22:30 -0600 Subject: [PATCH 25/27] Update page.tsx --- src/app/embeds/session/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/embeds/session/page.tsx b/src/app/embeds/session/page.tsx index 160de7ac3..a39efde21 100644 --- a/src/app/embeds/session/page.tsx +++ b/src/app/embeds/session/page.tsx @@ -245,7 +245,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) { {diff?.list .sort( - (a, b) => b.stats.last_battle_time - a.stats.last_battle_time, + (a, b) => a.stats.last_battle_time - b.stats.last_battle_time, ) .map( ({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => { From eab8bac68cbd9390359bd77cd50fb438986c223d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:56:42 -0600 Subject: [PATCH 26/27] fix: order --- src/app/embeds/session/page.tsx | 2 +- src/core/statistics/deltaTankStats.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/embeds/session/page.tsx b/src/app/embeds/session/page.tsx index a39efde21..160de7ac3 100644 --- a/src/app/embeds/session/page.tsx +++ b/src/app/embeds/session/page.tsx @@ -245,7 +245,7 @@ export default function SessionPage({ naked = true }: SessionPageProps) { {diff?.list .sort( - (a, b) => a.stats.last_battle_time - b.stats.last_battle_time, + (a, b) => b.stats.last_battle_time - a.stats.last_battle_time, ) .map( ({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => { diff --git a/src/core/statistics/deltaTankStats.ts b/src/core/statistics/deltaTankStats.ts index 08306533a..a1996edbe 100644 --- a/src/core/statistics/deltaTankStats.ts +++ b/src/core/statistics/deltaTankStats.ts @@ -31,7 +31,10 @@ export function deltaTankStats( in_garage: bTank.in_garage, max_xp: Math.max(aTank.max_xp, bTank.max_xp), in_garage_updated: bTank.in_garage_updated, - last_battle_time: bTank.last_battle_time - aTank.last_battle_time, + last_battle_time: Math.max( + bTank.last_battle_time, + aTank.last_battle_time, + ), mark_of_mastery: bTank.mark_of_mastery, tank_id: bTank.tank_id, From 7bd8629706870e23cbf45dccc3f684f159867776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Mon, 12 Feb 2024 09:52:37 -0600 Subject: [PATCH 27/27] Add files via upload --- public/google7348c0b85956d981.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/google7348c0b85956d981.html diff --git a/public/google7348c0b85956d981.html b/public/google7348c0b85956d981.html new file mode 100644 index 000000000..4d4a98af4 --- /dev/null +++ b/public/google7348c0b85956d981.html @@ -0,0 +1 @@ +google-site-verification: google7348c0b85956d981.html \ No newline at end of file