diff --git a/.gitignore b/.gitignore index bcf647cdf..11dac0cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ test.* /docs/.vitepress/cache servers.csv .next/ -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo +temp/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f9d65679c..b89dc3f62 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "blitzstars", "cleandist", "cockroackdb", + "corsproxy", "customperiod", "deburr", "diacriticless", @@ -36,6 +37,7 @@ "subnation", "tankaverages", "tankopedia", + "tankopedias", "tankstats", "tresabhi", "Unicum", diff --git a/docs/changelogs/1.15.0.md b/docs/changelogs/1.15.0.md new file mode 100644 index 000000000..9abb80c32 --- /dev/null +++ b/docs/changelogs/1.15.0.md @@ -0,0 +1,72 @@ +# Blitzkrieg 1.15.0 + +Ratings will never be the same. + +## Features + +![Blitzkrieg Ratings. Elevate your game. Keep it simple.](https://i.imgur.com/7PZVAGQ.png) + +- Blitzkrieg ratings leaderboard + - Available at https://blitz-krieg.vercel.app/tools/ratings + - **Highly experimental; [please report bugs on Discord server](https://discord.gg/nDt7AjGJQH)** + - Current season leaderboards are live meaning that there may be scores and positions that are out of order + - Options + - Regions can be swapped between NA, EU, and Asia + - The current season and all archived season (from 49 and onwards) are available + - Jump: used to jump to certain players + - Score: finds the first player with at least your desired score + - Position: finds the player at that position + - League: finds the top-most player in that league + - Player: finds the player + - **Jumping to scores and positions for the current season is a compounding function (hence the loading screen) and is limited to the speed of Wargaming's APIs which is pretty darn slow** + - Page controls + - Flip between pages left and right + - 33 players are loaded at once + - Left control is disabled on the first page and vice versa + - Page turning controls are also disabled if the next/previous page hasn't loaded yet + - Page numbers can be manually typed in the text field too + - This is available both at the top and the bottom of the screen + - Players + - Shows the position of the player + - Shows the player nickname and clan name + - "Deleted player ID" us shown when loading a deleted player + - A skeleton is shown when the player and clan is loading + - Shows the player percentile + - Shows reward for the position + - This is blank when there is no reward + - This column isn't available for archived seasons + - Shows the score + - A skeleton is shown when loading + - Click on a row to highlight it + - Using a jump button also highlights a row +- `/credits`: thanks who helped made this project + +## Changes + +- Error messages that user face are now even more simplified +- Increased username and clan autocomplete size limits to `25` +- Added capability for per-page accent theming + - Example: the ratings page is red while the session tracker is blue +- Simplified welcome message for SKLLD clan server + +## Technical Changes + +- Isolated tankopedia info +- Implemented `corsproxy.io` for non-timeout tolerant endpoints +- Latest archived season number is now data driven +- Ratings archives are now public + - This eliminates the need of `octokit` +- Ratings leaderboard archiver is now error tolerant + - Fetch attempts are made every `1000ms` +- Blitz errors are now managed better freeing up the queue faster +- Isolated first archived season number +- Added more advanced PSAs + - These can now feature images or embeds + - These can now modify message contents + - These can also now be secret + +## Experimental Changes + +- Added tankopedia extractor + - Retrieves tankopedia data from the game files + - This is not utilized as of right now diff --git a/next.config.js b/next.config.js index 5e8d200ea..4f8efe754 100644 --- a/next.config.js +++ b/next.config.js @@ -7,6 +7,7 @@ const nextConfig = { ...withVanillaExtract(), distDir: 'dist/website', + reactStrictMode: false, }; export default nextConfig; diff --git a/package.json b/package.json index 86789c79f..e72f2e69d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkrieg", - "version": "1.14.4", + "version": "1.15.0", "description": "🎉 Tools for everything World of Tanks Blitz", "main": "src/server.ts", "type": "module", @@ -9,6 +9,7 @@ "build:website": "next build", "build:docs": "vitepress build docs", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard", + "build:tankopedia": "vite-node --script scripts/buildTankopedia", "dev:bot": "yarn build:bot && yarn start:bot", "dev:docs": "vitepress dev docs", "dev:website": "next dev", @@ -33,12 +34,14 @@ "@resvg/resvg-js": "^2.4.1", "@vanilla-extract/css": "^1.13.0", "bepaint": "^0.1.0-beta.3", + "decompress": "^4.2.1", "discord.js": "^14.13.0", "dotenv": "^16.3.1", "esbuild": "^0.19.3", "express": "^4.18.2", "fuzzysort": "^2.0.4", "immer": "^10.0.2", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "markdown-escape": "^2.0.0", "next": "^13.5.2", @@ -49,12 +52,15 @@ "vite-node": "^0.34.5", "vitepress": "^1.0.0-rc.20", "vitepress-plugin-auto-sidebar": "^1.1.0", + "xml-js": "^1.6.11", "zustand": "^4.4.1" }, "devDependencies": { "@total-typescript/ts-reset": "^0.5.1", + "@types/decompress": "^4.2.6", "@types/escape-html": "^1.0.2", "@types/express": "^4.17.18", + "@types/js-yaml": "^4.0.8", "@types/lodash": "^4.14.199", "@types/markdown-escape": "^1.1.0", "@types/node": "^20.6.5", diff --git a/scripts/buildRatingsLeaderboard.ts b/scripts/buildRatingsLeaderboard.ts index 72e850404..a2b2fe43f 100644 --- a/scripts/buildRatingsLeaderboard.ts +++ b/scripts/buildRatingsLeaderboard.ts @@ -6,6 +6,7 @@ import { RatingsNeighbors, RatingsPlayer, } from '../src/commands/ratings'; +import { patientFetch } from '../src/core/blitzkrieg/patientFetch'; import commitMultipleFiles from './commitMultipleFiles.js'; /* @@ -13,12 +14,13 @@ import commitMultipleFiles from './commitMultipleFiles.js'; * Central European Time (UTC+1): use 0 23 * * *. * Central Asia Standard Time (UTC+7): use 0 17 * * *. * - * na/ratings/49/info.json: season info like rewards, start & end time, etc. - * na/ratings/49/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight - * na/ratings/49/midnight.json: last midnight's leaderboard relative to the region + * [region]/ratings/[season]/info.json: season info like rewards, start & end time, etc. + * [region]/ratings/[season]/latest.json: the absolute latest leaderboard recorded at UTC+0 midnight + * [region]/ratings/[season]/midnight.json: last midnight's leaderboard relative to the region */ -export type BlitzkriegRatingsLeaderboard = { id: number; score: number }[]; +export type BlitzkriegRatingsLeaderboardEntry = { id: number; score: number }; +export type BlitzkriegRatingsLeaderboard = BlitzkriegRatingsLeaderboardEntry[]; const publish = argv.includes('--publish'); const latest = argv.includes('--latest'); @@ -26,8 +28,8 @@ const server = argv.find((arg) => arg.startsWith('--region='))?.split('=')[1]; if (!server) throw new Error('Region parameter not specified'); -const NEIGHBORS = 1000; -const PLAYERS = await fetch( +const NEIGHBORS = 1024; +const PLAYERS = await patientFetch( `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, ) .then((response) => response.json() as Promise) @@ -42,7 +44,7 @@ const leaderboard: BlitzkriegRatingsLeaderboard = []; let registered = 0; async function branchFromPlayer(id: number) { - const { neighbors } = await fetch( + const { neighbors } = await patientFetch( `https://${server}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`, ).then((response) => response.json() as Promise); @@ -73,7 +75,7 @@ async function branchFromPlayer(id: number) { } async function branchFromLeague(id: number) { - const { result } = await fetch( + const { result } = await patientFetch( `https://${server}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`, ).then((response) => response.json() as Promise<{ result: RatingsPlayer[] }>); @@ -111,7 +113,7 @@ const leaderboardJSON = JSON.stringify(leaderboard); if (publish) { const octokit = new Octokit({ auth: env.GH_TOKEN }); - const info = await fetch( + const info = await patientFetch( `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, ).then( (response) => diff --git a/scripts/buildTankopedia.ts b/scripts/buildTankopedia.ts new file mode 100644 index 000000000..a028e33b0 --- /dev/null +++ b/scripts/buildTankopedia.ts @@ -0,0 +1,86 @@ +import decompress from 'decompress'; +import { readFile, readdir, rm, writeFile } from 'fs/promises'; +import { load } from 'js-yaml'; +import { ElementCompact, xml2js } from 'xml-js'; +import { NATION_IDS } from '../src/constants/nations'; + +const REWRITE = false; +const VEHICLE_DEFS = 'assets/Data/XML/item_defs/vehicles'; +const APK_FILE = 'temp/blitz.apk'; +const STRINGS = 'assets/Data/Strings'; +const LANGUAGE = 'en'; +const DIRECTORIES_OF_INTEREST = [VEHICLE_DEFS, STRINGS]; + +if (REWRITE) { + await rm('temp/blitz', { recursive: true, force: true }); + decompress(APK_FILE, 'temp/blitz', { + filter: (file) => + DIRECTORIES_OF_INTEREST.some((dir) => file.path.startsWith(dir)), + }); +} + +interface BlitzkriegTankopedia { + id: number; + nation: string; + name: string; + name_short?: string; + is_premium: boolean; + is_collector: boolean; + tier: number; + type: string; +} + +const strings = await readFile( + `temp/blitz/${STRINGS}/${LANGUAGE}.yaml`, + 'utf-8', +).then((data) => load(data) as Record); +const tankopedia: Record = {}; +const nations = await readdir(`temp/blitz/${VEHICLE_DEFS}`); + +await Promise.all( + nations + .filter((nation) => nation !== 'common') + .map(async (nation) => { + const listXML = await readFile( + `temp/blitz/${VEHICLE_DEFS}/${nation}/list.xml`, + 'utf-8', + ); + const list = xml2js(listXML, { compact: true }) as ElementCompact; + + await Promise.all( + Object.entries(list.root).map( + async ([vehicle, vehicleData]) => { + // const vehicleXML = await readFile( + // `temp/blitz/${VEHICLE_DEFS}/${nation}/${vehicle}.xml`, + // 'utf-8', + // ).then((xml) => xml2js(xml, { compact: true }) as ElementCompact); + const nationVehicleId = parseInt(list.root[vehicle].id._text); + const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; + // TODO: use userString property instead + const name = strings[`#${nation}_vehicles:${vehicle}`]; + const name_short = strings[`#${nation}_vehicles:${vehicle}_short`]; + const is_premium = 'gold' in vehicleData.price; + const is_collector = vehicleData.sellPrice + ? 'gold' in vehicleData.sellPrice + : false; + const tier = parseInt(vehicleData.level._text); + const tags = vehicleData.tags._text.split(' '); + const type = tags[0]; + + tankopedia[id] = { + id, + name, + name_short, + nation, + is_premium, + is_collector, + tier, + type, + }; + }, + ), + ); + }), +); + +writeFile('dist/tankopedia.json', JSON.stringify(tankopedia, null, 2)); diff --git a/src/app/api/[region]/ratings/current/info/route.ts b/src/app/api/[region]/ratings/current/info/route.ts new file mode 100644 index 000000000..f7245897c --- /dev/null +++ b/src/app/api/[region]/ratings/current/info/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server'; +import { Region } from '../../../../../../constants/regions'; +import getRatingsInfo from '../../../../../../core/blitz/getRatingsInfo'; + +export async function GET( + request: Request, + { params }: { params: { region: Region } }, +) { + return NextResponse.json(await getRatingsInfo(params.region)); +} diff --git a/src/app/api/[region]/ratings/current/league/[league]/route.ts b/src/app/api/[region]/ratings/current/league/[league]/route.ts new file mode 100644 index 000000000..9871416e4 --- /dev/null +++ b/src/app/api/[region]/ratings/current/league/[league]/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { Region } from '../../../../../../../constants/regions'; +import { getRatingsLeague } from '../../../../../../../core/blitz/getRatingsLeague'; + +export async function GET( + request: Request, + { params }: { params: { region: Region; league: number } }, +) { + return NextResponse.json( + await getRatingsLeague(params.region, params.league, true), + ); +} diff --git a/src/app/api/[region]/ratings/current/search/[username]/route.ts b/src/app/api/[region]/ratings/current/search/[username]/route.ts new file mode 100644 index 000000000..0f5e51b53 --- /dev/null +++ b/src/app/api/[region]/ratings/current/search/[username]/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { Region } from '../../../../../../../constants/regions'; +import { searchCurrentRatingsPlayers } from '../../../../../../../core/blitz/searchCurrentRatingsPlayers'; + +export async function GET( + request: Request, + { params }: { params: { region: Region; username: string } }, +) { + return NextResponse.json( + await searchCurrentRatingsPlayers(params.region, params.username), + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2c03919e2..69c55312d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -41,12 +41,7 @@ export default function RootLayout({ children }: RootLayoutProps) { : 'url(https://i.imgur.com/PhS06NJ.png)', }} > - + {!isEmbed && } {children} diff --git a/src/app/tools/ratings/components/PageTurner/index.css.ts b/src/app/tools/ratings/components/PageTurner/index.css.ts new file mode 100644 index 000000000..e7521e1c9 --- /dev/null +++ b/src/app/tools/ratings/components/PageTurner/index.css.ts @@ -0,0 +1,13 @@ +import { keyframes, style } from '@vanilla-extract/css'; + +const spinKeyframes = keyframes({ + from: { transform: 'rotate(0deg)' }, + to: { transform: 'rotate(360deg)' }, +}); + +export const infiniteSpin = style({ + animationName: spinKeyframes, + animationIterationCount: 'infinite', + animationTimingFunction: 'linear', + animationDuration: '1s', +}); diff --git a/src/app/tools/ratings/components/PageTurner/index.tsx b/src/app/tools/ratings/components/PageTurner/index.tsx new file mode 100644 index 000000000..ba009d392 --- /dev/null +++ b/src/app/tools/ratings/components/PageTurner/index.tsx @@ -0,0 +1,95 @@ +import { + CaretLeftIcon, + CaretRightIcon, + SymbolIcon, +} from '@radix-ui/react-icons'; +import { Button, Flex, TextField } from '@radix-ui/themes'; +import { range } from 'lodash'; +import { useEffect, useRef } from 'react'; +import { infiniteSpin } from './index.css'; + +interface PageTurnerProps { + leaderboard: Record; + page: number; + rowsPerPage: number; + pages: number; + onPageChange: (page: number, isButtonClick: boolean) => void; + totalPlayers: number; +} + +export function PageTurner({ + page, + rowsPerPage, + onPageChange, + pages, + leaderboard, + totalPlayers, +}: PageTurnerProps) { + const pageInput = useRef(null); + const rightLoading = range( + page * rowsPerPage, + Math.min(totalPlayers - 1, page * rowsPerPage + rowsPerPage + 1), + ).some((index) => !(index in leaderboard)); + const leftLoading = range( + Math.max(0, page * rowsPerPage - 1), + Math.max(0, page * rowsPerPage - rowsPerPage - 1) + 1, + ).some((index) => !(index in leaderboard)); + + useEffect(() => { + if (pageInput.current) pageInput.current.value = `${page + 1}`; + }, [page]); + + return ( + + + + + Page + { + onPageChange( + Math.max( + 0, + Math.min(pages - 1, event.target.valueAsNumber - 1), + ), + false, + ); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + (event.target as HTMLInputElement).blur(); + } + }} + /> + out of {pages} + + + + + ); +} diff --git a/src/app/tools/ratings/page.tsx b/src/app/tools/ratings/page.tsx new file mode 100644 index 000000000..3d3661c09 --- /dev/null +++ b/src/app/tools/ratings/page.tsx @@ -0,0 +1,1167 @@ +'use client'; + +import { blackA, orangeDark } from '@radix-ui/colors'; +import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; +import { + Button, + Dialog, + Flex, + Select, + Table, + Text, + TextField, +} from '@radix-ui/themes'; +import { produce } from 'immer'; +import { debounce, range } from 'lodash'; +import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import { create } from 'zustand'; +import { RatingsInfo, RatingsPlayer } from '../../../commands/ratings'; +import PageWrapper from '../../../components/PageWrapper'; +import { Skeleton } from '../../../components/Skeleton'; +import { LEAGUES } from '../../../constants/leagues'; +import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings'; +import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions'; +import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; +import fetchBlitz from '../../../core/blitz/fetchBlitz'; +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 { searchCurrentRatingsPlayers } from '../../../core/blitz/searchCurrentRatingsPlayers'; +import { + AccountList, + AccountListItem, +} from '../../../core/blitz/searchPlayersAcrossRegions'; +import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber'; +import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo'; +import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard'; +import { theme } from '../../../stitches.config'; +import { PageTurner } from './components/PageTurner'; + +const ROWS_PER_PAGE = Math.floor(100 / 3); +const SEEDING_SIZE = 2 ** 7; + +interface NameCacheEntry { + nickname: string; + clan: string | null; +} +type NameCache = Record>; +type LeaderboardCache = Record>>; +type InfoCache = Record>; +type ScoreCache = Record>>; + +const useNameCache = create(() => ({ + asia: {}, + com: {}, + eu: {}, +})); +const useLeaderboardCache = create(() => ({ + asia: {}, + com: {}, + eu: {}, +})); +const useInfoCache = create(() => ({ + asia: {}, + com: {}, + eu: {}, +})); +const useScoreCache = create(() => ({ + asia: {}, + com: {}, + eu: {}, +})); + +export default function Page() { + const [region, setRegion] = useState('com'); + const [season, setSeason] = useState(0); + const [page, setPage] = useState(0); + const [jumpToLeague, setJumpToLeague] = useState(0); + const [searchLoading, setSearchLoading] = useState(false); + const [jumpToScoreOpen, setJumpToScoreOpen] = useState(false); + const [jumpToPositionOpen, setJumpToPositionOpen] = useState(false); + const [searchResults, setSearchResults] = useState< + (AccountListItem & { number: number })[] | undefined + >(undefined); + const [loadingProgress, setLoadingProgress] = useState< + [number, number] | null + >(null); + const [highlightedPlayer, setHighlightedPlayer] = useState< + { type: 'id'; id: number } | { type: 'position'; position: number } | null + >(null); + + const ratingsInfo = useInfoCache()[region][season]; + const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState< + number | null + >(null); + const scores = useScoreCache()[region][season]; + + const names = useNameCache(); + const leaderboard = useLeaderboardCache(); + + const positionInput = useRef(null); + const scoreInput = useRef(null); + const leaguePositionCache = useRef>>({ + asia: {}, + com: {}, + eu: {}, + }); + + const pages = Math.ceil( + (ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) / + ROWS_PER_PAGE, + ); + + // page reset on season or region change + useEffect(() => { + setPage(0); + + (async () => { + const newInfo = + season === 0 + ? await getRatingsInfo(region) + : await getArchivedRatingsInfo(region, season); + useInfoCache.setState( + produce((draft: InfoCache) => { + draft[region][season] = newInfo; + }), + ); + })(); + }, [season, region]); + // initial caching + useEffect(() => { + if (season in leaderboard[region]) return; + + if (season === 0) { + (async () => { + const results = ( + await Promise.all( + range(0, 5).map(async (league: number) => { + const data = await getRatingsLeague(region, league); + leaguePositionCache.current[region][league] = + data.result[0].number - 1; + + return data; + }), + ) + ) + .map(({ result }) => result) + .flat(); + + useLeaderboardCache.setState( + produce((draft: LeaderboardCache) => { + if (!(season in draft[region])) draft[region][season] = {}; + + results.forEach((result) => { + draft[region][season][result.number - 1] = result.spa_id; + }); + }), + ); + useNameCache.setState( + produce((draft: NameCache) => { + results.forEach((result) => { + draft[region][result.spa_id] = { + nickname: result.nickname, + clan: result.clan_tag, + }; + }); + }), + ); + useScoreCache.setState( + produce((draft: ScoreCache) => { + if (!(season in draft[region])) draft[region][season] = {}; + + results.forEach((result) => { + draft[region][season][result.spa_id] = result.score; + }); + }), + ); + })(); + } else { + getArchivedRatingsLeaderboard(region, season).then((results) => { + useLeaderboardCache.setState( + produce((draft: LeaderboardCache) => { + if (!(season in draft[region])) draft[region][season] = {}; + + results.forEach((result, index) => { + draft[region][season][index] = result.id; + }); + }), + ); + useScoreCache.setState( + produce((draft: ScoreCache) => { + if (!(season in draft[region])) draft[region][season] = {}; + + results.forEach((result) => { + draft[region][season][result.id] = result.score; + }); + }), + ); + }); + } + }, [season in leaderboard[region]]); + // 2nd page caching for current season + useEffect(() => { + if ( + season === 0 && + leaderboard !== null && + leaderboard[region][season] && + 0 in leaderboard[region][season] + ) { + cacheNeighbors(leaderboard[region][season][0], ROWS_PER_PAGE * 3); + } + }, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]); + // neighboring page caching for all seasons + useEffect(() => { + if (!ratingsInfo || ratingsInfo?.detail) return; + + if (season === 0) { + const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2; + const best = range( + page * ROWS_PER_PAGE, + page * ROWS_PER_PAGE + ROWS_PER_PAGE + 1, + ) + .filter((index) => index in leaderboard[region][season]) + .map((index) => ({ index, distance: Math.abs(index - middleIndex) })) + .sort((a, b) => b.distance - a.distance) + .at(0); + + if (best === undefined) return; + + cacheNeighbors( + leaderboard[region][season][best.index], + 2 * (Math.floor(ROWS_PER_PAGE / 2) + best.distance + ROWS_PER_PAGE), + ); + } else { + const ids = range( + Math.max(0, (page - 1) * ROWS_PER_PAGE), + Math.min( + ratingsInfo.count - 1, + (page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE, + ), + ) + .map((index) => leaderboard[region][season]?.[index]) + .filter(Boolean) + .filter((id) => !(id in names[region])); + + if (ids && ids.length > 0) { + getClanAccountInfo(region, ids, ['clan']).then((data) => { + data.map((player, index) => { + useNameCache.setState( + produce((draft: NameCache) => { + draft[region][ids[index]] = player + ? { + nickname: player.account_name, + clan: player.clan?.tag ?? null, + } + : null; + }), + ); + }); + }); + } + } + }, [ + page, + region, + season, + ratingsInfo?.detail, + leaderboard[region][season] !== undefined && + 0 in leaderboard[region][season], + ]); + // latest archived season number caching + useEffect(() => {}, [ + getArchivedLatestSeasonNumber().then((result) => { + setLatestArchivedSeasonNumber(result); + }), + ]); + + async function cacheNeighbors( + id: number, + size: number, + ): Promise { + const radius = Math.round(size / 2); + const targetPosition = Object.entries(leaderboard[region][season]).find( + ([, player]) => id === player, + )?.[0]; + + if (targetPosition !== undefined) { + let isMissing = false; + const targetPositionAsNumber = parseInt(targetPosition); + + for ( + let position = Math.max(0, targetPositionAsNumber - radius); + position <= targetPositionAsNumber + radius; + position++ + ) { + isMissing = !(position in leaderboard[region][season]); + if (isMissing) break; + } + + if (!isMissing) { + return null; + } + } + + 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); + + useLeaderboardCache.setState( + produce((draft: LeaderboardCache) => { + neighbors.forEach((neighbor, neighborIndex) => { + const position = neighborIndex - radius + seedingPlayerPosition; + + if (!(position in draft[region][season])) { + draft[region][season][position] = neighbor.spa_id; + } + }); + }), + ); + useScoreCache.setState( + produce((draft: ScoreCache) => { + neighbors.forEach((neighbor) => { + if (!(neighbor.spa_id in draft[region][season])) { + draft[region][season][neighbor.spa_id] = neighbor.score; + } + }); + }), + ); + useNameCache.setState( + produce((draft: NameCache) => { + neighbors.forEach((neighbor) => { + if (neighbor.nickname) { + draft[region][neighbor.spa_id] = { + nickname: neighbor.nickname, + clan: neighbor.clan_tag, + }; + } + }); + }), + ); + + return neighbors; + } + async function jumpToPosition(position: number) { + if (!ratingsInfo || ratingsInfo?.detail) return; + + const targetPosition = Math.max( + 0, + Math.min(position, ratingsInfo.count - 1), + ); + + if (season === 0) { + const playerEntries = Object.entries(leaderboard[region][season]); + let closestPlayerId: number | null = null; + let closestPosition: number | null = null; + let closestPositionDistance = Infinity; + + playerEntries.forEach(([positionAsString, player]) => { + const position = parseInt(positionAsString); + const distance = Math.abs(position - targetPosition); + + if (distance < closestPositionDistance) { + closestPositionDistance = distance; + closestPosition = position; + closestPlayerId = player; + } + }); + + if (closestPlayerId === null || closestPosition === null) return; + + if (closestPositionDistance === 0) { + setHighlightedPlayer({ type: 'position', position: targetPosition }); + setPage(Math.floor(targetPosition / ROWS_PER_PAGE)); + + return; + } + + // 1 going down and -1 going up + const loadingDirection = Math.sign(targetPosition - closestPosition); + let seedingPlayer = closestPlayerId as number; + let targetPositionAcquired = false; + let neighbors: RatingsPlayer[] | null; + + setLoadingProgress([0, closestPositionDistance]); + + while (!targetPositionAcquired) { + neighbors = await cacheNeighbors(seedingPlayer, SEEDING_SIZE); + + if (neighbors === null) { + targetPositionAcquired = true; + break; + } + + seedingPlayer = neighbors.at(loadingDirection === -1 ? 0 : -1)!.spa_id; + + if (loadingDirection === 1) { + targetPositionAcquired = + targetPosition <= neighbors.at(-1)!.number - 1; + setLoadingProgress([ + Math.abs(neighbors.at(-1)!.number - 1 - closestPosition), + closestPositionDistance, + ]); + } else { + targetPositionAcquired = targetPosition >= neighbors[0].number - 1; + setLoadingProgress([ + Math.abs(neighbors[0].number - 1 - closestPosition), + closestPositionDistance, + ]); + } + } + + setLoadingProgress(null); + setPage(Math.floor(targetPosition / ROWS_PER_PAGE)); + setHighlightedPlayer({ type: 'position', position: targetPosition }); + } else { + setPage(Math.floor(targetPosition / ROWS_PER_PAGE)); + + if (leaderboard) { + setHighlightedPlayer({ + type: 'position', + position: targetPosition, + }); + } + } + } + async function jumpToScore() { + if (!ratingsInfo || ratingsInfo?.detail) return; + + const targetScore = scoreInput.current!.valueAsNumber; + + if (season === 0) { + const playerEntries = Object.entries(leaderboard[region][season]); + let closestId: number | null = null; + let closestScore: number | null = null; + let closestScoreDistance = Infinity; + let closesIndex = -1; + + playerEntries.forEach(([positionAsString, id]) => { + const position = parseInt(positionAsString); + const distance = Math.abs(scores[id] - targetScore); + + if (distance < closestScoreDistance) { + closestScoreDistance = distance; + closestScore = scores[id]; + closestId = id; + closesIndex = position; + } + }); + + if (closestId === null || closestScore === null) return; + + if (closestScoreDistance === 0) { + setHighlightedPlayer({ type: 'id', id: closesIndex }); + setPage(Math.floor(closesIndex / ROWS_PER_PAGE)); + } + + const loadingDirection = Math.sign(closestScore - targetScore); + let targetPlayerId: number; + let targetPlayerPosition: number; + let scoreAcquired = false; + let seedingId = closestId as number; + let seedingScore = scores[seedingId]; + + setLoadingProgress([0, closestScoreDistance]); + while (!scoreAcquired) { + const neighbors = await cacheNeighbors(seedingId, SEEDING_SIZE); + + if (neighbors === null) { + const closesEntry = Object.entries(scores) + .sort(([, a], [, b]) => b - a) + .findLast(([, score]) => score >= targetScore); + + if (closesEntry === undefined) return; + + targetPlayerId = parseInt(closesEntry[0]); + targetPlayerPosition = parseInt( + Object.entries(leaderboard[region][season]).find( + ([, id]) => id === targetPlayerId, + )![0], + ); + scoreAcquired = true; + + break; + } + + const seedingNeighbor = neighbors.at(loadingDirection === 1 ? -1 : 0)!; + seedingId = seedingNeighbor.spa_id; + seedingScore = scores[seedingId]; + + setLoadingProgress([ + closestScoreDistance - Math.abs(seedingNeighbor.score - targetScore), + closestScoreDistance, + ]); + + if (loadingDirection === 1) { + scoreAcquired = seedingScore <= targetScore; + } else { + scoreAcquired = seedingScore >= targetScore; + } + + if (scoreAcquired) { + if (loadingDirection === 1) { + const targetPlayer = neighbors.find( + (player) => player.score <= targetScore, + )!; + + targetPlayerId = targetPlayer.spa_id; + targetPlayerPosition = targetPlayer.number - 1; + } else { + const targetPlayer = neighbors.findLast( + (player) => player.score >= targetScore, + )!; + + targetPlayerId = targetPlayer.spa_id; + targetPlayerPosition = targetPlayer.number - 1; + } + } + } + + setPage(Math.floor(targetPlayerPosition! / ROWS_PER_PAGE)); + setHighlightedPlayer({ type: 'id', id: targetPlayerId! }); + setLoadingProgress(null); + } else { + let playerIndex = -1; + + for (let index = ratingsInfo.count; index >= 0; index--) { + if (!(index in leaderboard[region][season])) continue; + + if (scores[leaderboard[region][season][index]] >= targetScore) { + playerIndex = index; + break; + } + } + + if (playerIndex === -1) return; + + setPage(Math.floor(playerIndex / ROWS_PER_PAGE)); + + if (leaderboard) { + setHighlightedPlayer({ + type: 'position', + position: playerIndex, + }); + } + } + } + + const totalPlayers = + !ratingsInfo?.detail && ratingsInfo?.count ? ratingsInfo.count : undefined; + + return ( + + + + setRegion(value as Region)} + > + + + + {REGIONS.map((region) => ( + + {REGION_NAMES[region]} + + ))} + + + + setSeason(parseInt(value))} + > + + + + {ratingsInfo?.detail === undefined && ( + Current season + )} + + + Archives + + {latestArchivedSeasonNumber && + range( + FIRST_ARCHIVED_RATINGS_SEASON, + latestArchivedSeasonNumber + 1, + ) + .map((season) => ( + + Season {season} + + )) + .reverse()} + + + + + + + Jump to: + + + + + + + + { + if (event.key === 'Enter') { + jumpToScore(); + setJumpToScoreOpen(false); + } + }} + /> + + + + + + + + + + + + + + + + + + + + { + if (event.key === 'Enter') { + jumpToPosition(positionInput.current!.valueAsNumber - 1); + setJumpToPositionOpen(false); + } + }} + /> + + + + + + + + + + + + + + + + + + + + setJumpToLeague(parseInt(value))} + > + + + + {ratingsInfo?.detail === undefined && + ratingsInfo?.leagues.map(({ index, title }) => ( + + {title} + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + ) => { + const trimmedSearch = event.target.value.trim(); + const encodedSearch = encodeURIComponent(trimmedSearch); + + if (trimmedSearch) { + if (season === 0) { + setSearchLoading(true); + + const accountList = + await searchCurrentRatingsPlayers( + region, + trimmedSearch, + ); + const values = Object.values(accountList).filter( + (searchedPlayer) => !searchedPlayer.skip, + ); + const valuesWithTypes = + values as ((typeof values)[number] & { + skip: false; + })[]; + + useLeaderboardCache.setState( + produce((draft: LeaderboardCache) => { + if (draft) { + values.forEach((player) => { + const playerWithTypes = + player as typeof player & { skip: false }; + + draft[region][season][ + playerWithTypes.number - 1 + ] = playerWithTypes.spa_id; + }); + } + }), + ); + useNameCache.setState( + produce((draft: typeof names) => { + valuesWithTypes.forEach((searchedPlayer) => { + if (searchedPlayer.nickname) { + draft[region][searchedPlayer.spa_id] = { + nickname: searchedPlayer.nickname, + clan: searchedPlayer.clan_tag, + }; + } + }); + }), + ); + setSearchResults( + valuesWithTypes.map((searchedPlayer) => ({ + account_id: searchedPlayer.spa_id, + nickname: searchedPlayer.nickname, + number: searchedPlayer.number, + })), + ); + setSearchLoading(false); + } else { + setSearchLoading(true); + const accountList = await fetchBlitz( + `https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}`, + ); + const playerKeys = Object.keys( + leaderboard[region][season], + ); + + setSearchResults( + accountList + .map((searchedPlayer) => ({ + ...searchedPlayer, + number: parseInt( + playerKeys.find( + (indexAsString) => + leaderboard[region][season][ + parseInt(indexAsString) + ] === searchedPlayer.account_id, + )!, + ), + })) + .filter( + (player) => !Number.isNaN(player.number), + ), + ); + + setSearchLoading(false); + } + } else { + setSearchResults(undefined); + } + }, + 500, + )} + placeholder="Search for player..." + /> + + + + {!searchLoading && + searchResults?.map((player) => ( + + + + ))} + + {!searchLoading && searchResults?.length === 0 && ( + + )} + + {!searchLoading && searchResults === undefined && ( + + )} + + {searchLoading && ( + + )} + + + + + + + + + + + + + + + isButtonClick ? setPage(page) : jumpToPosition(page * ROWS_PER_PAGE) + } + /> + + + + + Position + Player + {season === 0 && ( + Reward + )} + + Percentile + + Score + + + + {range( + page * ROWS_PER_PAGE, + Math.min( + page * ROWS_PER_PAGE + ROWS_PER_PAGE, + ratingsInfo?.detail ? 0 : (ratingsInfo?.count ?? 1) - 1, + ), + ).map((index) => { + const id = leaderboard[region][season]?.[index]; + const clan = id ? names[region][id]?.clan : undefined; + const position = index + 1; + const highlight = highlightedPlayer + ? highlightedPlayer.type === 'id' + ? highlightedPlayer.id === id + : highlightedPlayer.position === index + : false; + const reward = + !ratingsInfo?.detail && ratingsInfo?.rewards + ? ratingsInfo.rewards.find( + (reward) => + reward.from_position <= position && + reward.to_position >= position, + ) + : undefined; + + return ( + { + setHighlightedPlayer({ type: 'position', position: index }); + }} + > + {position.toLocaleString()}. + + + {names[region][id] === null + ? `Deleted player ${id}` + : names[region][id]?.nickname ?? ( + + )} + {clan ? ( + {`[${clan}]`} + ) : ( + clan === undefined && + names[region][id] !== null && ( + + ) + )} + + + {season === 0 && ( + + {reward ? ( + + { + + {reward.count === 1 ? '' : `x ${reward.count}`} + + + ) : null} + + )} + + {totalPlayers ? ( + `${Math.ceil(((index + 1) / totalPlayers) * 100)}%` + ) : ( + + )} + + + {(leaderboard[region][season]?.[index] + ? scores[leaderboard[region][season][index]]?.toLocaleString() + : undefined) ?? ( + + )} + + + ); + })} + + + + isButtonClick ? setPage(page) : jumpToPosition(page * ROWS_PER_PAGE) + } + /> + + {loadingProgress !== null && ( + + + Loading {loadingProgress[0]} of {loadingProgress[1]} assets... + + +
+
+
+ + + Please be patient. Wargaming is slow. + {loadingProgress[0] === 0 && ( + First assets will be fetched shortly. + )} + + + )} + + ); +} diff --git a/src/app/tools/session/page.tsx b/src/app/tools/session/page.tsx index 7f9d19635..bfba6c65c 100644 --- a/src/app/tools/session/page.tsx +++ b/src/app/tools/session/page.tsx @@ -51,7 +51,7 @@ export default function Page() { }, [session]); return ( - +
diff --git a/src/commands/credits.ts b/src/commands/credits.ts new file mode 100644 index 000000000..9cbe952d1 --- /dev/null +++ b/src/commands/credits.ts @@ -0,0 +1,15 @@ +import { AttachmentBuilder, SlashCommandBuilder } from 'discord.js'; +import { CommandRegistry } from '../events/interactionCreate'; + +export const creditsCommand: CommandRegistry = { + inProduction: true, + inPublic: true, + + command: new SlashCommandBuilder() + .setName('credits') + .setDescription('Me and the people who made this bot'), + + async handler() { + return new AttachmentBuilder('https://i.imgur.com/ZF5ch33.png'); + }, +}; diff --git a/src/commands/ownedTanks.tsx b/src/commands/ownedTanks.tsx index 8f731590e..242429804 100644 --- a/src/commands/ownedTanks.tsx +++ b/src/commands/ownedTanks.tsx @@ -8,13 +8,13 @@ import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getTankStats from '../core/blitz/getTankStats'; import getTreeType from '../core/blitz/getTreeType'; import resolveTankName from '../core/blitz/resolveTankName'; +import { tankopediaInfo } from '../core/blitz/tankopediaInfo'; import { emblemIdToURL } from '../core/blitzkrieg/emblemIdToURL'; import { TIER_ROMAN_NUMERALS, TankopediaEntry, Tier, tankopedia, - tankopediaInfo, } from '../core/blitzstars/tankopedia'; import addTierChoices from '../core/discord/addTierChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices'; diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 18c4f4504..7abe31c82 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -4,20 +4,22 @@ import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, } from 'discord.js'; +import { range } from 'lodash'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; import * as Leaderboard from '../components/Leaderboard'; import TitleBar from '../components/TitleBar'; import Wrapper from '../components/Wrapper'; +import { LEAGUES } from '../constants/leagues'; +import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings'; import { REGION_NAMES_SHORT, Region } from '../constants/regions'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo'; import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain'; +import { getArchivedLatestSeasonNumber } from '../core/blitzkrieg/getArchivedLatestSeasonNumber'; import getArchivedRatingsInfo from '../core/blitzkrieg/getArchivedRatingsInfo'; -import getMidnightLeaderboard, { - DATABASE_REPO, -} from '../core/blitzkrieg/getMidnightLeaderboard'; -import { octokit } from '../core/blitzkrieg/octokit'; +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'; @@ -113,14 +115,6 @@ export interface RatingsNeighbors { neighbors: RatingsPlayer[]; } -const LEAGUES = [ - { name: 'Diamond', minScore: 5000 }, - { name: 'Platinum', minScore: 4000 }, - { name: 'Gold', minScore: 3000 }, - { name: 'Silver', minScore: 2000 }, - { name: 'Bronze', minScore: 0 }, -]; - function addSubcommands( option: SlashCommandSubcommandGroupBuilder, addOptions = (option: SlashCommandSubcommandBuilder) => option, @@ -178,23 +172,17 @@ const noOngoingSeason = embedNegative( export const ratingsCommand = new Promise( async (resolve) => { - const seasonChoices = await octokit.repos - .getContent({ - ...DATABASE_REPO, - path: 'com/ratings', - }) - .then(({ data }) => { - if (!Array.isArray(data)) { - throw new Error('Archived ratings data is malformed'); - } - return data.map( - (folder) => - ({ - name: `Season ${folder.name}`, - value: folder.name, - }) satisfies APIApplicationCommandOptionChoice, - ); - }); + const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); + const seasonChoices = range( + FIRST_ARCHIVED_RATINGS_SEASON, + latestArchivedSeasonNumber + 1, + ).map( + (number) => + ({ + name: `Season ${number}`, + value: `${number}`, + }) satisfies APIApplicationCommandOptionChoice, + ); const command = new SlashCommandBuilder() .setName('ratings') @@ -262,7 +250,7 @@ export const ratingsCommand = new Promise( const region = interaction.options.getString('region') as Region; regionRatingsInfo = await (subcommandGroup === 'current' ? getRatingsInfo(region) - : getArchivedRatingsInfo(season!, region)); + : getArchivedRatingsInfo(region, season!)); if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; @@ -285,19 +273,8 @@ export const ratingsCommand = new Promise( }) satisfies SimplifiedPlayer, ), ) - : await octokit.repos - .getContent({ - ...DATABASE_REPO, - path: `${region}/ratings/${season}/latest.json`, - }) - .then(async ({ data }) => { - if (Array.isArray(data) || data.type !== 'file') { - throw new Error('Archived ratings data is malformed'); - } - - const players = (await fetch(data.download_url!).then( - (response) => response.json(), - )) as BlitzkriegRatingsLeaderboard; + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { const firstPlayerIndex = leagueIndex === 0 ? 0 @@ -328,15 +305,16 @@ export const ratingsCommand = new Promise( `Unknown Player ${player.id}`, }) satisfies SimplifiedPlayer, ); - }); + }, + ); const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; midnightLeaderboard = subcommandGroup === 'current' - ? await getMidnightLeaderboard( + ? await getArchivedRatingsMidnightLeaderboard( region, regionRatingsInfo.current_season, ) - : await getMidnightLeaderboard(region, season!); + : await getArchivedRatingsMidnightLeaderboard(region, season!); players = result; playersBefore = result ? result[0].position - 1 : 0; @@ -354,21 +332,7 @@ export const ratingsCommand = new Promise( regionRatingsInfo = subcommandGroup === 'current' ? await getRatingsInfo(region) - : await octokit.repos - .getContent({ - ...DATABASE_REPO, - path: `${region}/ratings/${season}/info.json`, - }) - .then(async ({ data }) => { - if (Array.isArray(data) || data.type !== 'file') { - throw new Error('Archived ratings info is malformed'); - } - - const response = fetch(data.download_url!); - return (await response.then((response) => - response.json(), - )) as RatingsInfo; - }); + : await getArchivedRatingsInfo(region, season!); if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; @@ -397,22 +361,8 @@ export const ratingsCommand = new Promise( }) satisfies SimplifiedPlayer, ), ) - : await octokit.repos - .getContent({ - ...DATABASE_REPO, - path: `${region}/ratings/${season}/latest.json`, - }) - .then(async ({ data }) => { - if (Array.isArray(data) || data.type !== 'file') { - throw new Error( - 'Archived ratings latest data is malformed', - ); - } - - const response = fetch(data.download_url!); - const players = (await response.then((response) => - response.json(), - )) as BlitzkriegRatingsLeaderboard; + : await getArchivedRatingsLeaderboard(region, season!).then( + async (players) => { const playerIndex = players.findIndex( (player) => player.id === id, ); @@ -445,14 +395,15 @@ export const ratingsCommand = new Promise( clan: clanData[player.id]!.clan?.tag, nickname: clanData[player.id]!.account_name, })); - }); + }, + ); midnightLeaderboard = subcommandGroup === 'current' - ? await getMidnightLeaderboard( + ? await getArchivedRatingsMidnightLeaderboard( region, regionRatingsInfo.current_season, ) - : await getMidnightLeaderboard(region, season!); + : await getArchivedRatingsMidnightLeaderboard(region, season!); if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`; @@ -516,10 +467,12 @@ export const ratingsCommand = new Promise( {items && ( {playersBefore > 0 && ( - + )} {items} - {playersAfter > 0 && } + {playersAfter > 0 && ( + + )} )} diff --git a/src/commands/today.tsx b/src/commands/today.tsx deleted file mode 100644 index 3b3e5e348..000000000 --- a/src/commands/today.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { CommandRegistry } from '../events/interactionCreate'; - -/** - * TODO: @deprecated - */ -export const todayCommand: CommandRegistry = { - inProduction: true, - inPublic: true, - - command: new SlashCommandBuilder() - .setName('today') - .setDescription('This command has been renamed to `/breakdown today`'), - - handler() { - return '# This command has been renamed to `/breakdown today`\n`/today` now comes with filters, periods and more! Use `/breakdown today` for same old functionality but also do checkout the other options.'; - }, -}; diff --git a/src/components/Leaderboard/components/Gap.tsx b/src/components/Leaderboard/components/Gap.tsx index fab9f6aba..ea99825a0 100644 --- a/src/components/Leaderboard/components/Gap.tsx +++ b/src/components/Leaderboard/components/Gap.tsx @@ -1,10 +1,10 @@ import { theme } from '../../../stitches.config'; export interface GapProps { - number: number; + message: string; } -export function Gap({ number }: GapProps) { +export function Gap({ message: number }: GapProps) { return (
- + {number.toLocaleString()} more + {number.toLocaleString()}
); diff --git a/src/components/Leaderboard/components/Item.tsx b/src/components/Leaderboard/components/Item.tsx index e2c33f3b3..79a707ebe 100644 --- a/src/components/Leaderboard/components/Item.tsx +++ b/src/components/Leaderboard/components/Item.tsx @@ -1,10 +1,11 @@ +import { ComponentProps } from 'react'; import { RatingsReward } from '../../../commands/ratings'; import { theme } from '../../../stitches.config'; -export interface ItemProps { +export interface ItemProps extends ComponentProps<'div'> { position: number; deltaPosition?: number; - score: number; + score?: number; deltaScore?: number; reward?: RatingsReward; nickname: string; @@ -25,10 +26,13 @@ export function Item({ nickname, clan, highlight, + ...props }: ItemProps) { return (
)} - - {score.toLocaleString()} - + {score !== undefined && ( + + {score.toLocaleString()} + + )}
); diff --git a/src/components/PageWrapper.tsx b/src/components/PageWrapper.tsx index 71086349b..666937af3 100644 --- a/src/components/PageWrapper.tsx +++ b/src/components/PageWrapper.tsx @@ -1,24 +1,31 @@ +import { Flex, Theme } from '@radix-ui/themes'; import { ComponentProps } from 'react'; +interface PageWrapperProps extends ComponentProps { + color?: ComponentProps['accentColor']; +} + export default function PageWrapper({ style, + color, ...props -}: ComponentProps<'div'>) { +}: PageWrapperProps) { return ( -
+ + ...style, + }} + /> + ); } diff --git a/src/components/Skeleton/index.css.ts b/src/components/Skeleton/index.css.ts new file mode 100644 index 000000000..99acbb31b --- /dev/null +++ b/src/components/Skeleton/index.css.ts @@ -0,0 +1,13 @@ +import { slateDarkA } from '@radix-ui/colors'; +import { keyframes, style } from '@vanilla-extract/css'; + +const animation = keyframes({ + '0%': { backgroundColor: slateDarkA.slateA2 }, + '50%': { backgroundColor: slateDarkA.slateA3 }, + '100%': { backgroundColor: slateDarkA.slateA2 }, +}); + +export const skeleton = style({ + borderRadius: 8, + animation: `${animation} 2s ease-in-out infinite`, +}); diff --git a/src/components/Skeleton/index.tsx b/src/components/Skeleton/index.tsx new file mode 100644 index 000000000..ca110b831 --- /dev/null +++ b/src/components/Skeleton/index.tsx @@ -0,0 +1,6 @@ +import { ComponentProps } from 'react'; +import { skeleton } from './index.css'; + +export function Skeleton({ className, ...props }: ComponentProps<'div'>) { + return
; +} diff --git a/src/constants/leagues.ts b/src/constants/leagues.ts new file mode 100644 index 000000000..082886490 --- /dev/null +++ b/src/constants/leagues.ts @@ -0,0 +1,7 @@ +export const LEAGUES = [ + { name: 'Diamond', minScore: 5000 }, + { name: 'Platinum', minScore: 4000 }, + { name: 'Gold', minScore: 3000 }, + { name: 'Silver', minScore: 2000 }, + { name: 'Bronze', minScore: 0 }, +]; diff --git a/src/constants/nations.ts b/src/constants/nations.ts new file mode 100644 index 000000000..f7adc2840 --- /dev/null +++ b/src/constants/nations.ts @@ -0,0 +1,11 @@ +export const NATION_IDS: Record = { + ussr: 0, + germany: 1, + usa: 2, + china: 3, + france: 4, + uk: 5, + japan: 6, + other: 7, + european: 8, +}; diff --git a/src/constants/ratings.ts b/src/constants/ratings.ts new file mode 100644 index 000000000..ec9ead703 --- /dev/null +++ b/src/constants/ratings.ts @@ -0,0 +1 @@ +export const FIRST_ARCHIVED_RATINGS_SEASON = 49; diff --git a/src/constants/regions.ts b/src/constants/regions.ts index ee4b04781..d6e088235 100644 --- a/src/constants/regions.ts +++ b/src/constants/regions.ts @@ -1,4 +1,6 @@ -export type Region = 'com' | 'eu' | 'asia'; +export const REGIONS = ['com', 'eu', 'asia'] as const; + +export type Region = (typeof REGIONS)[number]; export type RegionSubdomain = 'na' | 'eu' | 'asia'; export const REGION_NAMES: Record = { diff --git a/src/constants/tools.ts b/src/constants/tools.ts index c9c0b0241..efcaab6a6 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -9,13 +9,12 @@ export const TOOLS: Tool[] = [ { id: 'session', title: 'Session tracker', - description: 'Track your performance in a session', + description: 'Live session performance tracker', }, { id: 'ratings', title: 'Ratings', - description: 'Current and old season leader boards', - disabled: true, + description: 'Current and archived ratings leaderboards', }, { id: 'tankopedia', diff --git a/src/core/blitz/fetchBlitz.ts b/src/core/blitz/fetchBlitz.ts index 64f1ae26d..566f1715b 100644 --- a/src/core/blitz/fetchBlitz.ts +++ b/src/core/blitz/fetchBlitz.ts @@ -17,7 +17,7 @@ async function manageQueue() { if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) { inProgress++; - setTimeout(() => { + const timeout = setTimeout(() => { inProgress--; manageQueue(); }, 1000); @@ -32,7 +32,11 @@ async function manageQueue() { if (data.status === 'ok') { resolve(data.data); } else { - throw new Error(`Wargaming response error status:"${data.status}"`, { + clearTimeout(timeout); + inProgress--; + manageQueue(); + + throw new Error(`Wargaming response error status: "${data.status}"`, { cause: `Message: "${data.error.message}"\nURL: "${url}"`, }); } diff --git a/src/core/blitz/getRatingsInfo.ts b/src/core/blitz/getRatingsInfo.ts index 56588cdac..6c77df9f7 100644 --- a/src/core/blitz/getRatingsInfo.ts +++ b/src/core/blitz/getRatingsInfo.ts @@ -1,12 +1,18 @@ import { RatingsInfo } from '../../commands/ratings'; import { Region } from '../../constants/regions'; +import { context } from '../blitzkrieg/context'; +import { patientFetch } from '../blitzkrieg/patientFetch'; import regionToRegionSubdomain from './regionToRegionSubdomain'; export default async function getRatingsInfo(region: Region) { - const response = await fetch( - `https://${regionToRegionSubdomain( - region, - )}.wotblitz.com/en/api/rating-leaderboards/season/`, + const response = await patientFetch( + context === 'website' + ? `/api/${region}/ratings/current/info` + : `https://${regionToRegionSubdomain( + region, + )}.wotblitz.com/en/api/rating-leaderboards/season/`, + undefined, + { cache: 'no-store' }, ); return (await response.json()) as Promise; diff --git a/src/core/blitz/getRatingsLeague.ts b/src/core/blitz/getRatingsLeague.ts new file mode 100644 index 000000000..49445335e --- /dev/null +++ b/src/core/blitz/getRatingsLeague.ts @@ -0,0 +1,23 @@ +import { LeagueTop } from '../../commands/ratings'; +import { Region } from '../../constants/regions'; +import { context } from '../blitzkrieg/context'; +import { patientFetch } from '../blitzkrieg/patientFetch'; +import regionToRegionSubdomain from './regionToRegionSubdomain'; + +export async function getRatingsLeague( + region: Region, + league: number, + usePatientFetcher = false, +) { + const fetcher = usePatientFetcher ? patientFetch : fetch; + const response = await fetcher( + context === 'website' + ? `/api/${region}/ratings/current/league/${league}` + : `https://${regionToRegionSubdomain( + region, + )}.wotblitz.com/en/api/rating-leaderboards/league/${league}/top/`, + ); + const json = (await response.json()) as LeagueTop; + + return json; +} diff --git a/src/core/blitz/getRatingsNeighbors.ts b/src/core/blitz/getRatingsNeighbors.ts new file mode 100644 index 000000000..045cf9a01 --- /dev/null +++ b/src/core/blitz/getRatingsNeighbors.ts @@ -0,0 +1,26 @@ +import { RatingsNeighbors } from '../../commands/ratings'; +import { Region } from '../../constants/regions'; +import { patientFetch } from '../blitzkrieg/patientFetch'; +import { withCORSProxy } from '../blitzkrieg/withCORSProxy'; +import regionToRegionSubdomain from './regionToRegionSubdomain'; + +export async function getRatingsNeighbors( + region: Region, + id: number, + count: number, + usePatientFetcher = false, +) { + const fetcher = usePatientFetcher + ? patientFetch + : (url: string) => fetch(url, { cache: 'no-store' }); + const response = await fetcher( + withCORSProxy( + `https://${regionToRegionSubdomain( + region, + )}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${count}`, + ), + ); + const json = (await response.json()) as RatingsNeighbors; + + return json; +} diff --git a/src/core/blitz/searchClansAcrossRegions.ts b/src/core/blitz/searchClansAcrossRegions.ts index 98b4cfabb..45539244b 100644 --- a/src/core/blitz/searchClansAcrossRegions.ts +++ b/src/core/blitz/searchClansAcrossRegions.ts @@ -9,43 +9,45 @@ export type ClanListWithRegion = (Clan & { export default async function searchClansAcrossRegions( search: string, - limit = 9, + limit = 25, ) { const trimmed = search.trim(); if (trimmed.length < 3 && trimmed.length > 100) return []; - const normalizedLimit = Math.round(limit / 3); + const normalizedLimit = Math.floor(limit / 3); + + // TODO: remove splice once wargaming fixes the functionality of the limit felid return ( await Promise.all([ fetchBlitz( `https://api.wotblitz.com/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`, - ).then( - (value) => - value && - value.map((account) => ({ + ).then((value) => + value + .map((account) => ({ ...account, region: 'com' as Region, - })), + })) + .splice(0, normalizedLimit), ), fetchBlitz( `https://api.wotblitz.eu/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`, - ).then( - (value) => - value && - value.map((account) => ({ + ).then((value) => + value + .map((account) => ({ ...account, region: 'eu' as Region, - })), + })) + .splice(0, normalizedLimit), ), fetchBlitz( `https://api.wotblitz.asia/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${search}&limit=${normalizedLimit}`, - ).then( - (value) => - value && - value.map((account) => ({ + ).then((value) => + value + .map((account) => ({ ...account, region: 'asia' as Region, - })), + })) + .splice(0, normalizedLimit), ), ]) ) diff --git a/src/core/blitz/searchCurrentRatingsPlayers.ts b/src/core/blitz/searchCurrentRatingsPlayers.ts new file mode 100644 index 000000000..4be3a943c --- /dev/null +++ b/src/core/blitz/searchCurrentRatingsPlayers.ts @@ -0,0 +1,43 @@ +import { Region } from '../../constants/regions'; +import { context } from '../blitzkrieg/context'; +import regionToRegionSubdomain from './regionToRegionSubdomain'; + +type CurrentRatingsPlayerSearch = Record< + number, + { + spa_id: number; + nickname: string; + clan_tag: string; + } & ( + | { + skip: true; + } + | { + skip: false; + mmr: number; + season_number: number; + calibrationBattlesLeft: number; + number: number; + updated_at: string; + neighbors: []; + } + ) +>; + +export async function searchCurrentRatingsPlayers( + region: Region, + name: string, +) { + const encodedSearch = encodeURIComponent(name); + const response = await fetch( + context === 'website' + ? `/api/${region}/ratings/current/search/${encodedSearch}` + : `https://${regionToRegionSubdomain( + region, + )}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`, + { cache: 'no-store' }, + ); + const accountList = (await response.json()) as CurrentRatingsPlayerSearch; + + return accountList; +} diff --git a/src/core/blitz/searchPlayersAcrossRegions.ts b/src/core/blitz/searchPlayersAcrossRegions.ts index e8a12a17a..98dc76754 100644 --- a/src/core/blitz/searchPlayersAcrossRegions.ts +++ b/src/core/blitz/searchPlayersAcrossRegions.ts @@ -2,12 +2,12 @@ import { Region } from '../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; import fetchBlitz from './fetchBlitz'; -interface Account { +export interface AccountListItem { nickname: string; account_id: number; } -export type AccountList = Account[]; -export type AccountListWithServer = (Account & { +export type AccountList = AccountListItem[]; +export type AccountListWithServer = (AccountListItem & { region: Region; })[]; @@ -16,10 +16,10 @@ export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/; export default async function searchPlayersAcrossRegions( search: string, - limit = 9, + limit = 25, ) { const trimmedSearch = search.trim(); - const normalizedLimit = Math.round(limit / 3); + const normalizedLimit = Math.floor(limit / 3); const encodedSearch = encodeURIComponent(trimmedSearch); if (usernamePattern.test(trimmedSearch)) { diff --git a/src/core/blitz/tankopediaInfo.ts b/src/core/blitz/tankopediaInfo.ts new file mode 100644 index 000000000..cc9855fea --- /dev/null +++ b/src/core/blitz/tankopediaInfo.ts @@ -0,0 +1,15 @@ +import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; +import fetchBlitz from './fetchBlitz'; + +export interface TankopediaInfo { + achievement_sections: Record; + tanks_updated_at: number; + languages: Record; + vehicle_types: Record; + vehicle_nations: Record; + game_version: string; +} + +export const tankopediaInfo = fetchBlitz( + `https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`, +); diff --git a/src/core/blitzkrieg/context.ts b/src/core/blitzkrieg/context.ts index 07ba4b209..7d57bd9d7 100644 --- a/src/core/blitzkrieg/context.ts +++ b/src/core/blitzkrieg/context.ts @@ -1,4 +1,4 @@ -type Context = 'website' | 'bot'; +type Context = 'website' | 'server'; export const context: Context = - typeof window === 'undefined' ? 'bot' : 'website'; + typeof window === 'undefined' ? 'server' : 'website'; diff --git a/src/core/blitzkrieg/filtersToDescription.ts b/src/core/blitzkrieg/filtersToDescription.ts index 85d79c20d..8393ac294 100644 --- a/src/core/blitzkrieg/filtersToDescription.ts +++ b/src/core/blitzkrieg/filtersToDescription.ts @@ -1,10 +1,7 @@ import { TREE_TYPE_NAMES } from '../../components/Tanks'; import resolveTankName from '../blitz/resolveTankName'; -import { - TIER_ROMAN_NUMERALS, - Tier, - tankopediaInfo, -} from '../blitzstars/tankopedia'; +import { tankopediaInfo } from '../blitz/tankopediaInfo'; +import { TIER_ROMAN_NUMERALS, Tier } from '../blitzstars/tankopedia'; import { StatFilters } from '../statistics/filterStats'; export async function filtersToDescription({ diff --git a/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts b/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts new file mode 100644 index 000000000..8d508e0e1 --- /dev/null +++ b/src/core/blitzkrieg/getArchivedLatestSeasonNumber.ts @@ -0,0 +1,33 @@ +import getRatingsInfo from '../blitz/getRatingsInfo'; + +let cachedLatestArchivedSeasonNumber: number | null = null; + +export async function getArchivedLatestSeasonNumber() { + const ratingsInfo = await getRatingsInfo('com'); + + if (ratingsInfo.detail) { + const comURL = ( + (await fetch( + 'https://api.github.com/repos/tresabhi/blitzkrieg-db/git/trees/main', + ).then((response) => response.json())) as { + tree: { path: string; url: string }[]; + } + ).tree.find(({ path }) => path === 'com')!.url; + const ratingsURL = ( + (await fetch(comURL).then((response) => response.json())) as { + tree: [{ url: string }]; + } + ).tree[0].url; + const latestSeason = Math.max( + ...( + (await fetch(ratingsURL).then((response) => { + response.json(); + })) as { tree: { path: string }[] } + ).tree.map(({ path }) => parseInt(path)), + ); + + return latestSeason; + } else { + return ratingsInfo.current_season - 1; + } +} diff --git a/src/core/blitzkrieg/getArchivedRatingsInfo.ts b/src/core/blitzkrieg/getArchivedRatingsInfo.ts index 3a1e3157a..e08b9793a 100644 --- a/src/core/blitzkrieg/getArchivedRatingsInfo.ts +++ b/src/core/blitzkrieg/getArchivedRatingsInfo.ts @@ -1,9 +1,7 @@ import { RatingsInfo } from '../../commands/ratings'; import { Region } from '../../constants/regions'; -import { DATABASE_REPO } from './getMidnightLeaderboard'; -import { octokit } from './octokit'; -const ARCHIVED_RATINGS_CACHE: Record< +const INFO_CACHE: Record< Region, Record< number, @@ -18,27 +16,21 @@ const ARCHIVED_RATINGS_CACHE: Record< }; export default async function getArchivedRatingsInfo( - season: number, region: Region, + season: number, ) { - if (ARCHIVED_RATINGS_CACHE[region][season]) { - return ARCHIVED_RATINGS_CACHE[region][season]; + if (INFO_CACHE[region][season]) { + return INFO_CACHE[region][season]; } - const { data } = await octokit.repos.getContent({ - ...DATABASE_REPO, - path: `${region}/ratings/${season}/info.json`, - }); - - if (Array.isArray(data) || data.type !== 'file') - throw new Error('Archived ratings info is malformed'); - - const content = Buffer.from(data.content, 'base64').toString(); - const jsonContent = JSON.parse(content) as RatingsInfo & { + const response = await fetch( + `https://raw.githubusercontent.com/tresabhi/blitzkrieg-db/main/${region}/ratings/${season}/info.json`, + ); + const jsonContent = (await response.json()) as RatingsInfo & { detail: undefined; }; - ARCHIVED_RATINGS_CACHE[region][season] = jsonContent; + INFO_CACHE[region][season] = jsonContent; return jsonContent; } diff --git a/src/core/blitzkrieg/getArchivedRatingsLeaderboard.ts b/src/core/blitzkrieg/getArchivedRatingsLeaderboard.ts new file mode 100644 index 000000000..29e59e56e --- /dev/null +++ b/src/core/blitzkrieg/getArchivedRatingsLeaderboard.ts @@ -0,0 +1,28 @@ +import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard'; +import { Region } from '../../constants/regions'; + +const LEADERBOARD_CACHE: Record< + Region, + Record +> = { + asia: {}, + eu: {}, + com: {}, +}; + +export async function getArchivedRatingsLeaderboard( + region: Region, + season: number, +) { + if (LEADERBOARD_CACHE[region][season]) { + return LEADERBOARD_CACHE[region][season]; + } + + const response = await fetch( + `https://raw.githubusercontent.com/tresabhi/blitzkrieg-db/main/${region}/ratings/${season}/latest.json`, + ); + const json = (await response.json()) as BlitzkriegRatingsLeaderboard; + LEADERBOARD_CACHE[region][season] = json; + + return json; +} diff --git a/src/core/blitzkrieg/getMidnightLeaderboard.ts b/src/core/blitzkrieg/getArchivedRatingsMidnightLeaderboard.ts similarity index 56% rename from src/core/blitzkrieg/getMidnightLeaderboard.ts rename to src/core/blitzkrieg/getArchivedRatingsMidnightLeaderboard.ts index 17794025d..85c7029b0 100644 --- a/src/core/blitzkrieg/getMidnightLeaderboard.ts +++ b/src/core/blitzkrieg/getArchivedRatingsMidnightLeaderboard.ts @@ -1,6 +1,5 @@ import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard'; import { Region } from '../../constants/regions'; -import { octokit } from './octokit'; export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' }; @@ -13,7 +12,7 @@ const MIDNIGHT_LEADERBOARD_CACHE: Record< asia: {}, }; -export default async function getMidnightLeaderboard( +export default async function getArchivedRatingsMidnightLeaderboard( region: Region, season: number, ) { @@ -22,19 +21,13 @@ export default async function getMidnightLeaderboard( } try { - const { data } = await octokit.repos.getContent({ - ...DATABASE_REPO, - path: `${region}/ratings/${season}/midnight.json`, - }); + const response = await fetch( + `https://raw.githubusercontent.com/tresabhi/blitzkrieg-db/main/${region}/ratings/${season}/midnight.json`, + ); + const json = (await response.json()) as BlitzkriegRatingsLeaderboard; + MIDNIGHT_LEADERBOARD_CACHE[region][season] = json; - if (!Array.isArray(data) && data.type === 'file') { - const response = await fetch(data.download_url!); - const json = (await response.json()) as BlitzkriegRatingsLeaderboard; - - MIDNIGHT_LEADERBOARD_CACHE[region][season] = json; - - return json; - } + return json; } catch (error) { console.warn( `No midnight leaderboard found for season ${season}, region ${region}`, diff --git a/src/core/blitzkrieg/patientFetch.ts b/src/core/blitzkrieg/patientFetch.ts new file mode 100644 index 000000000..ebb4e1f71 --- /dev/null +++ b/src/core/blitzkrieg/patientFetch.ts @@ -0,0 +1,21 @@ +export async function patientFetch( + url: string, + timeout = 1000, + options?: Partial<{ cache: 'no-store' }>, +) { + let patientResponse: Response; + + while (patientResponse! === undefined) { + try { + patientResponse = await fetch(url, options); + } catch (error) { + console.warn( + `Failed to fetch ${url}; retrying in ${timeout}ms...`, + error, + ); + await new Promise((resolve) => setTimeout(resolve, timeout)); + } + } + + return patientResponse; +} diff --git a/src/core/blitzkrieg/withCORSProxy.ts b/src/core/blitzkrieg/withCORSProxy.ts new file mode 100644 index 000000000..cce2d45ad --- /dev/null +++ b/src/core/blitzkrieg/withCORSProxy.ts @@ -0,0 +1,7 @@ +import { context } from './context'; + +export function withCORSProxy(url: string) { + return context === 'website' + ? `https://corsproxy.io/?${encodeURIComponent(url)}` + : url; +} diff --git a/src/core/blitzstars/tankAverages.ts b/src/core/blitzstars/tankAverages.ts index a210431c6..6316d7db9 100644 --- a/src/core/blitzstars/tankAverages.ts +++ b/src/core/blitzstars/tankAverages.ts @@ -85,13 +85,13 @@ tankopedia; export type TankAverages = Record; export const tankAverages = fetch( - context === 'bot' - ? 'https://www.blitzstars.com/api/tankaverages.json' - : '/api/tank-averages', + context === 'website' + ? '/api/tank-averages' + : 'https://www.blitzstars.com/api/tankaverages.json', ) .then((response) => response.json()) .then((json) => { - if (context === 'bot') { + if (context === 'server') { const partialTankAverages: TankAverages = {}; (json as IndividualTankAverage[]).forEach((individualTankAverage) => { diff --git a/src/core/blitzstars/tankopedia.ts b/src/core/blitzstars/tankopedia.ts index a21182b0e..faf794f6c 100644 --- a/src/core/blitzstars/tankopedia.ts +++ b/src/core/blitzstars/tankopedia.ts @@ -1,6 +1,4 @@ import { deburr } from 'lodash'; -import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID'; -import fetchBlitz from '../blitz/fetchBlitz'; import { context } from '../blitzkrieg/context'; export interface TankopediaEntry { @@ -20,13 +18,13 @@ export interface Tankopedia { } export const tankopedia = fetch( - context === 'bot' - ? 'https://www.blitzstars.com/bs-tankopedia.json' - : '/api/tankopedia', + context === 'website' + ? '/api/tankopedia' + : 'https://www.blitzstars.com/bs-tankopedia.json', ) .then(async (response) => response.json()) .then((wrapperTankopedia) => - context === 'bot' + context === 'server' ? (wrapperTankopedia as { data: Tankopedia }).data : (wrapperTankopedia as Tankopedia), ); @@ -56,20 +54,6 @@ export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) => ), ); -export interface TankopediaInfo { - achievement_sections: Record; - tanks_updated_at: number; - languages: Record; - vehicle_types: Record; - vehicle_nations: Record; - game_version: string; -} - -// this is blocking because info is needed for command creation -export const tankopediaInfo = fetchBlitz( - `https://api.wotblitz.com/wotb/encyclopedia/info/?application_id=${WARGAMING_APPLICATION_ID}`, -); - export const TANK_ICONS: Record = { 'AT-SPG': 'https://i.imgur.com/BIHSEH0.png', lightTank: 'https://i.imgur.com/CSNha5V.png', diff --git a/src/core/discord/addFilterOptions.ts b/src/core/discord/addFilterOptions.ts index 9a79642a4..9468e0484 100644 --- a/src/core/discord/addFilterOptions.ts +++ b/src/core/discord/addFilterOptions.ts @@ -4,7 +4,7 @@ import { SlashCommandSubcommandGroupBuilder, } from 'discord.js'; import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks'; -import { tankopediaInfo } from '../blitzstars/tankopedia'; +import { tankopediaInfo } from '../blitz/tankopediaInfo'; import addPeriodSubCommands from './addPeriodSubCommands'; import addTankChoices from './addTankChoices'; import addTierChoices from './addTierChoices'; diff --git a/src/core/discord/autocompleteClan.ts b/src/core/discord/autocompleteClan.ts index 83b4bdc0e..bbbd9a2de 100644 --- a/src/core/discord/autocompleteClan.ts +++ b/src/core/discord/autocompleteClan.ts @@ -3,26 +3,39 @@ import { AutocompleteInteraction, CacheType, } from 'discord.js'; -import { REGION_NAMES } from '../../constants/regions'; +import { REGION_NAMES_SHORT } from '../../constants/regions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; +export const DISCORD_CHOICES_MAX_NAME_SIZE = 25; +export const OVERFLOW_SUFFIX = '…'; + export default async function autocompleteClan( interaction: AutocompleteInteraction, ) { const focusedOption = interaction.options.getFocused(true); if (focusedOption.name !== 'clan') return; if (focusedOption.value.length < 2) return interaction.respond([]); - const players = await searchClansAcrossRegions(focusedOption.value); + const clans = await searchClansAcrossRegions(focusedOption.value); await interaction.respond( - players - ? players.map( - (player) => - ({ - name: `${player.name} (${REGION_NAMES[player.region]})`, - value: `${player.region}/${player.clan_id}`, - }) satisfies ApplicationCommandOptionChoiceData, - ) + clans + ? clans.map((clan) => { + let name = `${clan.name} (${REGION_NAMES_SHORT[clan.region]})`; + + if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { + const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; + + name = `${clan.name.slice( + 0, + clan.name.length - overSize - OVERFLOW_SUFFIX.length, + )}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[clan.region]})`; + } + + return { + name, + value: `${clan.region}/${clan.clan_id}`, + } satisfies ApplicationCommandOptionChoiceData; + }) : [], ); } diff --git a/src/core/discord/autocompleteTanks.ts b/src/core/discord/autocompleteTanks.ts index adfc8477b..de74a907f 100644 --- a/src/core/discord/autocompleteTanks.ts +++ b/src/core/discord/autocompleteTanks.ts @@ -5,6 +5,10 @@ import { } from 'discord.js'; import { go } from 'fuzzysort'; import { TANK_NAMES_DIACRITICS } from '../blitzstars/tankopedia'; +import { + DISCORD_CHOICES_MAX_NAME_SIZE, + OVERFLOW_SUFFIX, +} from './autocompleteClan'; export default async function autocompleteTanks( interaction: AutocompleteInteraction, @@ -18,13 +22,23 @@ export default async function autocompleteTanks( go(focusedOption.value, await TANK_NAMES_DIACRITICS, { keys: ['combined'], limit: 10, - }).map( - async (item) => - ({ - name: item.obj.original, - value: `${item.obj.id}`, - }) satisfies ApplicationCommandOptionChoiceData, - ), + }).map(async (item) => { + let name = item.obj.original; + + if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { + const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; + + name = `${item.obj.original.slice( + 0, + item.obj.original.length - overSize - OVERFLOW_SUFFIX.length, + )}${OVERFLOW_SUFFIX}`; + } + + return { + name, + value: `${item.obj.id}`, + } satisfies ApplicationCommandOptionChoiceData; + }), ) : [], ); diff --git a/src/core/discord/autocompleteUsername.ts b/src/core/discord/autocompleteUsername.ts index 5160a2024..333b51624 100644 --- a/src/core/discord/autocompleteUsername.ts +++ b/src/core/discord/autocompleteUsername.ts @@ -5,6 +5,10 @@ import { } from 'discord.js'; import { REGION_NAMES_SHORT } from '../../constants/regions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; +import { + DISCORD_CHOICES_MAX_NAME_SIZE, + OVERFLOW_SUFFIX, +} from './autocompleteClan'; export default async function autocompleteUsername( interaction: AutocompleteInteraction, @@ -16,15 +20,25 @@ export default async function autocompleteUsername( try { await interaction.respond( players - ? players.map( - (player) => - ({ - name: `${player.nickname} (${ - REGION_NAMES_SHORT[player.region] - })`, - value: `${player.region}/${player.account_id}`, - }) satisfies ApplicationCommandOptionChoiceData, - ) + ? players.map((player) => { + let name = `${player.nickname} (${ + REGION_NAMES_SHORT[player.region] + })`; + + if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { + const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; + + name = `${player.nickname.slice( + 0, + player.nickname.length - overSize - OVERFLOW_SUFFIX.length, + )}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[player.region]})`; + } + + return { + name, + value: `${player.region}/${player.account_id}`, + } satisfies ApplicationCommandOptionChoiceData; + }) : [], ); } catch (error) {} diff --git a/src/core/discord/psa.ts b/src/core/discord/psa.ts index 06441f663..5f6d78f7c 100644 --- a/src/core/discord/psa.ts +++ b/src/core/discord/psa.ts @@ -1,13 +1,30 @@ -export interface PSAData { - title: string; - description: string; -} +export type PSAData = { + secret?: boolean; + links?: { + title: string; + url: string; + }[]; + commands?: string[]; +} & ( + | { + type: 'image'; + image: string; + title?: string; + } + | { + type: 'embed'; + title: string; + description: string; + } +); export interface PSA { data: PSAData | null; } -export const psa: PSA = { data: null }; +export const psa: PSA = { + data: null, +}; fetch( 'https://gist.githubusercontent.com/tresabhi/ed4b136f08856e615212e573aabe1968/raw', diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index ad798295f..01bf564f1 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -1,12 +1,5 @@ -import { - AttachmentBuilder, - EmbedBuilder, - GuildMember, - TextChannel, -} from 'discord.js'; -import markdownEscape from 'markdown-escape'; +import { AttachmentBuilder, GuildMember, TextChannel } from 'discord.js'; import discord from '../../discord.json' assert { type: 'json' }; -import { ACCENT_COLOR } from '../constants/colors'; export default async function (member: GuildMember) { if (member.guild.id === discord.sklld_guild_id) { @@ -15,15 +8,7 @@ export default async function (member: GuildMember) { discord.sklld_verify_channel, ) as TextChannel ).send({ - content: member.toString(), - embeds: [ - new EmbedBuilder() - .setColor(ACCENT_COLOR) - .setTitle(`Welcome, ${markdownEscape(member.user.displayName)}`) - .setDescription( - `Welcome to the Skilled server! To continue, please use the \`/link\` command.`, - ), - ], + content: `# Welcome, ${member.toString()}!\nPlease use the \`/link\` command like the video below to continue.`, files: [new AttachmentBuilder('https://i.imgur.com/2p6GFgC.gif')], }); } diff --git a/src/events/interactionCreate/handlers/chatInputCommand.ts b/src/events/interactionCreate/handlers/chatInputCommand.ts index dbe76ff67..adb323b07 100644 --- a/src/events/interactionCreate/handlers/chatInputCommand.ts +++ b/src/events/interactionCreate/handlers/chatInputCommand.ts @@ -1,13 +1,16 @@ import { ActionRowBuilder, + AttachmentBuilder, ButtonBuilder, ButtonStyle, CacheType, ChatInputCommandInteraction, + InteractionReplyOptions, } from 'discord.js'; 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'; @@ -20,52 +23,122 @@ export default async function handleChatInputCommand( await interaction.deferReply(); + if (registry.inPreview && interaction.guildId !== discord.tres_guild_id) { + interaction.editReply({ + embeds: [ + embedWarning( + `\`/${registry.command.name}\` is in Public Preview`, + '[Join the official Discord server](https://discord.gg/nDt7AjGJQH) to gain early access to this command before it is released to the public.', + ), + ], + }); + + return; + } + try { - if (registry.inPreview && interaction.guildId !== discord.tres_guild_id) { - interaction.editReply({ - embeds: [ - embedWarning( - `\`/${registry.command.name}\` is in Public Preview`, - '[Join the official Discord server](https://discord.gg/nDt7AjGJQH) to gain early access to this command before it is released to the public.', - ), - ], - }); - - return; - } - const returnable = await registry.handler(interaction); if (registry.handlesInteraction) return; const reply = await normalizeInteractionReturnable(returnable!); - if (psa.data) { - if (!reply.embeds) reply.embeds = []; - reply.embeds.push(embedWarning(psa.data.title, psa.data.description)); + if ( + psa.data && + !psa.data.secret && + (psa.data.commands === undefined || + psa.data.commands.includes(interaction.commandName)) + ) { + if (psa.data.type === 'embed') { + if (!reply.embeds) reply.embeds = []; + reply.embeds.push(embedWarning(psa.data.title, psa.data.description)); + } else { + if (!reply.files) reply.files = []; + reply.files.push(new AttachmentBuilder(psa.data.image)); + reply.content = reply.content ?? psa.data.title; + } + + if (psa.data.links) { + if (!reply.components?.[0]) { + reply.components = [new ActionRowBuilder()]; + } + + (reply.components[0] as ActionRowBuilder).addComponents( + psa.data.links.map(({ title, url }) => buttonLink(url, title)), + ); + } } await interaction.editReply(reply); - } catch (error) { - const actionRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('Get Help on Discord Server') - .setURL('https://discord.gg/nDt7AjGJQH') - .setStyle(ButtonStyle.Link), - ); - if (!(error instanceof UserError)) { - console.error(interaction.commandName, error); + if ( + psa.data && + psa.data.secret && + (psa.data.commands === undefined || + psa.data.commands.includes(interaction.commandName)) + ) { + const followUp: InteractionReplyOptions = { + ephemeral: true, + }; + + if (psa.data.type === 'embed') { + if (!followUp.embeds) followUp.embeds = []; + followUp.embeds.push( + embedWarning(psa.data.title, psa.data.description), + ); + } else { + if (!followUp.files) followUp.files = []; + followUp.files.push(new AttachmentBuilder(psa.data.image)); + followUp.content = followUp.content ?? psa.data.title; + } + + if (psa.data.links) { + if (!followUp.components?.[0]) { + followUp.components = [new ActionRowBuilder()]; + } + + ( + followUp.components[0] as ActionRowBuilder + ).addComponents( + psa.data.links.map(({ title, url }) => buttonLink(url, title)), + ); + } + + interaction.followUp(followUp); } + } catch (error) { + const components = [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('Get Help on Discord Server') + .setURL('https://discord.gg/nDt7AjGJQH') + .setStyle(ButtonStyle.Link), + ), + ]; - await interaction.editReply({ - embeds: [ - embedNegative( - (error as Error).message, - `${(error as Error).cause ?? 'No further information is available.'}`, - ), - ], - components: [actionRow], - }); + if (error instanceof UserError) { + interaction.editReply({ + embeds: [ + embedNegative( + error.message, + (error.cause as string | undefined) ?? + 'No further information is available.', + ), + ], + 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.", + ), + ], + components, + }); + } } } diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 7a41cc454..6dedea8b9 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -16,6 +16,7 @@ import { import discord from '../../../discord.json' assert { type: 'json' }; import { aboutCommand } from '../../commands/about'; import { breakdownCommand } from '../../commands/breakdown'; +import { creditsCommand } from '../../commands/credits'; import { debugCommand } from '../../commands/debug'; import { eligibleCommand } from '../../commands/eligible'; import { evolutionCommand } from '../../commands/evolution'; @@ -31,7 +32,6 @@ 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'; @@ -102,7 +102,7 @@ export const COMMANDS_RAW: CommandRegistry[] = [ evolutionCommand, statsCommand, ratingsCommand, - todayCommand, + creditsCommand, ]; export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) => diff --git a/src/stitches.config.ts b/src/stitches.config.ts index 0e5b70ea0..f8016ac4a 100644 --- a/src/stitches.config.ts +++ b/src/stitches.config.ts @@ -1,6 +1,7 @@ import { amberDark, blueDark, + orangeDark, slate, slateDark, tealDark, @@ -16,6 +17,7 @@ export const theme = { ...createColors(slate, 'light'), ...createColors(amberDark, 'amber'), + ...createColors(orangeDark, 'orange'), ...createColors(blueDark, 'blue'), ...createColors(tealDark, 'teal'), }, diff --git a/yarn.lock b/yarn.lock index b8670b494..5ae1040b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1882,6 +1882,13 @@ dependencies: "@types/node" "*" +"@types/decompress@^4.2.6": + version "4.2.6" + resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.6.tgz#2ae3fb360c4d27593f51293e54990b9e709477ae" + integrity sha512-ULmsMkKxU7aIGbnxP4Rz28hLdRZ4q0cdy6kcb8dx+UgDGOn+id5fibvoeTnjuolhrRM7f7TOtGADeKEk60SSsQ== + dependencies: + "@types/node" "*" + "@types/escape-html@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c" @@ -1912,6 +1919,11 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2" integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg== +"@types/js-yaml@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.8.tgz#7574e422d70d4a1b41f517d1d9abc61be2299a97" + integrity sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA== + "@types/linkify-it@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b" @@ -2333,6 +2345,11 @@ base64-js@0.0.8: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + before-after-hook@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" @@ -2348,6 +2365,14 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bl@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -2390,11 +2415,42 @@ browserslist@^4.21.9: node-releases "^2.0.13" update-browserslist-db "^1.0.13" +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.2.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + busboy@1.6.0, busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -2496,6 +2552,11 @@ color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^2.8.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -2540,6 +2601,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cross-spawn@^7.0.0: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2602,6 +2668,59 @@ debug@^4.1.0, debug@^4.3.1, debug@^4.3.4: dependencies: ms "2.1.2" +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + deep-freeze@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" @@ -2730,6 +2849,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +end-of-stream@^1.0.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + esbuild@0.17.6: version "0.17.6" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.6.tgz#bbccd4433629deb6e0a83860b3b61da120ba4e01" @@ -2907,11 +3033,33 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + fflate@^0.7.3: version "0.7.4" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50" integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw== +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== + +file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -2975,6 +3123,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -3010,6 +3163,14 @@ get-nonce@^1.0.0: resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -3049,7 +3210,7 @@ globby@^13.1.3: merge2 "^1.4.1" slash "^4.0.0" -graceful-fs@^4.1.2: +graceful-fs@^4.1.10, graceful-fs@^4.1.2: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3114,6 +3275,11 @@ identity-function@^1.0.0: resolved "https://registry.yarnpkg.com/identity-function/-/identity-function-1.0.0.tgz#bea1159f0985239be3ca348edf40ce2f0dd2c21d" integrity sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw== +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.1.8, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" @@ -3129,7 +3295,7 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inherits@2.0.4: +inherits@2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3168,6 +3334,11 @@ is-iterable@^1.1.0: resolved "https://registry.yarnpkg.com/is-iterable/-/is-iterable-1.1.1.tgz#71f9aa6f113e1d968ebe1d41cff4c8fb23a817bc" integrity sha512-EdOZCr0NsGE00Pot+x1ZFx9MJK3C6wy91geZpXwvwexDLJvA4nzYyZf7r+EIwSeVsOLDdBz7ATg9NqKTzuNYuQ== +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== + is-number@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" @@ -3183,6 +3354,16 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3349,6 +3530,13 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + map-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" @@ -3507,6 +3695,11 @@ npm-normalize-package-bin@^3.0.0: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" @@ -3618,6 +3811,11 @@ pathe@^1.1.0, pathe@^1.1.1: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -3628,6 +3826,28 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + pkg-types@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" @@ -3684,6 +3904,11 @@ prisma@^5.4.1: dependencies: "@prisma/engines" "5.4.1" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -3778,6 +4003,19 @@ read-package-json-fast@^3.0.0: json-parse-even-better-errors "^3.0.0" npm-normalize-package-bin "^3.0.0" +readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + regenerator-runtime@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" @@ -3812,11 +4050,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -3838,6 +4081,11 @@ satori@^0.10.8: postcss-value-parser "^4.2.0" yoga-wasm-web "^0.3.3" +sax@^1.2.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -3845,6 +4093,13 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +seek-bzip@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== + dependencies: + commander "^2.8.1" + semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -3987,6 +4242,13 @@ string.prototype.codepointat@^0.2.1: resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: name strip-ansi-cjs version "6.0.1" @@ -4002,6 +4264,13 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + strip-json-comments@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-5.0.1.tgz#0d8b7d01b23848ed7dbdf4baaaa31a8250d8cfa0" @@ -4038,11 +4307,34 @@ tabbable@^6.2.0: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== +tar-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + tiny-inflate@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -4107,6 +4399,14 @@ ufo@^1.3.0: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.0.tgz#c92f8ac209daff607c57bbd75029e190930a0019" integrity sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw== +unbzip2-stream@^1.0.9: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + undici@5.22.1: version "5.22.1" resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" @@ -4167,6 +4467,11 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -4318,11 +4623,31 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yauzl@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"