Compare commits

...
13 Commits
14 changed files with 127 additions and 89 deletions

No files matched your search

+2
View File
@@ -13,9 +13,11 @@
"Everytime", "Everytime",
"fullstats", "fullstats",
"fuzzysort", "fuzzysort",
"immer",
"kranvagn", "kranvagn",
"Leaderboard", "Leaderboard",
"leaderboards", "leaderboards",
"MURICA",
"Oopsy", "Oopsy",
"outdir", "outdir",
"outfile", "outfile",
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
themeConfig: { themeConfig: {
nav: [ nav: [
{ text: 'Home', link: '/' }, { text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/about' }, { text: 'Guide', link: '/guide/introduction' },
], ],
sidebar: [ sidebar: [
+24
View File
@@ -0,0 +1,24 @@
# Blitzkrieg 1.15.1
'MURICA!
## Changes
- "Neighbor" is now spelled in the American way
- Updated documentation website with the latest information
## Fixes
- Fixed the guide link on the documentation website
## Technical features
- Added capability to fetch patiently while also parsing the JSON
## Technical changes
- Removed neighbor relative position caching
## Technical fixes
- `/ratings` now uses the non-caching APIs
+9
View File
@@ -0,0 +1,9 @@
# Blitzkrieg 1.15.2
Infinity reduced to a finite value.
## Fixes
- Added a maximum value to the jump to position button
- Removed neighbor checking for the seeding player
- This fixes the jump to position button not loading at all
-17
View File
@@ -1,17 +0,0 @@
# Installing Blitzkrieg
It's easy as 1, 2, 3!
## Step 1
Join a server that already has Blitzkrieg or [join our official Discord server](https://discord.gg/nDt7AjGJQH).
## Step 2
View the bot's profile and click "Add to Server".
<video src="https://i.imgur.com/Zq0P3m2.mp4" autoplay loop muted width=320 title="how to install"></video>
## Step 3
Review the permissions the bot is asking for and add it to your server!
+13 -1
View File
@@ -1,6 +1,10 @@
# Introduction # Introduction
Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blitz. 🎉 Services for everything and anything World of Tanks Blitz
## Bot
Blitzkrieg is available as a Discord bot and can be [installed very easily](/guide/invite).
- Track statistics - Track statistics
- Find inactive players in a clan - Find inactive players in a clan
@@ -8,3 +12,11 @@ Blitzkrieg is a multipurpose bot for anything and everything World of Tanks Blit
- Create embeds for streams - Create embeds for streams
- Track a player/tanks statistical evolution - Track a player/tanks statistical evolution
- ...and a variety of other similar things - ...and a variety of other similar things
## Website
Provides a bunch of very helpful tools; https://blitz-krieg.vercel.app/
- Session tracker
- Ratings leaderboard
- Way more to come
+2 -2
View File
@@ -12,10 +12,10 @@ hero:
actions: actions:
- theme: brand - theme: brand
text: Add to Discord Server text: Add to Discord Server
link: guide/install link: /guide/invite
- theme: alt - theme: alt
text: Guide text: Guide
link: /guide/about link: /guide/introduction
features: features:
- title: Clean and Efficient UI - title: Clean and Efficient UI
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.15.0", "version": "1.15.2",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
+8 -13
View File
@@ -6,7 +6,7 @@ import {
RatingsNeighbors, RatingsNeighbors,
RatingsPlayer, RatingsPlayer,
} from '../src/commands/ratings'; } from '../src/commands/ratings';
import { patientFetch } from '../src/core/blitzkrieg/patientFetch'; import { patientFetchJSON } from '../src/core/blitzkrieg/patientFetchJSON';
import commitMultipleFiles from './commitMultipleFiles.js'; import commitMultipleFiles from './commitMultipleFiles.js';
/* /*
@@ -29,11 +29,9 @@ const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1];
if (!server) throw new Error('Region parameter not specified'); if (!server) throw new Error('Region parameter not specified');
const NEIGHBORS = 1024; const NEIGHBORS = 1024;
const PLAYERS = await patientFetch( const PLAYERS = await patientFetchJSON<RatingsInfo>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
) ).then((data) => (data.detail === undefined ? data.count : undefined));
.then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) { if (PLAYERS === undefined) {
throw new Error('No current ratings season running'); throw new Error('No current ratings season running');
@@ -44,9 +42,9 @@ const leaderboard: BlitzkriegRatingsLeaderboard = [];
let registered = 0; let registered = 0;
async function branchFromPlayer(id: number) { async function branchFromPlayer(id: number) {
const { neighbors } = await patientFetch( const { neighbors } = await patientFetchJSON<RatingsNeighbors>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
).then((response) => response.json() as Promise<RatingsNeighbors>); );
const firstPlayer = neighbors[0]; const firstPlayer = neighbors[0];
const lastPlayer = neighbors[neighbors.length - 1]; const lastPlayer = neighbors[neighbors.length - 1];
@@ -75,9 +73,9 @@ async function branchFromPlayer(id: number) {
} }
async function branchFromLeague(id: number) { async function branchFromLeague(id: number) {
const { result } = await patientFetch( const { result } = await patientFetchJSON<{ result: RatingsPlayer[] }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>); );
const firstPlayer = result[0]; const firstPlayer = result[0];
const lastPlayer = result[result.length - 1]; const lastPlayer = result[result.length - 1];
@@ -113,11 +111,8 @@ const leaderboardJSON = JSON.stringify(leaderboard);
if (publish) { if (publish) {
const octokit = new Octokit({ auth: env.GH_TOKEN }); const octokit = new Octokit({ auth: env.GH_TOKEN });
const info = await patientFetch( const info = await patientFetchJSON<RatingsInfo & { detail: undefined }>(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then(
(response) =>
response.json() as Promise<RatingsInfo & { detail: undefined }>,
); );
const normalizedServer = server === 'na' ? 'com' : server; const normalizedServer = server === 'na' ? 'com' : server;
+6 -16
View File
@@ -137,15 +137,12 @@ export default function Page() {
await Promise.all( await Promise.all(
range(0, 5).map(async (league: number) => { range(0, 5).map(async (league: number) => {
const data = await getRatingsLeague(region, league); const data = await getRatingsLeague(region, league);
leaguePositionCache.current[region][league] = leaguePositionCache.current[region][league] = data[0].number - 1;
data.result[0].number - 1;
return data; return data;
}), }),
) )
) ).flat();
.map(({ result }) => result)
.flat();
useLeaderboardCache.setState( useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => { produce((draft: LeaderboardCache) => {
@@ -302,19 +299,12 @@ export default function Page() {
} }
} }
const seedingPlayerNeighborEntry = Object.entries(
leaderboard[region][season],
).find(([, player]) => id === player);
if (seedingPlayerNeighborEntry === undefined) return null;
const seedingPlayerPosition = parseInt(seedingPlayerNeighborEntry[0]);
const { neighbors } = await getRatingsNeighbors(region, id, size, true); const { neighbors } = await getRatingsNeighbors(region, id, size, true);
useLeaderboardCache.setState( useLeaderboardCache.setState(
produce((draft: LeaderboardCache) => { produce((draft: LeaderboardCache) => {
neighbors.forEach((neighbor, neighborIndex) => { neighbors.forEach((neighbor, neighborIndex) => {
const position = neighborIndex - radius + seedingPlayerPosition; const position = neighbor.number - 1;
if (!(position in draft[region][season])) { if (!(position in draft[region][season])) {
draft[region][season][position] = neighbor.spa_id; draft[region][season][position] = neighbor.spa_id;
@@ -349,9 +339,9 @@ export default function Page() {
async function jumpToPosition(position: number) { async function jumpToPosition(position: number) {
if (!ratingsInfo || ratingsInfo?.detail) return; if (!ratingsInfo || ratingsInfo?.detail) return;
const targetPosition = Math.max( const targetPosition = Math.min(
0, ratingsInfo.count - 1,
Math.min(position, ratingsInfo.count - 1), Math.max(0, Math.min(position, ratingsInfo.count - 1)),
); );
if (season === 0) { if (season === 0) {
+25 -31
View File
@@ -15,6 +15,8 @@ import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo';
import { getRatingsLeague } from '../core/blitz/getRatingsLeague';
import { getRatingsNeighbors } from '../core/blitz/getRatingsNeighbors';
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain'; import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber'; import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber';
import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo';
@@ -123,14 +125,14 @@ function addSubcommands(
.addSubcommand((option) => .addSubcommand((option) =>
addOptions( addOptions(
option option
.setName('neighbours') .setName('neighbors')
.setDescription('Your position and neighbours'), .setDescription('Your position and neighbors'),
) )
.addStringOption(addUsernameChoices) .addStringOption(addUsernameChoices)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName('limit')
.setDescription('How many neighbours to display (default: 10)') .setDescription('How many neighbors to display (default: 10)')
.setRequired(false) .setRequired(false)
.setMinValue(5) .setMinValue(5)
.setMaxValue(30), .setMaxValue(30),
@@ -228,7 +230,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
| 'current'; | 'current';
const subcommand = interaction.options.getSubcommand(true) as const subcommand = interaction.options.getSubcommand(true) as
| 'league' | 'league'
| 'neighbours'; | 'neighbors';
const limit = interaction.options.getInteger('limit') ?? 10; const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season'); const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined; const season = seasonOption ? parseInt(seasonOption) : undefined;
@@ -257,11 +259,8 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
const regionSubdomain = regionToRegionSubdomain(region); const regionSubdomain = regionToRegionSubdomain(region);
const result = const result =
subcommandGroup === 'current' subcommandGroup === 'current'
? await fetch( ? await getRatingsLeague(region, leagueIndex).then(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`, async (result) =>
)
.then((response) => response.json() as Promise<LeagueTop>)
.then(async ({ result }) =>
result.slice(0, limit).map( result.slice(0, limit).map(
(player) => (player) =>
({ ({
@@ -272,7 +271,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
nickname: player.nickname, nickname: player.nickname,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer,
), ),
) )
: await getArchivedRatingsLeaderboard(region, season!).then( : await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => { async (players) => {
const firstPlayerIndex = const firstPlayerIndex =
@@ -338,29 +337,24 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const regionSubdomain = regionToRegionSubdomain(region);
const neighbors = const neighbors =
subcommandGroup === 'current' subcommandGroup === 'current'
? await fetch( ? await getRatingsNeighbors(
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round( region,
limit / 2, id,
)}`, Math.round(limit / 2),
).then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
) )
.then(
(response) => response.json() as Promise<RatingsNeighbors>,
)
.then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: await getArchivedRatingsLeaderboard(region, season!).then( : await getArchivedRatingsLeaderboard(region, season!).then(
async (players) => { async (players) => {
const playerIndex = players.findIndex( const playerIndex = players.findIndex(
@@ -414,7 +408,7 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
titleName = neighbors titleName = neighbors
? accountInfo.nickname ? accountInfo.nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`; : `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours'; titleDescription = 'Ratings neighbors';
playersBefore = neighbors ? neighbors[0].position - 1 : 0; playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter = playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].position; regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
+9 -4
View File
@@ -1,4 +1,4 @@
import { LeagueTop } from '../../commands/ratings'; import { LeagueTop, RatingsPlayer } from '../../commands/ratings';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { context } from '../blitzkrieg/context'; import { context } from '../blitzkrieg/context';
import { patientFetch } from '../blitzkrieg/patientFetch'; import { patientFetch } from '../blitzkrieg/patientFetch';
@@ -9,7 +9,10 @@ export async function getRatingsLeague(
league: number, league: number,
usePatientFetcher = false, usePatientFetcher = false,
) { ) {
const fetcher = usePatientFetcher ? patientFetch : fetch; const fetcher = (url: string) =>
usePatientFetcher
? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
const response = await fetcher( const response = await fetcher(
context === 'website' context === 'website'
? `/api/${region}/ratings/current/league/${league}` ? `/api/${region}/ratings/current/league/${league}`
@@ -17,7 +20,9 @@ export async function getRatingsLeague(
region, region,
)}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`, )}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`,
); );
const json = (await response.json()) as LeagueTop; const json = await response.json();
return json; return context === 'website'
? (json as RatingsPlayer[])
: (json as LeagueTop).result;
} }
+4 -3
View File
@@ -10,9 +10,10 @@ export async function getRatingsNeighbors(
count: number, count: number,
usePatientFetcher = false, usePatientFetcher = false,
) { ) {
const fetcher = usePatientFetcher const fetcher = (url: string) =>
? patientFetch usePatientFetcher
: (url: string) => fetch(url, { cache: 'no-store' }); ? patientFetch(url, undefined, { cache: 'no-store' })
: fetch(url, { cache: 'no-store' });
const response = await fetcher( const response = await fetcher(
withCORSProxy( withCORSProxy(
`https://${regionToRegionSubdomain( `https://${regionToRegionSubdomain(
+23
View File
@@ -0,0 +1,23 @@
export async function patientFetchJSON<Type extends object>(
url: string,
timeout = 1000,
options?: Partial<{ cache: 'no-store' }>,
) {
let patientJSON: Type;
while (patientJSON! === undefined) {
try {
patientJSON = (await fetch(url, options).then(({ json }) =>
json(),
)) as Type;
} catch (error) {
console.warn(
`Failed to fetch ${url} as JSON; retrying in ${timeout}ms...`,
error,
);
await new Promise((resolve) => setTimeout(resolve, timeout));
}
}
return patientJSON;
}