mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-14 03:10:01 +00:00
✨ feat: World Discovery
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { VRChat, FavoritedWorld } from "vrchat";
|
||||
import type { VRChat, FavoritedWorld, LimitedWorld } from "vrchat";
|
||||
|
||||
// VRChat web routes that are missing from the SDK.
|
||||
|
||||
@@ -37,3 +37,90 @@ export async function getFavoriteGroupWorlds(
|
||||
}
|
||||
return worlds;
|
||||
}
|
||||
|
||||
interface InfoPushContentList {
|
||||
name: string | { fallback?: string };
|
||||
shortName?: string | null;
|
||||
tag?: string | null;
|
||||
sortHeading?: string;
|
||||
sortOrder?: string;
|
||||
sortOwnership?: string;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
interface InfoPushEntry {
|
||||
id: string;
|
||||
priority?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
data?: { contentList?: InfoPushContentList };
|
||||
}
|
||||
|
||||
export interface WorldCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
tag?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
export async function getWorldCategories(vrc: VRChat): Promise<WorldCategory[]> {
|
||||
const { data } = await vrc.client.get<InfoPushEntry[], unknown, true>({
|
||||
url: "/infoPush",
|
||||
query: { include: "user-all", require: "world-category" },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
// vrchat gives events the lowest priority number, but we want them first
|
||||
return data
|
||||
.filter((e) => e.data?.contentList)
|
||||
.sort((a, b) => {
|
||||
const ev = Number(isEvent(b)) - Number(isEvent(a));
|
||||
if (ev !== 0) return ev;
|
||||
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||
})
|
||||
.map((e) => {
|
||||
const cl = e.data!.contentList!;
|
||||
return {
|
||||
id: e.id,
|
||||
name: categoryName(cl),
|
||||
tag: cl.tag || undefined,
|
||||
sort: cl.sortHeading,
|
||||
order: cl.sortOrder,
|
||||
platform: cl.platform === "ThisPlatformSupported" ? undefined : cl.platform,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function isEvent(e: InfoPushEntry): boolean {
|
||||
const tag = e.data?.contentList?.tag ?? "";
|
||||
return tag.startsWith("admin_") || Boolean(e.startDate) || Boolean(e.endDate);
|
||||
}
|
||||
|
||||
function categoryName(cl: InfoPushContentList): string {
|
||||
if (typeof cl.name === "string") return cl.name;
|
||||
return cl.name.fallback ?? cl.shortName ?? "Worlds";
|
||||
}
|
||||
|
||||
export async function getCategoryWorlds(
|
||||
vrc: VRChat,
|
||||
category: WorldCategory,
|
||||
n: number,
|
||||
): Promise<LimitedWorld[]> {
|
||||
const tags = ["system_approved", category.tag].filter(Boolean).join(",");
|
||||
const { data } = await vrc.client.get<LimitedWorld[], unknown, true>({
|
||||
url: "/worlds",
|
||||
query: {
|
||||
releaseStatus: "public",
|
||||
sort: category.sort,
|
||||
order: category.order ?? "descending",
|
||||
tag: tags || undefined,
|
||||
featured: false,
|
||||
n,
|
||||
offset: 0,
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { VRChat } from "vrchat";
|
||||
import type { FavoriteWorldFolder, World } from "../../shared/types/world";
|
||||
import type { DiscoverCategory, FavoriteWorldFolder, World } from "../../shared/types/world";
|
||||
import { httpStatusOf } from "./errors";
|
||||
import { getFavoriteGroupWorlds, type WorldFavoriteGroupType } from "./rawEndpoints";
|
||||
import {
|
||||
getCategoryWorlds,
|
||||
getFavoriteGroupWorlds,
|
||||
getWorldCategories,
|
||||
type WorldFavoriteGroupType,
|
||||
} from "./rawEndpoints";
|
||||
import { toWorld } from "./mappers";
|
||||
import { cachedRead } from "./cachedRead";
|
||||
import { worldStore } from "../store/worldStore";
|
||||
@@ -131,6 +136,26 @@ export async function searchWorlds(query: string): Promise<World[]> {
|
||||
return worlds;
|
||||
}
|
||||
|
||||
const DISCOVER_ROW_SIZE = 12;
|
||||
|
||||
export async function getDiscover(): Promise<DiscoverCategory[]> {
|
||||
const categories = await cachedRead(cacheKeys.discover(), policies.discover, async (vrc) => {
|
||||
const cats = await getWorldCategories(vrc);
|
||||
const rows = await Promise.all(cats.map((cat) => loadCategory(vrc, cat).catch(() => null)));
|
||||
return rows.filter((r): r is DiscoverCategory => r !== null && r.worlds.length > 0);
|
||||
});
|
||||
for (const cat of categories) for (const w of cat.worlds) worldStore.addWorld(w);
|
||||
return categories;
|
||||
}
|
||||
|
||||
async function loadCategory(
|
||||
vrc: VRChat,
|
||||
cat: Awaited<ReturnType<typeof getWorldCategories>>[number],
|
||||
): Promise<DiscoverCategory> {
|
||||
const raw = await getCategoryWorlds(vrc, cat, DISCOVER_ROW_SIZE);
|
||||
return { id: cat.id, name: cat.name, worlds: raw.map(toWorld) };
|
||||
}
|
||||
|
||||
export async function getUserWorlds(userId: string, isSelf: boolean): Promise<World[]> {
|
||||
const worlds = await cachedRead(
|
||||
cacheKeys.userWorlds(userId),
|
||||
|
||||
Reference in New Issue
Block a user