feat: more login progress

This commit is contained in:
TrèsAbhi committed 2024-10-09 21:04:49 -05:00
1 parent 1e64cb9e7d
commit 513105a624
18 files changed
+290 -295

No files matched your search

+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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';
@@ -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<RatingInfo>(
`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<number, RatingPlayer> = {};
const leaderboard: BkrlBaseEntry[] = [];
let registered = 0;
async function branchFromPlayer(id: number) {
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
`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<void>[] = [];
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<RatingInfo & { detail: undefined }>(
`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);
});
+7 -16
View File
@@ -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>(
'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',
},
]);
+199
View File
@@ -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 (
<App.Provider>
<Content />
</App.Provider>
);
}
/**
* 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<Extension>)
.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<PatreonAuthResponse>)
.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 (
<Flex
align="end"
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'var(--color-overlay)',
zIndex: 2,
}}
>
<Flex
p="8"
style={{
width: '100%',
background: 'var(--color-panel-solid)',
}}
justify="center"
>
<Flex
align="start"
gap="4"
style={{
maxWidth: 640 * 2,
}}
direction="column"
>
<Flex direction="column" gap="2">
<Heading>
{policiesAgreementIndex === -1
? "BlitzKit's policies"
: "BlitzKit's policies have changed"}
</Heading>
<Text>
This website utilizes cookies to perform analytics and personalize
your experience. You can learn more through{' '}
<Link href="/docs/legal/privacy-policy">our privacy policy</Link>.
You may opt out of personalized advertizement by visiting{' '}
<Link href="https://optout.aboutads.info/">WebChoices</Link>. By
using BlitzKit, you also agree to our{' '}
<Link href="/docs/legal/terms-of-service">terms of service</Link>.
</Text>
</Flex>
<Button
mt="2"
onClick={() => {
appStore.setState({
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
});
setShowPoliciesAgreement(false);
}}
>
I agree
</Button>
</Flex>
</Flex>
</Flex>
);
}
@@ -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 (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
return (
<App.Provider>
@@ -41,13 +46,13 @@ export function NavbarAccount() {
width="320px"
onOpenAutoFocus={(event) => event.preventDefault()}
>
{(patreon.token || wargaming.token) && (
{(logins.patreon || logins.wargaming) && (
<Flex direction="column" gap="2">
<Text align="center" color="gray">
Logged in accounts
</Text>
{patreon.token && (
{logins.patreon && (
<Flex align="center" gap="2" justify="center">
<PatreonIcon width={15} height={15} />
<Text>Patreon</Text>
@@ -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() {
</Flex>
)}
{wargaming.token && (
{logins.wargaming && (
<Flex align="center" gap="2" justify="center">
<WargamingIcon width={15} height={15} />
<Text>
@@ -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() {
</Flex>
)}
{!patreon.token !== !wargaming.token && (
{!logins.patreon !== !logins.wargaming && (
<Inset side="x">
<Separator my="4" size="4" />
</Inset>
)}
{(!patreon.token || !wargaming.token) && (
{(!logins.patreon || !logins.wargaming) && (
<Flex direction="column" gap="2">
<Text align="center" color="gray">
Log in with...
</Text>
{!patreon.token && (
{!logins.patreon && (
<Link style={{ width: '100%' }} href={patreonLoginUrl()}>
<Button style={{ width: '100%' }} color="tomato">
<PatreonIcon width={15} height={15} /> Patreon
@@ -107,7 +116,7 @@ export function NavbarAccount() {
</Link>
)}
{!wargaming.token && <WargamingLoginButton />}
{!logins.wargaming && <WargamingLoginButton />}
</Flex>
)}
</Popover.Content>
+10 -3
View File
@@ -1,5 +1,4 @@
import { assertSecret, patreonLoginUrl } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { ArrowRightIcon } from '@radix-ui/react-icons';
import {
Badge,
@@ -11,12 +10,20 @@ import {
Text,
} from '@radix-ui/themes';
import { useAdExempt } from '../hooks/useAdExempt';
import { $patreonLogin } from '../stores/patreonLogin';
import { App } from '../stores/app';
import { PatreonIcon } from './PatreonIcon';
export function Plugs() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const exempt = useAdExempt();
const hasPatreon = useStore($patreonLogin).token !== undefined;
const hasPatreon = App.use((state) => state.logins.patreon !== undefined);
return (
<Flex
@@ -9,11 +9,12 @@ import {
Text,
} from '@radix-ui/themes';
import { useState } from 'react';
import { App } from '../../../../../../stores/app';
import { Duel } from '../../../../../../stores/duel';
import { $wargamingLogin } from '../../../../../../stores/wargamingLogin';
import { Stars } from '../../Stars';
export function VoteCaster() {
const appStore = App.useStore();
const tank = Duel.use((state) => state.protagonist.tank);
const [easiness, setEasiness] = useState<StarsInt | null>(null);
const [firepower, setFirepower] = useState<StarsInt | null>(null);
@@ -97,9 +98,9 @@ export function VoteCaster() {
survivability === null)
}
onClick={() => {
const wargaming = $wargamingLogin.get();
const { wargaming } = appStore.getState().logins;
if (!wargaming.token) {
if (!wargaming) {
return setShowDialogue(false);
}
@@ -1,10 +1,9 @@
import { formatCompact } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
import { memo } from 'react';
import { useTankVotes } from '../../../../../hooks/useTankVotes';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { $wargamingLogin } from '../../../../../stores/wargamingLogin';
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
import { StarRow } from './components/StarRow';
import { VoteCaster } from './components/VoteCaster';
@@ -13,7 +12,7 @@ export const Votes = memo(
() => {
const id = Duel.use((state) => state.protagonist.tank.id);
const votes = useTankVotes(id);
const wargaming = useStore($wargamingLogin);
const wargaming = App.use((state) => state.logins.wargaming);
return (
<Flex direction="column" gap="3">
@@ -1,22 +1,19 @@
import { getAccountInfo, idToRegion } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { Text } from '@radix-ui/themes';
import { useMemo } from 'react';
import usePromise from 'react-promise-suspense';
import { $wargamingLogin } from '../stores/wargamingLogin';
import { App } from '../stores/app';
export function WargamingAccountName() {
const wargaming = useStore($wargamingLogin);
const wargaming = App.use((state) => state.logins.wargaming);
const promise = useMemo(async () => {
if (!wargaming.token) return null;
if (!wargaming) return null;
const data = await getAccountInfo(
idToRegion(Number(wargaming.id)),
Number(wargaming.id),
idToRegion(wargaming.id),
wargaming.id,
[],
{
access_token: wargaming.token,
},
{ access_token: wargaming.token },
);
return data?.nickname as string | undefined;
+3 -4
View File
@@ -1,16 +1,15 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { $patreonLogin } from '../stores/patreonLogin';
import { App } from '../stores/app';
let cache: Record<string, boolean | Promise<boolean>> = {};
export function useAdExempt() {
const patreon = useStore($patreonLogin);
const patreon = App.use((state) => state.logins.patreon);
const [exempt, setExempt] = useState(false);
useEffect(() => {
(async () => {
if (!patreon.token) return setExempt(false);
if (!patreon) return setExempt(false);
if (!(patreon.token in cache)) {
cache[patreon.token] = new Promise<boolean>(async (resolve) => {
+3 -4
View File
@@ -3,18 +3,17 @@ import {
type BlitzkitResponse,
type TankVotes,
} from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { $wargamingLogin } from '../stores/wargamingLogin';
import { App } from '../stores/app';
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
export function useTankVotes(id: number) {
const [votes, setVotes] = useState<TankVotes | null>(null);
const wargaming = useStore($wargamingLogin);
const wargaming = App.use((state) => state.logins.wargaming);
useEffect(() => {
if (!wargaming.token) return;
if (!wargaming) return;
if (id in cache) {
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
@@ -1,6 +1,7 @@
---
import { Flex } from '@radix-ui/themes';
import { BlitzKitTheme } from '../components/BlitzKitTheme';
import { Checks } from '../components/Checks';
import Footer from '../components/Footer.astro';
import Navbar from '../components/Navbar.astro';
import Party3 from '../components/Party3.astro';
@@ -43,11 +44,9 @@ const isRoot = pathname === '/';
<Flex direction="column" minHeight="100vh">
<Navbar />
<!-- <Checks /> -->
<Checks client:only />
<!-- <Suspense fallback={(<PageLoader />)}> -->
<slot />
<!-- </Suspense> -->
<Footer />
</Flex>
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { $patreonLogin } from '../../../stores/patreonLogin';
import { App } from '../../../stores/app';
import { $wargamingLogin } from '../../../stores/wargamingLogin';
export const AUTH_PROVIDERS = ['wargaming', 'patreon'] as const;
@@ -25,6 +25,8 @@ interface AuthorizeProps {
}
export function Authorize({ provider }: AuthorizeProps) {
const mutateApp = App.useMutation();
useEffect(() => {
(async () => {
const searchParams = new URLSearchParams(window.location.search);
@@ -56,10 +58,12 @@ export function Authorize({ provider }: AuthorizeProps) {
);
const data = (await response.json()) as PatreonAuthResponse;
$patreonLogin.set({
token: data.access_token,
refreshToken: data.refresh_token,
expires: `${Date.now() + data.expires_in * 1000}`,
mutateApp((draft) => {
draft.logins.patreon = {
token: data.access_token,
refreshToken: data.refresh_token,
expires: Date.now() + data.expires_in * 1000,
};
});
break;
@@ -1,10 +1,13 @@
import { PageWrapper } from '../../../components/PageWrapper';
import { TankSearch } from '../../../components/TankSearch';
import { App } from '../../../stores/app';
export function Page() {
return (
<PageWrapper color="purple" size={1028}>
<TankSearch />
<App.Provider>
<TankSearch />
</App.Provider>
</PageWrapper>
);
}
+16 -4
View File
@@ -1,14 +1,26 @@
import { merge } from 'lodash-es';
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { createContextualStore } from '../../core/zustand/createContextualStore';
interface AppStore {
developerMode: boolean;
policiesAgreementIndex: number;
logins: {
wargaming?: { token: string; id: number; expires: number };
patreon?: { token: string; refreshToken: string; expires: number };
};
}
export const App = createContextualStore(() =>
create<AppStore>()(() => ({
developerMode: false,
policiesAgreementIndex: 0,
})),
create<AppStore>()(
persist(
subscribeWithSelector<AppStore>(() => ({
developerMode: false,
policiesAgreementIndex: -1,
logins: {},
})),
{ name: 'app', merge: (a, b) => merge(b, a) },
),
),
);
@@ -1,15 +0,0 @@
import { persistentMap } from '@nanostores/persistent';
type PatreonLogin =
| {
token: undefined;
}
| {
token: string;
refreshToken: string;
expires: string;
};
export const $patreonLogin = persistentMap<PatreonLogin>('patreon:', {
token: undefined,
});
@@ -1,15 +0,0 @@
import { persistentMap } from '@nanostores/persistent';
type WargamingLogin =
| {
token: undefined;
}
| {
token: string;
id: string;
expires: string;
};
export const $wargamingLogin = persistentMap<WargamingLogin>('wargaming:', {
token: undefined,
});