diff --git a/package.json b/package.json index c7318091f..10f443e73 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "blitzkit", "private": true, - "version": "1.22.6", + "version": "2.0.0", "description": "🎉 Tools for everything World of Tanks Blitz", "type": "module", "types": "./types.d.ts", diff --git a/packages/core/src/blitzkit/index.ts b/packages/core/src/blitzkit/index.ts index 58e5a45e6..ce31cc01b 100644 --- a/packages/core/src/blitzkit/index.ts +++ b/packages/core/src/blitzkit/index.ts @@ -27,7 +27,7 @@ export * from './modelDefinitions'; export * from './normalizeBoundingBox'; // export * from './parseBkni'; // export * from './patientFetch'; -// export * from './patientFetchJSON'; +export * from './patientFetchJSON'; export * from './progressiveStat'; export * from './provisionDefinitions'; export * from './resolveDpm'; diff --git a/packages/scripts/src/buildRatingLeaderboard.ts b/packages/scripts/src/buildRatingLeaderboard.ts deleted file mode 100644 index af54dc6b0..000000000 --- a/packages/scripts/src/buildRatingLeaderboard.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { - BkrlBaseEntry, - BkrlFormat, - BkrlSuperset1Entry, - BkrlWriteStream, - CdonValue, - CdonWriteStream, - getAccountInfo, - patientFetchJSON, - RatingInfo, - RatingNeighbors, - RatingPlayer, - RegionSubdomain, - regionSubdomainToRegion, - superCompress, -} from '@blitzkit/core'; -import { chunk } from 'lodash-es'; -import { argv } from 'process'; -import { commitAssets } from './core/github/commitAssets'; -import { FileChange } from './core/github/commitMultipleFiles'; - -/* - * Central North American Time (UTC-5): use 0 5 * * *. - * Central European Time (UTC+1): use 0 23 * * *. - * Central Asia Standard Time (UTC+7): use 0 17 * * *. - */ - -const production = argv.includes('--production'); -const target = argv - .find((arg) => arg.startsWith('--target=')) - ?.split('=')[1] as 'midnight' | 'latest' | undefined; -const regionSubdomains = argv - .find((arg) => arg.startsWith('--region=')) - ?.split('=')[1]; - -if (!target) throw new Error('Target parameter not specified'); -if (!regionSubdomains) throw new Error('Region parameter not specified'); - -regionSubdomains.split(',').forEach(async (regionSubdomain) => { - const region = regionSubdomainToRegion(regionSubdomain as RegionSubdomain); - - console.log(`Building ${target} rating leaderboard for ${region}`); - - const NEIGHBORS = 2 ** 8; - const PLAYERS = await patientFetchJSON( - `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`, - ).then((data) => (data.detail === undefined ? data.count : undefined)); - - if (PLAYERS === undefined) { - throw new Error('No current rating season running'); - } - - const players: Record = {}; - const leaderboard: BkrlBaseEntry[] = []; - let registered = 0; - - async function branchFromPlayer(id: number) { - const { neighbors } = await patientFetchJSON( - `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`, - ); - - const firstPlayer = neighbors[0]; - const lastPlayer = neighbors[neighbors.length - 1]; - - const branches: Promise[] = []; - if (!players[firstPlayer.number]) - branches.push(branchFromPlayer(firstPlayer.spa_id)); - if (!players[lastPlayer.number]) - branches.push(branchFromPlayer(lastPlayer.spa_id)); - - neighbors.forEach((neighbor) => { - if (players[neighbor.number]) return; - - players[neighbor.number] = neighbor; - registered++; - }); - - console.log( - `Registered ${registered} / ${PLAYERS} (${( - (registered / PLAYERS!) * - 100 - ).toFixed(2)}%)`, - ); - - await Promise.all(branches); - } - - async function branchFromLeague(id: number) { - const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>( - `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`, - ); - - const firstPlayer = result[0]; - const lastPlayer = result.at(-1)!; - - await Promise.all([ - branchFromPlayer(firstPlayer.spa_id), - branchFromPlayer(lastPlayer.spa_id), - ]); - } - - console.log('Branching from leagues...'); - await Promise.all([ - branchFromLeague(0), - branchFromLeague(1), - branchFromLeague(2), - branchFromLeague(3), - branchFromLeague(4), - ]); - - console.log('Converting to an array...'); - for (let index = 0; index < PLAYERS; index++) { - const listing = players[index + 1]; - if (listing) { - leaderboard[index] = { id: listing.spa_id, score: listing.score }; - } else { - console.warn(`Position ${index + 1} had no player`); - leaderboard[index] = { id: 0, score: 0 }; - } - } - - console.log('Requesting further stats...'); - const comprehensiveLeaderboard: BkrlSuperset1Entry[] = []; - let chunkIndex = 0; - for (const leaderboardChunk of chunk(leaderboard, 100)) { - console.log( - `Chunk ${chunkIndex + 1} of ${Math.ceil(leaderboard.length / 100)}`, - ); - - const stats = await getAccountInfo( - region, - leaderboardChunk.map(({ id }) => id), - ['statistics.rating'], - ); - - stats.forEach((stat, index) => { - const trueIndex = chunkIndex * 100 + index; - comprehensiveLeaderboard[trueIndex] = - leaderboardChunk[index].id === 0 || - stat?.statistics.rating === undefined - ? { - battles: 0, - damage: 0, - id: 0, - kills: 0, - score: 0, - survived: 0, - wins: 0, - } - : { - id: leaderboardChunk[index].id, - score: leaderboardChunk[index].score, - battles: stat.statistics.rating.battles, - damage: stat.statistics.rating.damage_dealt, - kills: stat.statistics.rating.frags, - survived: stat.statistics.rating.survived_battles, - wins: stat.statistics.rating.wins, - }; - }); - - chunkIndex++; - } - - const info = await patientFetchJSON( - `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`, - ); - const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain; - const infoContent = new CdonWriteStream().cdon( - info as unknown as CdonValue, - ).uint8Array; - const leaderboardWriteStream = new BkrlWriteStream(); - - leaderboardWriteStream.bkrl({ - format: BkrlFormat.Superset1, - entries: comprehensiveLeaderboard, - }); - - const changes: FileChange[] = [ - { - path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`, - content: superCompress(infoContent), - encoding: 'base64', - }, - { - path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`, - content: Buffer.from(leaderboardWriteStream.uint8Array).toString( - 'base64', - ), - encoding: 'base64', - }, - ]; - - commitAssets(`rating leaderboard ${regionSubdomain} ${target}`, changes); -}); diff --git a/packages/scripts/src/reviews.ts b/packages/scripts/src/reviews.ts index 655b8ab18..c3914b7d1 100644 --- a/packages/scripts/src/reviews.ts +++ b/packages/scripts/src/reviews.ts @@ -1,7 +1,6 @@ import { asset, - decodeProtobuf, - encodeProtobufToBase64, + encodePB64, Reviews, toUniqueId, Video, @@ -27,13 +26,7 @@ console.log('Finding reviews...'); const currentReviews = await fetch(asset('definitions/reviews.pb')) .then((response) => response.arrayBuffer()) - .then((buffer) => - decodeProtobuf( - 'reviews', - 'blitzkit.Reviews', - new Uint8Array(buffer), - ), - ); + .then((buffer) => Reviews.decode(new Uint8Array(buffer))); const auth = await google.auth.getClient({ scopes: ['https://www.googleapis.com/auth/youtube.readonly'], }); @@ -131,12 +124,10 @@ for (const tank of tanksSanitized) { } } -const content = await encodeProtobufToBase64( - 'reviews', - 'blitzkit.Reviews', - reviews, -); - await commitAssets('reviews', [ - { content, encoding: 'base64', path: 'definitions/reviews.pb' }, + { + content: encodePB64(Reviews, reviews), + encoding: 'base64', + path: 'definitions/reviews.pb', + }, ]); diff --git a/packages/website/src/components/Checks.tsx b/packages/website/src/components/Checks.tsx new file mode 100644 index 000000000..65d782d97 --- /dev/null +++ b/packages/website/src/components/Checks.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { idToRegion } from '@blitzkit/core'; +import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes'; +import { useEffect, useState } from 'react'; +import type { PatreonAuthResponse } from '../pages/auth/[provider]/_index'; +import { App } from '../stores/app'; +import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../stores/app/constants'; + +type Extension = + | { + status: 'ok'; + data: { + access_token: string; + account_id: number; + expires_at: number; + }; + } + | { + status: 'error'; + }; + +export function Checks() { + return ( + + + + ); +} + +/** + * Wargaming: 14 (refresh: 7) + * Patreon: 31 (refresh: 15) + */ +function Content() { + const logins = App.use((state) => state.logins); + const mutateApp = App.useMutation(); + const appStore = App.useStore(); + const policiesAgreementIndex = App.use( + (state) => state.policiesAgreementIndex, + ); + const isLegal = window.location.pathname.startsWith('/legal'); + const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false); + + useEffect(() => { + function unsubscribe() { + window.removeEventListener('scroll', show); + window.removeEventListener('pointermove', show); + window.removeEventListener('pointerdown', show); + } + + function show() { + setShowPoliciesAgreement(true); + unsubscribe(); + } + + if ( + policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX && + !isLegal + ) { + window.addEventListener('scroll', show); + window.addEventListener('pointermove', show); + window.addEventListener('pointerdown', show); + } + + return unsubscribe; + }, [policiesAgreementIndex, isLegal]); + + useEffect(() => { + if (logins.wargaming) { + const expiresIn = logins.wargaming.expires - Date.now(); + const expiresInDays = expiresIn / 1000 / 60 / 60 / 24; + + if (expiresInDays < 0) { + mutateApp((draft) => { + draft.logins.wargaming = undefined; + }); + } else if (expiresInDays < 7) { + const { wargaming } = appStore.getState().logins; + + if (!wargaming) return; + + fetch( + `https://api.worldoftanks.${idToRegion( + wargaming.id, + )}/wot/auth/prolongate/?application_id=${ + import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID + }&access_token=${wargaming.token}`, + ) + .then((response) => response.json() as Promise) + .then((json) => { + mutateApp((draft) => { + if (json.status === 'error') { + draft.logins.wargaming = undefined; + } else { + draft.logins.wargaming = { + id: wargaming.id, + token: json.data.access_token, + expires: json.data.expires_at * 1000, + }; + } + }); + }); + } + } + + if (logins.patreon) { + const expiresIn = logins.patreon.expires - Date.now(); + const expiresInDays = expiresIn / 1000 / 60 / 60 / 24; + + if (expiresInDays < 0) { + mutateApp((draft) => { + draft.logins.patreon = undefined; + }); + } else if (expiresInDays < 15) { + const { patreon } = appStore.getState().logins; + + if (!patreon) return; + + fetch(`/api/patreon/refresh/${patreon.refreshToken}`) + .then((response) => response.json() as Promise) + .then((data) => { + mutateApp((draft) => { + draft.logins.patreon = { + token: data.access_token, + refreshToken: data.refresh_token, + expires: Date.now() + data.expires_in * 1000, + }; + }); + }); + } + } + }); + + if (!showPoliciesAgreement) return null; + + return ( + + + + + + {policiesAgreementIndex === -1 + ? "BlitzKit's policies" + : "BlitzKit's policies have changed"} + + + + This website utilizes cookies to perform analytics and personalize + your experience. You can learn more through{' '} + our privacy policy. + You may opt out of personalized advertizement by visiting{' '} + WebChoices. By + using BlitzKit, you also agree to our{' '} + terms of service. + + + + + + + + ); +} diff --git a/packages/website/src/components/NavbarAccount/index.tsx b/packages/website/src/components/NavbarAccount/index.tsx index 9a97248ba..c06d751d5 100644 --- a/packages/website/src/components/NavbarAccount/index.tsx +++ b/packages/website/src/components/NavbarAccount/index.tsx @@ -1,5 +1,4 @@ import { patreonLoginUrl } from '@blitzkit/core'; -import { useStore } from '@nanostores/react'; import { PersonIcon } from '@radix-ui/react-icons'; import { Button, @@ -14,8 +13,6 @@ import { } from '@radix-ui/themes'; import { Suspense } from 'react'; import { App } from '../../stores/app'; -import { $patreonLogin } from '../../stores/patreonLogin'; -import { $wargamingLogin } from '../../stores/wargamingLogin'; import { BlitzKitTheme } from '../BlitzKitTheme'; import { PatreonIcon } from '../PatreonIcon'; import { WargamingAccountName } from '../WargamingAccountName'; @@ -23,8 +20,16 @@ import { WargamingIcon } from '../WargamingIcon'; import { WargamingLoginButton } from '../WargamingLoginButton'; export function NavbarAccount() { - const patreon = useStore($patreonLogin); - const wargaming = useStore($wargamingLogin); + return ( + + + + ); +} + +function Content() { + const logins = App.use((state) => state.logins); + const mutateApp = App.useMutation(); return ( @@ -41,13 +46,13 @@ export function NavbarAccount() { width="320px" onOpenAutoFocus={(event) => event.preventDefault()} > - {(patreon.token || wargaming.token) && ( + {(logins.patreon || logins.wargaming) && ( Logged in accounts - {patreon.token && ( + {logins.patreon && ( Patreon @@ -55,7 +60,9 @@ export function NavbarAccount() { color="red" variant="ghost" onClick={() => { - $patreonLogin.setKey('token', undefined); + mutateApp((draft) => { + draft.logins.patreon = undefined; + }); }} > Logout @@ -63,7 +70,7 @@ export function NavbarAccount() { )} - {wargaming.token && ( + {logins.wargaming && ( @@ -77,7 +84,9 @@ export function NavbarAccount() { color="red" variant="ghost" onClick={() => { - $wargamingLogin.setKey('token', undefined); + mutateApp((draft) => { + draft.logins.wargaming = undefined; + }); }} > Logout @@ -87,19 +96,19 @@ export function NavbarAccount() { )} - {!patreon.token !== !wargaming.token && ( + {!logins.patreon !== !logins.wargaming && ( )} - {(!patreon.token || !wargaming.token) && ( + {(!logins.patreon || !logins.wargaming) && ( Log in with... - {!patreon.token && ( + {!logins.patreon && (