Compare commits

...
26 Commits
Author SHA1 Message Date
TrèsAbhi 597a1a04d2 Merge pull request #45 from tresabhi/1.14.3 2023-10-20 11:26:24 -05:00
TrèsAbhi aec6f49ea1 Update 1.14.2.md 2023-10-20 11:26:13 -05:00
TrèsAbhi e1ad710de5 fix: corrected version number 2023-10-20 11:26:03 -05:00
TrèsAbhi edc8447617 feat: changelogs 2023-10-20 11:24:19 -05:00
TrèsAbhi f726d4f8e8 feat: don't manage queue after task completion 2023-10-20 11:12:06 -05:00
TrèsAbhi 1f333f6b88 fix: manage queue after space available 2023-10-20 11:11:41 -05:00
TrèsAbhi 9b855fca47 fix: fetchBlitz return type 2023-10-20 11:08:22 -05:00
TrèsAbhi c5f433fdf4 feat: removed unnesesary type 2023-10-20 10:57:24 -05:00
TrèsAbhi fd61dae1c1 feat: added queues to fetchBlitz 2023-10-20 10:50:54 -05:00
TrèsAbhi 2c45f9759f fix: removed lie about username inference 2023-10-20 10:29:01 -05:00
TrèsAbhi 0d3261f748 feat: named asia region short name "ASIA" 2023-10-20 10:28:41 -05:00
TrèsAbhi d38e873800 feat: used short region names for username autocomplete 2023-10-20 10:28:20 -05:00
TrèsAbhi ca38914095 feat: chunked images in breakdown 2023-10-20 10:22:12 -05:00
TrèsAbhi ee5beeea45 fix: preserve image order 2023-10-20 10:12:01 -05:00
TrèsAbhi b8be916c18 Merge pull request #43 from tresabhi/1.14.2 2023-10-17 22:08:51 -05:00
TrèsAbhi 24d0313fd3 feat: changelogs 2023-10-17 22:08:39 -05:00
TrèsAbhi c3d2a81327 fix: generic stats name overflow 2023-10-17 22:06:50 -05:00
TrèsAbhi dfeb117663 fix: getClanInfo now uses clan_id 2023-10-17 22:04:27 -05:00
TrèsAbhi 495a830506 Merge pull request #42 from tresabhi/1.14.1 2023-10-17 21:14:13 -05:00
TrèsAbhi a730f67ca5 feat: changelogs 2023-10-17 21:14:30 -05:00
TrèsAbhi 9a3867770d feat: reduced render workers to 2 2023-10-17 21:01:26 -05:00
TrèsAbhi f8a66bb5e7 feat: simpler /link message 2023-10-17 20:59:33 -05:00
TrèsAbhi 4b00e1342e feat: user error discrimination 2023-10-17 20:58:36 -05:00
TrèsAbhi f9e0584f00 fix: memory leak in render threads 2023-10-17 20:45:45 -05:00
TrèsAbhi 900c290e75 Update cd-host.yml 2023-10-16 20:50:40 -05:00
TrèsAbhi 69e1577d18 fix: prisma copy over 2023-10-16 20:44:47 -05:00
36 changed files with 277 additions and 147 deletions

No files matched your search

+1 -1
View File
@@ -29,4 +29,4 @@ jobs:
unzip bot.zip unzip bot.zip
- name: Run bot - name: Run bot
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} node --enable-source-maps index.cjs run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} DISCORD_BLITZ_DB=${{ secrets.DISCORD_BLITZ_DB }} node --enable-source-maps index.cjs
+1
View File
@@ -15,6 +15,7 @@
"kranvagn", "kranvagn",
"Leaderboard", "Leaderboard",
"leaderboards", "leaderboards",
"Oopsy",
"outdir", "outdir",
"outfile", "outfile",
"ownedtanks", "ownedtanks",
+17
View File
@@ -0,0 +1,17 @@
# Blitzkrieg 1.14.1
Oopsy daisy, a memory leak!
## Changes
- Simplified message for users to use `/link`
## Fixes
- Fixed a memory leak in the render thread manager
## Technical changes
- Added `UserError` extending `Error`
- This lets the bot avoid spamming the debug console for mistakes users make
- Reduced render threads to 2
+8
View File
@@ -0,0 +1,8 @@
# Blitzkrieg 1.14.2
A late night hot fix for my homies
## Fixes
- Clan info is now fetched using `clan_id` instead of `account_id`
- Generic stats row names are now bounded by a max width
+24
View File
@@ -0,0 +1,24 @@
# Blitzkrieg 1.14.3
Pull your brush out and start polishing.
## Changes
![HisRoyalFatness' career breakdown](https://i.imgur.com/GhgMTnS.png)
- Rows in `/breakdown` are now chunked into `8` per image
- This is true unless the number of pages exceeds `10` in which case the rows are even distributed across all `10` pages
- `10` is the maximum number of images that Discord allows
- Short region names are used in username autocomplete instead of their full names
- Example: "North America" is "NA" now
## Fixes
- Removed reference to username inference in the username option description
- Removed an unnecessary type `asd` that did absolutely nothing used nowhere
## Technical changes
- Image orders are now preserved even if processed in parallel and all completed out of order
- `fetchBlitz` is now throttled to `10` requests per second
- Request overflows are managed by queueing
+17 -9
View File
@@ -1,17 +1,16 @@
import { build } from 'esbuild'; import { build } from 'esbuild';
import { readdir } from 'fs/promises'; import { cp, readdir } from 'fs/promises';
const FILES = [ readdir('src/workers').then((workers) => {
const files = [
'bot', 'bot',
'index', 'index',
(await readdir('src/workers')).map( ...workers.map((file) => `workers/${file.replace('.ts', '')}`),
(file) => `workers/${file.replace('.ts', '')}`, ];
),
];
await build({ build({
entryPoints: FILES.map((file) => `src/${file}.ts`), entryPoints: files.map((file) => `src/${file}.ts`),
outdir: 'dist/bot', outdir: 'dist/bot',
platform: 'node', platform: 'node',
@@ -28,9 +27,18 @@ await build({
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
minifyIdentifiers: false, // cause goofy errors minifyIdentifiers: false, // causes goofy errors
minifySyntax: true, minifySyntax: true,
minifyWhitespace: true, minifyWhitespace: true,
logLevel: 'info', logLevel: 'info',
});
}); });
// copy schema.prisma
cp('prisma/schema.prisma', 'dist/bot/schema.prisma');
readdir('node_modules/prisma').then((files) =>
files
.filter((file) => file.endsWith('.node'))
.map((file) => cp(`node_modules/prisma/${file}`, `dist/bot/${file}`)),
);
+1 -1
View File
@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import * as Breakdown from '../../../components/Breakdown'; import * as Breakdown from '../../../components/Breakdown';
import { TreeTypeEnum } from '../../../components/Tanks'; import { TreeTypeEnum } from '../../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchWargaming'; import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { tankAverages } from '../../../core/blitzstars/tankAverages'; import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { import {
TankopediaEntry, TankopediaEntry,
+65 -35
View File
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { chunk } from 'lodash';
import * as Breakdown from '../components/Breakdown'; import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
@@ -35,6 +36,9 @@ 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 { CommandRegistryRaw } from '../events/interactionCreate';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 10;
async function render( async function render(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
@@ -132,23 +136,10 @@ async function render(
0, 0,
) / careerBattles; ) / careerBattles;
return ( const children: JSX.Element[] = [];
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
{filteredOrder.length === 0 && ( if (filteredOrder.length > 0) {
<NoData type={NoDataType.BattlesInPeriod} /> children.push(
)}
{filteredOrder.length > 0 && (
<Breakdown.Root>
<Breakdown.Row <Breakdown.Row
key={id} key={id}
type="summary" type="summary"
@@ -187,9 +178,10 @@ async function render(
delta: currentDamage - careerDamage, delta: currentDamage - careerDamage,
}, },
]} ]}
/> />,
);
{await Promise.all( await Promise.all(
filteredOrder.map(async (id, index) => { filteredOrder.map(async (id, index) => {
const tankopediaEntry = awaitedTankopedia[id]; const tankopediaEntry = awaitedTankopedia[id];
const current = orderedCurrentStats[index]; const current = orderedCurrentStats[index];
@@ -197,7 +189,7 @@ async function render(
const currentWN8 = orderedCurrentWN8[index]; const currentWN8 = orderedCurrentWN8[index];
const careerWN8 = orderedCareerWN8[index]; const careerWN8 = orderedCareerWN8[index];
return ( children.push(
<Breakdown.Row <Breakdown.Row
key={id} key={id}
type="tank" type="tank"
@@ -212,16 +204,12 @@ async function render(
}, },
{ {
title: 'Winrate', title: 'Winrate',
current: `${( current: `${(100 * (current.wins / current.battles)).toFixed(
100 *
(current.wins / current.battles)
).toFixed(2)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(
2, 2,
)}%`, )}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta: delta:
current.wins / current.battles - current.wins / current.battles - career.wins / career.battles,
career.wins / career.battles,
}, },
{ {
title: 'WN8', title: 'WN8',
@@ -251,14 +239,56 @@ async function render(
career.damage_dealt / career.battles, career.damage_dealt / career.battles,
}, },
]} ]}
/> />,
); );
}), }),
)}
</Breakdown.Root>
)}
</Wrapper>
); );
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
return pages
.map((page, index) => (
<Wrapper>
{index === 0 && (
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
)}
<Breakdown.Root>{page}</Breakdown.Root>
</Wrapper>
))
.reverse();
} else {
return [
<Wrapper>
<TitleBar
name={accountInfo.nickname}
image={
clanData?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData?.clan?.emblem_set_id}.png`
: undefined
}
description={`${name} • ${filterDescriptions}`}
/>
{filteredOrder.length === 0 && (
<NoData type={NoDataType.BattlesInPeriod} />
)}
</Wrapper>,
];
}
} }
export const breakdownCommand = new Promise<CommandRegistryRaw>( export const breakdownCommand = new Promise<CommandRegistryRaw>(
@@ -286,11 +316,11 @@ export const breakdownCommand = new Promise<CommandRegistryRaw>(
...filters, ...filters,
}); });
return Promise.all([ return [
render(player, period, filters), ...(await render(player, period, filters)),
buttonPrimary(path, 'Refresh'), buttonPrimary(path, 'Refresh'),
getBlitzStarsLinkButton(player.region, player.id), await getBlitzStarsLinkButton(player.region, player.id),
]); ];
}, },
autocomplete: (interaction) => { autocomplete: (interaction) => {
+9 -6
View File
@@ -18,7 +18,7 @@ import getMidnightLeaderboard, {
DATABASE_REPO, DATABASE_REPO,
} from '../core/blitzkrieg/getMidnightLeaderboard'; } from '../core/blitzkrieg/getMidnightLeaderboard';
import { octokit } from '../core/blitzkrieg/octokit'; import { octokit } from '../core/blitzkrieg/octokit';
import throwError from '../core/blitzkrieg/throwError'; 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 addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
@@ -185,7 +185,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
}) })
.then(({ data }) => { .then(({ data }) => {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw throwError('Archived ratings data is malformed'); throw new Error('Archived ratings data is malformed');
} }
return data.map( return data.map(
(folder) => (folder) =>
@@ -292,7 +292,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
}) })
.then(async ({ data }) => { .then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') { if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings data is malformed'); throw new Error('Archived ratings data is malformed');
} }
const players = (await fetch(data.download_url!).then( const players = (await fetch(data.download_url!).then(
@@ -361,7 +361,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
}) })
.then(async ({ data }) => { .then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') { if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings info is malformed'); throw new Error('Archived ratings info is malformed');
} }
const response = fetch(data.download_url!); const response = fetch(data.download_url!);
@@ -404,7 +404,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
}) })
.then(async ({ data }) => { .then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') { if (Array.isArray(data) || data.type !== 'file') {
throw throwError( throw new Error(
'Archived ratings latest data is malformed', 'Archived ratings latest data is malformed',
); );
} }
@@ -418,9 +418,12 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
); );
if (playerIndex === -1) { if (playerIndex === -1) {
throw throwError( throw new UserError(
`${accountInfo.nickname} didn't player ratings in season ${season}`, `${accountInfo.nickname} didn't player 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.',
},
); );
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions'; import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming'; import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
+1 -1
View File
@@ -2,7 +2,7 @@ import { SlashCommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import { REGION_NAMES, Region } from '../constants/regions'; import { REGION_NAMES, Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming'; import fetchBlitz from '../core/blitz/fetchBlitz';
import { import {
AccountList, AccountList,
usernamePattern, usernamePattern,
@@ -15,9 +15,18 @@ export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
alignItems: 'center', alignItems: 'center',
}} }}
> >
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}> <span
style={{
color: theme.colors.textLowContrast,
fontSize: 16,
whiteSpace: 'nowrap',
maxWidth: 128,
overflow: 'hidden',
}}
>
{stat[0]} {stat[0]}
</span> </span>
<span <span
style={{ style={{
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
+1 -1
View File
@@ -11,7 +11,7 @@ export const REGION_NAMES: Record<Region, string> = {
export const REGION_NAMES_SHORT: Record<Region, string> = { export const REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA', com: 'NA',
eu: 'EU', eu: 'EU',
asia: 'AS', asia: 'ASIA',
}; };
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = { export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
+52
View File
@@ -0,0 +1,52 @@
export type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
const MAX_CALLS_PER_SECOND = 10;
const queue: { url: string; resolve: (data: object) => void }[] = [];
let inProgress = 0;
async function manageQueue() {
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
inProgress++;
setTimeout(() => {
inProgress--;
manageQueue();
}, 1000);
const { url, resolve } = queue.shift()!;
const data = (await fetch(url)
.then((response) => response.json())
.catch((error) => ({ status: 'error', error }))) as BlitzResponse<
typeof resolve
>;
if (data.status === 'ok') {
resolve(data.data);
} else {
throw new Error(`Wargaming response error status:"${data.status}"`, {
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
});
}
}
}
export default function fetchBlitz<Data extends object>(url: string) {
return new Promise<Data>((resolve) => {
queue.push({
url,
resolve(data) {
resolve(data as Data);
},
});
manageQueue();
});
}
-26
View File
@@ -1,26 +0,0 @@
import throwError from '../blitzkrieg/throwError';
export type BlitzResponse<Data extends object> =
| {
status: 'error';
error: { field: string; message: string; code: 402; value: null };
}
| {
status: 'ok';
data: Data;
};
export default async function fetchBlitz<Data extends object>(url: string) {
const data = (await fetch(url).then((response) =>
response.json(),
)) as BlitzResponse<Data>;
if (data.status === 'ok') {
return data.data;
} else {
throw throwError(
`Wargaming response error status:"${data.status}"`,
`Message: "${data.error.message}"\nURL: "${url}"`,
);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
interface IndividualAccountAchievements { interface IndividualAccountAchievements {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
export interface AllStats { export interface AllStats {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeExtra } from './normalizeExtra'; import { normalizeExtra } from './normalizeExtra';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
+2 -2
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
import { normalizeIds } from './normalizeIds'; import { normalizeIds } from './normalizeIds';
export interface IndividualClanInfo { export interface IndividualClanInfo {
@@ -41,7 +41,7 @@ export async function getClanInfo<
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[], ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
>(region: Region, ids: Ids) { >(region: Region, ids: Ids) {
const object = await fetchBlitz<ClanInfo>( const object = await fetchBlitz<ClanInfo>(
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&account_id=${normalizeIds( `https://api.wotblitz.${region}/wotb/clans/info/?application_id=${WARGAMING_APPLICATION_ID}&clan_id=${normalizeIds(
ids, ids,
)}`, )}`,
); );
+5 -5
View File
@@ -1,8 +1,8 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { TanksStats } from '../../types/tanksStats'; import { TanksStats } from '../../types/tanksStats';
import throwError from '../blitzkrieg/throwError'; import { UserError } from '../blitzkrieg/userError';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
export default async function getTankStats(region: Region, id: number) { export default async function getTankStats(region: Region, id: number) {
const tankStats = await fetchBlitz<TanksStats>( const tankStats = await fetchBlitz<TanksStats>(
@@ -10,10 +10,10 @@ export default async function getTankStats(region: Region, id: number) {
); );
if (tankStats[id] === null) { if (tankStats[id] === null) {
throw throwError( throw new UserError('No tank stats available', {
'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.", "This player doesn't have any stats for tanks available. This may not be the player you are looking for.",
); });
} }
return tankStats[id]; return tankStats[id];
+7 -9
View File
@@ -1,5 +1,5 @@
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import throwError from '../blitzkrieg/throwError'; import { UserError } from '../blitzkrieg/userError';
import { TANKS, tankopedia } from '../blitzstars/tankopedia'; import { TANKS, tankopedia } from '../blitzstars/tankopedia';
export default async function resolveTankId(tank: string | number) { export default async function resolveTankId(tank: string | number) {
@@ -12,10 +12,9 @@ export default async function resolveTankId(tank: string | number) {
}); });
if (searchResult.length === 0) { if (searchResult.length === 0) {
throw throwError( throw new UserError('Tank not found', {
'Tank not found', cause: `Could not find tank by the name "${tank}".`,
`Could not find tank by the name "${tank}".`, });
);
} else { } else {
return searchResult[0].obj.tank_id; return searchResult[0].obj.tank_id;
} }
@@ -23,10 +22,9 @@ export default async function resolveTankId(tank: string | number) {
if ((await tankopedia)[number]) { if ((await tankopedia)[number]) {
return number; return number;
} else { } else {
throw throwError( throw new UserError('Tank not found', {
'Tank not found', cause: `Could not find tank by the ID "${number}".`,
`Could not find tank by the ID "${number}".`, });
);
} }
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { Clan, ClanList } from '../../types/clanList'; import { Clan, ClanList } from '../../types/clanList';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
export type ClanListWithRegion = (Clan & { export type ClanListWithRegion = (Clan & {
region: 'com' | 'eu' | 'asia'; region: 'com' | 'eu' | 'asia';
+1 -1
View File
@@ -1,6 +1,6 @@
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from './fetchWargaming'; import fetchBlitz from './fetchBlitz';
interface Account { interface Account {
nickname: string; nickname: string;
@@ -2,7 +2,6 @@ import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { DATABASE_REPO } from './getMidnightLeaderboard'; import { DATABASE_REPO } from './getMidnightLeaderboard';
import { octokit } from './octokit'; import { octokit } from './octokit';
import throwError from './throwError';
const ARCHIVED_RATINGS_CACHE: Record< const ARCHIVED_RATINGS_CACHE: Record<
Region, Region,
@@ -32,7 +31,7 @@ export default async function getArchivedRatingsInfo(
}); });
if (Array.isArray(data) || data.type !== 'file') if (Array.isArray(data) || data.type !== 'file')
throw throwError('Archived ratings info is malformed'); throw new Error('Archived ratings info is malformed');
const content = Buffer.from(data.content, 'base64').toString(); const content = Buffer.from(data.content, 'base64').toString();
const jsonContent = JSON.parse(content) as RatingsInfo & { const jsonContent = JSON.parse(content) as RatingsInfo & {
+13 -9
View File
@@ -1,6 +1,6 @@
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
const MAX_WORKERS = 3; const MAX_WORKERS = 2;
const workers = [...Array(MAX_WORKERS)].map(() => { const workers = [...Array(MAX_WORKERS)].map(() => {
const worker = new Worker(`${__dirname}/workers/render.cjs`); const worker = new Worker(`${__dirname}/workers/render.cjs`);
@@ -19,18 +19,22 @@ function manageQueue() {
function cleanup() { function cleanup() {
workers.push(worker); workers.push(worker);
worker.off('message', handleMessage);
worker.off('error', handleError);
manageQueue(); manageQueue();
} }
function handleMessage(png: Uint8Array) {
cleanup();
resolve(Buffer.from(png));
}
function handleError(error: Error) {
cleanup();
throw error;
}
worker.postMessage(svg); worker.postMessage(svg);
worker.on('message', (png: Uint8Array) => { worker.on('message', handleMessage);
cleanup(); worker.on('error', handleError);
resolve(Buffer.from(png));
});
worker.on('error', (error) => {
cleanup();
throw error;
});
} }
} }
-3
View File
@@ -1,3 +0,0 @@
export default function throwError(message: string, cause?: string) {
return new Error(message, { cause });
}
+4
View File
@@ -0,0 +1,4 @@
/**
* Discriminator for user caused errors to not report in logs
*/
export class UserError extends Error {}
+1 -3
View File
@@ -1,7 +1,6 @@
import { deburr } from 'lodash'; import { deburr } from 'lodash';
import { TreeTypeString } from '../../components/Tanks';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import fetchBlitz from '../blitz/fetchWargaming'; import fetchBlitz from '../blitz/fetchBlitz';
import { context } from '../blitzkrieg/context'; import { context } from '../blitzkrieg/context';
export interface TankopediaEntry { export interface TankopediaEntry {
@@ -16,7 +15,6 @@ export interface TankopediaEntry {
type: string; type: string;
description: string; description: string;
} }
type asd = TreeTypeString;
export interface Tankopedia { export interface Tankopedia {
[id: number]: TankopediaEntry | undefined; [id: number]: TankopediaEntry | undefined;
} }
+1 -3
View File
@@ -3,8 +3,6 @@ import { SlashCommandStringOption } from 'discord.js';
export default function addUsernameChoices(option: SlashCommandStringOption) { export default function addUsernameChoices(option: SlashCommandStringOption) {
return option return option
.setName('username') .setName('username')
.setDescription( .setDescription('The username you use in Blitz')
'The username you use in Blitz (inferred from Discord nickname by default)',
)
.setAutocomplete(true); .setAutocomplete(true);
} }
+4 -2
View File
@@ -3,7 +3,7 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_NAMES } from '../../constants/regions'; import { REGION_NAMES_SHORT } from '../../constants/regions';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
export default async function autocompleteUsername( export default async function autocompleteUsername(
@@ -19,7 +19,9 @@ export default async function autocompleteUsername(
? players.map( ? players.map(
(player) => (player) =>
({ ({
name: `${player.nickname} (${REGION_NAMES[player.region]})`, name: `${player.nickname} (${
REGION_NAMES_SHORT[player.region]
})`,
value: `${player.region}/${player.account_id}`, value: `${player.region}/${player.account_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
) )
@@ -11,6 +11,7 @@ import jsxToPngThreaded from '../blitzkrieg/jsxToPngThreaded';
export default async function normalizeInteractionReturnable( export default async function normalizeInteractionReturnable(
returnable: InteractionReturnable, returnable: InteractionReturnable,
) { ) {
const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions = {}; const reply: InteractionEditReplyOptions = {};
const awaitedReturnable = await returnable; const awaitedReturnable = await returnable;
@@ -19,7 +20,7 @@ export default async function normalizeInteractionReturnable(
: [awaitedReturnable]; : [awaitedReturnable];
await Promise.all( await Promise.all(
normalizedReturnable.map(async (item) => { normalizedReturnable.map(async (item, index) => {
if (typeof item === 'string') { if (typeof item === 'string') {
reply.content = item; reply.content = item;
} else if (item instanceof EmbedBuilder) { } else if (item instanceof EmbedBuilder) {
@@ -38,11 +39,16 @@ export default async function normalizeInteractionReturnable(
return; return;
} else { } else {
const image = await jsxToPngThreaded(item); const image = await jsxToPngThreaded(item);
if (!reply.files) reply.files = []; images.push([index, image]);
reply.files.push(image);
} }
}), }),
); );
if (images.length > 0) {
images.sort(([a], [b]) => b - a);
if (!reply.files) reply.files = [];
images.forEach(([, image]) => reply.files?.push(image));
}
return reply; return reply;
} }
+4 -5
View File
@@ -1,7 +1,7 @@
import { ChatInputCommandInteraction } from 'discord.js'; import { ChatInputCommandInteraction } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
import throwError from '../blitzkrieg/throwError'; import { UserError } from '../blitzkrieg/userError';
import { serverAndIdPattern } from './resolvePlayerFromCommand'; import { serverAndIdPattern } from './resolvePlayerFromCommand';
export default async function resolveClanFromCommand( export default async function resolveClanFromCommand(
@@ -18,10 +18,9 @@ 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 throwError( throw new UserError('Could not find clan', {
'Could not find clan', cause: `I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`, });
);
} }
} }
} }
+7 -9
View File
@@ -3,7 +3,7 @@ import markdownEscape from 'markdown-escape';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz'; import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz';
import throwError from '../blitzkrieg/throwError'; import { UserError } from '../blitzkrieg/userError';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
@@ -34,12 +34,11 @@ export default async function resolvePlayerFromCommand(
id: accounts[0].account_id, id: accounts[0].account_id,
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
throw throwError( throw new UserError('Could not find user', {
'Could not find user', cause: `I couldn't find user "${markdownEscape(
`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 {
@@ -48,10 +47,9 @@ 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 throwError( throw new UserError('Use the `/link` command', {
"You're not linked", cause: 'Link your Blitz and Discord accounts to get started.',
'Use the `/link` command to link your Discord and Blitz accounts.', });
);
} }
} }
} }
@@ -1,6 +1,5 @@
import { ButtonInteraction, CacheType } from 'discord.js'; import { ButtonInteraction, CacheType } from 'discord.js';
import { commands } from '..'; import { commands } from '..';
import throwError from '../../../core/blitzkrieg/throwError';
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
export default async function handleButton( export default async function handleButton(
@@ -13,8 +12,7 @@ export default async function handleButton(
const button = (await commands)[commandName]?.button; const button = (await commands)[commandName]?.button;
if (!button) { if (!button) {
throwError(`Button handler not found for "${commandName}"`); throw new Error(`Button handler not found for "${commandName}"`);
return;
} }
if (interaction.message.interaction?.user.id !== interaction.user.id) { if (interaction.message.interaction?.user.id !== interaction.user.id) {
@@ -7,10 +7,11 @@ import {
} from 'discord.js'; } from 'discord.js';
import { commands } from '..'; import { commands } from '..';
import discord from '../../../../discord.json' assert { type: 'json' }; import discord from '../../../../discord.json' assert { type: 'json' };
import { psa } from '../../../core/discord/psa'; import { UserError } from '../../../core/blitzkrieg/userError';
import embedNegative from '../../../core/discord/embedNegative'; import embedNegative from '../../../core/discord/embedNegative';
import embedWarning from '../../../core/discord/embedWarning'; import embedWarning from '../../../core/discord/embedWarning';
import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable'; import normalizeInteractionReturnable from '../../../core/discord/normalizeInteractionReturnable';
import { psa } from '../../../core/discord/psa';
export default async function handleChatInputCommand( export default async function handleChatInputCommand(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
@@ -53,7 +54,9 @@ export default async function handleChatInputCommand(
.setStyle(ButtonStyle.Link), .setStyle(ButtonStyle.Link),
); );
if (!(error instanceof UserError)) {
console.error(interaction.commandName, error); console.error(interaction.commandName, error);
}
await interaction.editReply({ await interaction.editReply({
embeds: [ embeds: [
+1 -1
View File
@@ -4,7 +4,7 @@ import { devtools, persist } from 'zustand/middleware';
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn'; import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
import { Region } from '../constants/regions'; import { Region } from '../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchWargaming'; import fetchBlitz from '../core/blitz/fetchBlitz';
import { NormalizedTankStats, TanksStats } from '../types/tanksStats'; import { NormalizedTankStats, TanksStats } from '../types/tanksStats';
type Session = ( type Session = (