mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
import type { VRChat } from "vrchat";
|
|
import type { FavoriteWorldFolder, World } from "../../shared/types/world";
|
|
import { httpStatusOf } from "./errors";
|
|
import { getFavoriteGroupWorlds, type WorldFavoriteGroupType } from "./rawEndpoints";
|
|
import { toWorld } from "./mappers";
|
|
import { cachedRead } from "./cachedRead";
|
|
import { worldStore } from "../store/worldStore";
|
|
import { broadcast } from "../windows";
|
|
import { cacheKeys, policies } from "../cache/policies";
|
|
|
|
export async function getWorld(worldId: string): Promise<World> {
|
|
const world = await cachedRead(cacheKeys.world(worldId), policies.world, async (vrc) => {
|
|
const { data } = await vrc.getWorld({ path: { worldId }, throwOnError: true });
|
|
return toWorld(data);
|
|
});
|
|
worldStore.addWorld(world);
|
|
return world;
|
|
}
|
|
|
|
type CachedFavorites = { worlds: World[]; folders: FavoriteWorldFolder[] };
|
|
|
|
export async function getFavoriteWorlds(userId: string): Promise<FavoriteWorldFolder[]> {
|
|
const { worlds, folders } = await cachedRead<CachedFavorites>(
|
|
cacheKeys.favoriteWorlds(userId),
|
|
policies.favoriteWorlds,
|
|
(vrc) => loadFavoriteWorlds(vrc, userId),
|
|
);
|
|
for (const w of worlds) worldStore.addWorld(w);
|
|
broadcast("world:favoriteFolders", { userId, folders, done: true });
|
|
return folders;
|
|
}
|
|
|
|
async function loadFavoriteWorlds(vrc: VRChat, userId: string): Promise<CachedFavorites> {
|
|
let groups;
|
|
try {
|
|
const { data } = await vrc.getFavoriteGroups({
|
|
query: { ownerId: userId, n: 100 },
|
|
throwOnError: true,
|
|
});
|
|
groups = data.filter((g): g is (typeof data)[number] & { type: WorldFavoriteGroupType } =>
|
|
isWorldGroupType(g.type),
|
|
);
|
|
} catch (err) {
|
|
if (isPrivateFavorites(err)) return { worlds: [], folders: [] };
|
|
throw err;
|
|
}
|
|
|
|
const worlds: World[] = [];
|
|
const seen = new Set<string>();
|
|
const members: { id: string; group: string }[] = [];
|
|
const names = new Map<string, string>();
|
|
for (const group of groups) {
|
|
if (group.displayName) names.set(group.name, group.displayName);
|
|
let raw;
|
|
try {
|
|
raw = await getFavoriteGroupWorlds(vrc, group.type, group.name, userId);
|
|
} catch (err) {
|
|
if (isPrivateFavorites(err)) continue;
|
|
throw err;
|
|
}
|
|
for (const rawWorld of raw) {
|
|
members.push({ id: rawWorld.id, group: group.name });
|
|
if (!seen.has(rawWorld.id)) {
|
|
seen.add(rawWorld.id);
|
|
const world = toWorld(rawWorld);
|
|
worlds.push(world);
|
|
worldStore.addWorld(world);
|
|
}
|
|
}
|
|
broadcast("world:favoriteFolders", {
|
|
userId,
|
|
folders: groupIntoFolders(members, names),
|
|
done: false,
|
|
});
|
|
}
|
|
|
|
const folders = groupIntoFolders(members, names);
|
|
broadcast("world:favoriteFolders", { userId, folders, done: true });
|
|
return { worlds, folders };
|
|
}
|
|
|
|
function isWorldGroupType(type: string): type is WorldFavoriteGroupType {
|
|
return type === "world" || type === "vrcPlusWorld";
|
|
}
|
|
|
|
function isPrivateFavorites(err: unknown): boolean {
|
|
const status = httpStatusOf(err);
|
|
return status === 401 || status === 403;
|
|
}
|
|
|
|
function groupIntoFolders(
|
|
members: { id: string; group: string }[],
|
|
names: Map<string, string>,
|
|
): FavoriteWorldFolder[] {
|
|
const order: string[] = [];
|
|
const byGroup = new Map<string, string[]>();
|
|
for (const { id, group } of members) {
|
|
const ids = byGroup.get(group);
|
|
if (ids) ids.push(id);
|
|
else {
|
|
byGroup.set(group, [id]);
|
|
order.push(group);
|
|
}
|
|
}
|
|
return order.map((name) => ({
|
|
name,
|
|
displayName: names.get(name) ?? prettyFolderName(name),
|
|
worldIds: byGroup.get(name)!,
|
|
}));
|
|
}
|
|
|
|
function prettyFolderName(key: string): string {
|
|
const m = /^worlds(\d+)$/.exec(key);
|
|
if (m) return `Group ${m[1]}`;
|
|
return key.charAt(0).toUpperCase() + key.slice(1);
|
|
}
|
|
|
|
export async function searchWorlds(query: string): Promise<World[]> {
|
|
const worlds = await cachedRead(
|
|
cacheKeys.worldSearch(query),
|
|
policies.worldSearch,
|
|
async (vrc) => {
|
|
const { data } = await vrc.searchWorlds({
|
|
query: { search: query, n: 25, sort: "relevance" as const },
|
|
throwOnError: true,
|
|
});
|
|
return data.map(toWorld);
|
|
},
|
|
);
|
|
for (const w of worlds) worldStore.addWorld(w);
|
|
return worlds;
|
|
}
|
|
|
|
export async function getUserWorlds(userId: string, isSelf: boolean): Promise<World[]> {
|
|
const worlds = await cachedRead(
|
|
cacheKeys.userWorlds(userId),
|
|
policies.userWorlds,
|
|
async (vrc) => {
|
|
const query = isSelf
|
|
? { user: "me" as const, releaseStatus: "all" as const, n: 50, sort: "updated" as const }
|
|
: { userId, releaseStatus: "public" as const, n: 50, sort: "updated" as const };
|
|
const { data } = await vrc.searchWorlds({ query, throwOnError: true });
|
|
return data.map(toWorld);
|
|
},
|
|
);
|
|
worldStore.setAuthorWorlds(userId, worlds);
|
|
return worlds;
|
|
}
|