feat: api

This commit is contained in:
TrèsAbhi committed 2025-06-03 20:31:53 -05:00
1 parent 2dc4f12246
commit 7e11c1e5ce
26 files changed
+149 -866

No files matched your search

+2 -1
View File
@@ -6,6 +6,7 @@
"dependencies": {
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
"ts-proto": "^2.2.2"
"ts-proto": "^2.2.2",
"@blitzkit/closed": "workspace:*"
}
}
@@ -1,9 +1,7 @@
import { CatalogItem } from '@protos/blitz_items';
import { Metadata } from '@protos/blitz_metadata';
import { LocalizationResourcesComponent } from '@protos/blitz_static_localization_resources_component';
import { ProfileAvatarComponent } from '@protos/blitz_static_profile_avatar_component';
import { RemoteStorageComponent } from '@protos/blitz_static_remote_storage_component';
import { SellableComponent } from '@protos/blitz_static_sellable_component';
import { StuffUIComponent } from '@protos/blitz_static_stuff_ui_component';
import { BlitzkitAllAvatarsComponent } from '@protos/blitzkit_static_all_avatars_component';
import { load } from 'js-yaml';
import { minimatch } from 'minimatch';
import { CatalogItemAccessor } from 'packages/core/src/blitz/catalogItemAccessor';
@@ -12,12 +10,35 @@ interface LocalizationConfig {
namespaces: string[];
}
export abstract class MetadataAccessor {
abstract get(item: string): Promise<CatalogItemAccessor>;
export class MetadataAccessor {
items: Record<string, CatalogItem> = {};
abstract add(item: CatalogItemAccessor): void;
constructor(metadata: Metadata) {
for (const item of metadata.catalog_items) {
this.items[item.catalog_id] = item;
}
}
abstract filter(predicate: (item: string) => boolean): CatalogItemAccessor[];
get(item: string) {
if (item in this.items) return new CatalogItemAccessor(this.items[item]);
throw new Error(`Item ${item} does not exist in socket metadata`);
}
add(item: CatalogItemAccessor) {
this.items[item.id] = item.pack();
}
filter(predicate: (item: string) => boolean) {
const items: CatalogItemAccessor[] = [];
for (const item in this.items) {
if (predicate(item)) {
items.push(new CatalogItemAccessor(this.items[item]));
}
}
return items;
}
async strings(locale: string) {
const clientConfig = await this.get(
@@ -97,44 +118,4 @@ export abstract class MetadataAccessor {
return filtered;
}
injectBlitzkit() {
this.injectBlitzkitProfileAvatars();
return this;
}
injectBlitzkitProfileAvatars() {
const allAvatars: BlitzkitAllAvatarsComponent = { avatars: [] };
for (const item of this.filter((item) =>
item.startsWith('ProfileAvatarEntity.'),
)) {
const stuff = item.get(StuffUIComponent, 'UIComponent');
const avatar = item.get(ProfileAvatarComponent, 'profileAvatarComponent');
const sellable = item.getOptional(SellableComponent, 'sellableComponent');
allAvatars.avatars.push({
id: item.undiscriminatedId(),
name: stuff.display_name,
obtaining: stuff.obtaining_methods,
description: stuff.description,
grade: stuff.grade,
category: avatar.category,
hidden_if_not_obtained: avatar.hidden_if_not_obtained,
sale: sellable?.reward,
});
}
this.add(
CatalogItemAccessor.fromComponent(
'BlitzkitAllAvatarsEntity.blitzkit_all_avatars',
{
type_url:
'./blitzkit_static_all_avatars_component.BlitzkitAllAvatarsComponent',
value: BlitzkitAllAvatarsComponent.encode(allAvatars).finish(),
},
),
);
}
}
@@ -1,3 +0,0 @@
export * from './abstract';
export * from './remote';
export * from './socket';
@@ -1,30 +0,0 @@
import { CatalogItem } from '@protos/blitz_items';
import { CatalogItemAccessor } from '../catalogItemAccessor';
import { MetadataAccessor } from './abstract';
export class RemoteMetadataAccessor extends MetadataAccessor {
constructor(private base: string) {
super();
}
async get(item: string) {
const response = await fetch(`${this.base}/${item}.pb`);
if (!response.ok) {
throw new Error(`Item ${item} does not exist in remote metadata`);
}
const buffer = new Uint8Array(await response.arrayBuffer());
const catalogItem = CatalogItem.decode(buffer);
return new CatalogItemAccessor(catalogItem);
}
add() {
throw new Error('Cannot add items to remote metadata');
}
filter(): CatalogItemAccessor[] {
throw new Error('Cannot filter remote metadata');
}
}
@@ -1,37 +0,0 @@
import { CatalogItem } from '@protos/blitz_items';
import { Metadata } from '@protos/blitz_metadata';
import { CatalogItemAccessor } from '../catalogItemAccessor';
import { MetadataAccessor } from './abstract';
export class SocketMetadataAccessor extends MetadataAccessor {
items: Record<string, CatalogItem> = {};
constructor(metadata: Metadata) {
super();
for (const item of metadata.catalog_items) {
this.items[item.catalog_id] = item;
}
}
async get(item: string) {
if (item in this.items) return new CatalogItemAccessor(this.items[item]);
throw new Error(`Item ${item} does not exist in socket metadata`);
}
add(item: CatalogItemAccessor) {
this.items[item.id] = item.pack();
}
filter(predicate: (item: string) => boolean) {
const items: CatalogItemAccessor[] = [];
for (const item in this.items) {
if (predicate(item)) {
items.push(new CatalogItemAccessor(this.items[item]));
}
}
return items;
}
}
@@ -0,0 +1,8 @@
import { BlitzKitAvatars } from '@protos/blitzkit';
import { MetadataAccessor } from '../../blitz/metadata';
export abstract class BlitzkitAPI {
abstract metadata: MetadataAccessor;
abstract avatars(): Promise<BlitzKitAvatars>;
}
+15
View File
@@ -0,0 +1,15 @@
import { BlitzKitAvatars } from '@protos/blitzkit';
import { MetadataAccessor } from '../../blitz/metadata';
import { BlitzkitAPI } from './abstract';
export class BlitzkitClientAPI extends BlitzkitAPI {
get metadata(): MetadataAccessor {
throw new Error('Cannot access metadata from client');
}
async avatars() {
const response = await fetch('/api/internal/avatars.pb');
const buffer = await response.arrayBuffer();
return BlitzKitAvatars.decode(new Uint8Array(buffer));
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './abstract';
export * from './client';
export * from './server';
+37
View File
@@ -0,0 +1,37 @@
import { ProfileAvatarComponent } from '@protos/blitz_static_profile_avatar_component';
import { SellableComponent } from '@protos/blitz_static_sellable_component';
import { StuffUIComponent } from '@protos/blitz_static_stuff_ui_component';
import { BlitzKitAvatars } from '@protos/blitzkit';
import { BlitzkitAPI } from '.';
import { MetadataAccessor } from '../../blitz/metadata';
export class BlitzkitServerAPI extends BlitzkitAPI {
constructor(public metadata: MetadataAccessor) {
super();
}
async avatars() {
const avatars: BlitzKitAvatars = { avatars: [] };
for (const item of this.metadata.filter((item) =>
item.startsWith('ProfileAvatarEntity.'),
)) {
const stuff = item.get(StuffUIComponent, 'UIComponent');
const avatar = item.get(ProfileAvatarComponent, 'profileAvatarComponent');
const sellable = item.getOptional(SellableComponent, 'sellableComponent');
avatars.avatars.push({
id: item.undiscriminatedId(),
name: stuff.display_name,
obtaining: stuff.obtaining_methods,
description: stuff.description,
grade: stuff.grade,
category: avatar.category,
hidden_if_not_obtained: avatar.hidden_if_not_obtained,
sale: sellable?.reward,
});
}
return avatars;
}
}
+1
View File
@@ -41,6 +41,7 @@ export * from './uniqueGuns';
export * from './unwrapBlitzkitResponse';
export * from './votes';
// export * from './withCORSProxy';
export * from './api';
export * from './createDefaultProvisions';
export * from './gallery';
export * from './metaSortTank';
@@ -1,5 +0,0 @@
import { RemoteMetadataAccessor } from 'packages/core/src';
export const remoteMetadata = new RemoteMetadataAccessor(
'/api/internal/catalog',
);
@@ -1,6 +0,0 @@
import { fetchMetadata } from '@blitzkit/closed';
import { SocketMetadataAccessor } from 'packages/core/src';
export const socketMetadata = new SocketMetadataAccessor(
await fetchMetadata(),
).injectBlitzkit();
@@ -1,11 +0,0 @@
import type { MetadataAccessor } from 'packages/core/src';
let accessor: MetadataAccessor;
if (import.meta.env.SSR) {
accessor = await import('./metadata.socket').then((m) => m.socketMetadata);
} else {
accessor = await import('./metadata.remote').then((m) => m.remoteMetadata);
}
export const metadata = accessor;
@@ -1,11 +0,0 @@
let base: string;
if (import.meta.env.DEV) {
base = 'http://localhost:4321';
} else {
base = import.meta.env.SITE;
}
export function api(path: string) {
return `${base}/api/${path}`;
}
@@ -0,0 +1,3 @@
import { BlitzkitClientAPI } from '@blitzkit/core';
export const api = new BlitzkitClientAPI();
@@ -0,0 +1,11 @@
import type { BlitzkitAPI } from 'packages/core/src';
let api: BlitzkitAPI;
if (import.meta.env.SSR) {
api = await import('./server').then((module) => module.api);
} else {
api = await import('./client').then((module) => module.api);
}
export { api };
@@ -0,0 +1,6 @@
import { fetchMetadata } from '@blitzkit/closed';
import { BlitzkitServerAPI, MetadataAccessor } from '@blitzkit/core';
const metadata = await fetchMetadata();
const accessor = new MetadataAccessor(metadata);
export const api = new BlitzkitServerAPI(accessor);
@@ -1,11 +1,10 @@
import { BlitzkitAllAvatarsComponent } from '@protos/blitzkit_static_all_avatars_component';
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { Flex, Grid, Spinner, TextField } from '@radix-ui/themes';
import fuzzysort from 'fuzzysort';
import { debounce, times } from 'lodash-es';
import { AvatarGroup } from 'packages/website-ue/src/components/Avatars/Group';
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
import { metadata } from 'packages/website-ue/src/core/blitz/metadata';
import { api } from 'packages/website-ue/src/core/blitzkit/api';
import {
LocaleProvider,
useLocale,
@@ -14,13 +13,7 @@ import {
import type { MaybeSkeletonComponentProps } from 'packages/website-ue/src/types/maybeSkeletonComponentProps';
import { useCallback, useMemo, useRef, useState } from 'react';
const allAvatars = await metadata
.get('BlitzkitAllAvatarsEntity.blitzkit_all_avatars')
.then((entity) =>
entity
.get(BlitzkitAllAvatarsComponent, 'blitzkitAllAvatarsComponent')
.avatars.reverse(),
);
const { avatars } = await api.avatars();
export function Page({ localeData }: LocaleAcceptorProps) {
return (
@@ -39,7 +32,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
const [search, setSearch] = useState<string | null>(null);
const [searching, setSearching] = useState(false);
const groups = useMemo(() => {
const avatars = allAvatars
const avatarsSorted = avatars
.map((avatar) => ({
avatar,
name: gameStrings[avatar.name] as string | undefined,
@@ -52,9 +45,9 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
? -1
: a.name.localeCompare(b.name),
);
const groups = new Map<string | undefined, typeof avatars>();
const groups = new Map<string | undefined, typeof avatarsSorted>();
for (const avatar of avatars) {
for (const avatar of avatarsSorted) {
if (groups.has(avatar.name)) {
groups.get(avatar.name)!.push(avatar);
} else {
@@ -1,6 +1,6 @@
---
import { avatarsTool } from 'packages/website-ue/src/constants/tools';
import { metadata } from 'packages/website-ue/src/core/blitz/metadata';
import { api } from 'packages/website-ue/src/core/blitzkit/api';
import ToolLayout from 'packages/website-ue/src/layouts/ToolLayout.astro';
import { Page } from './_index';
@@ -8,11 +8,11 @@ export { getStaticPaths } from '../index.astro';
const { locale } = Astro.props;
const gameStrings = await metadata.stringsPartial(locale, [
const gameStrings = await api.metadata.stringsPartial(locale, [
'ProfileAvatarEntity__*',
]);
---
<ToolLayout tool={avatarsTool} {locale} >
<ToolLayout tool={avatarsTool} {locale}>
<Page localeData={{ locale, gameStrings }} client:load />
</ToolLayout>
@@ -1,29 +1,28 @@
import { ProfileAvatarComponent } from '@protos/blitz_static_profile_avatar_component';
import type { APIRoute, GetStaticPaths, GetStaticPathsItem } from 'astro';
import { socketMetadata } from 'packages/website-ue/src/core/blitz/metadata.socket';
import type { BlitzKitAvatar } from '@protos/blitzkit';
import type { APIRoute, GetStaticPaths } from 'astro';
import { api } from 'packages/website-ue/src/core/blitzkit/api';
export const getStaticPaths: GetStaticPaths = async () => {
const paths: GetStaticPathsItem[] = [];
for (const item in socketMetadata.items) {
if (!item.startsWith('ProfileAvatarEntity.')) continue;
const id = await socketMetadata
.get(item)
.then((item) => item.undiscriminatedId());
paths.push({ params: { id } });
}
return paths;
const { avatars } = await api.avatars();
return avatars.map((avatar) => ({
params: { id: avatar.id },
props: { avatar },
}));
};
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => {
const item = await socketMetadata.get(`ProfileAvatarEntity.${params.id}`);
export const GET: APIRoute<
{ avatar: BlitzKitAvatar },
{ id: string }
> = async ({ params }) => {
const item = api.metadata.get(`ProfileAvatarEntity.${params.id}`);
const avatar = item.get(ProfileAvatarComponent, 'profileAvatarComponent');
try {
return await fetch(avatar.avatar);
const response = await fetch(avatar.avatar);
const buffer = await response.arrayBuffer();
return new Response(buffer, { headers: response.headers });
} catch {
return new Response(null, { status: 404 });
}
@@ -0,0 +1,7 @@
import { BlitzKitAvatars } from '@protos/blitzkit';
import type { APIRoute } from 'astro';
import { api } from 'packages/website-ue/src/core/blitzkit/api';
export const GET: APIRoute = async () => {
return new Response(BlitzKitAvatars.encode(await api.avatars()).finish());
};
@@ -1,19 +0,0 @@
import type { APIRoute, GetStaticPaths, GetStaticPathsItem } from 'astro';
import { socketMetadata } from 'packages/website-ue/src/core/blitz/metadata.socket';
export const getStaticPaths: GetStaticPaths = () => {
const paths: GetStaticPathsItem[] = [];
for (const item in socketMetadata.items) {
paths.push({ params: { item } });
}
return paths;
};
export const GET: APIRoute<{}, { item: string }> = async ({ params }) => {
return new Response(
await socketMetadata.get(params.item).then((item) => item.encode()),
{ headers: { 'Content-Type': 'application/octet-stream' } },
);
};