import "server-only"; import { PERMS } from "@/lib/permission-slugs"; import { authorizeHousekeeping } from "../../foundation/authorization"; import { satisfiesCapability } from "../../foundation/capability-context"; import { anyCapability, type CapabilityRequirement, fail, type HousekeepingCapabilityContext, type HousekeepingSearchProvider, ok, } from "../../foundation/contracts"; import { peopleCommunityQuery } from "./queries/community"; import { peopleSupportQuery } from "./queries/support"; import { peopleUsersQuery } from "./queries/users"; export const PEOPLE_SEARCH_PROVIDER_IDS = [ "people.users", "people.guilds", "people.tickets", ] as const; export interface PeopleSearchCandidate { readonly id: string; readonly title: string; readonly description?: string; readonly href: string; readonly capability: CapabilityRequirement; } export interface PeopleSearchAdapters { users( context: HousekeepingCapabilityContext, term: string, limit: number, ): Promise; guilds( context: HousekeepingCapabilityContext, term: string, limit: number, ): Promise; tickets( context: HousekeepingCapabilityContext, term: string, limit: number, ): Promise; } function safeHref(href: string): href is `/ase/${string}` { return ( href.startsWith("/ase/") && !href.includes("\\") && !Array.from(href).some((character) => { const code = character.codePointAt(0) ?? 0; return code < 32 || code === 127; }) ); } function boundedLimit(limit: number): number { return Number.isFinite(limit) ? Math.min(25, Math.max(1, Math.trunc(limit))) : 25; } function createProvider( id: (typeof PEOPLE_SEARCH_PROVIDER_IDS)[number], capability: CapabilityRequirement, load: PeopleSearchAdapters[keyof PeopleSearchAdapters], ): HousekeepingSearchProvider { return { id, owner: "people", capability, async search(context, input) { const authorization = authorizeHousekeeping(context, capability); if (!authorization.ok) return authorization; const limit = boundedLimit(input.limit); const term = input.term.normalize("NFC").trim().slice(0, 128); try { const candidates = await load(context, term, limit); return ok( candidates .filter( (item) => safeHref(item.href) && satisfiesCapability(context, item.capability), ) .slice(0, limit) .map((item) => ({ ...item, domain: "people" as const, type: "entity" as const, href: item.href as `/ase/${string}`, })), authorization.correlationId, ); } catch { return fail( "DEPENDENCY_UNAVAILABLE", "errors.housekeeping.dependencyUnavailable", authorization.correlationId, ); } }, }; } export function createPeopleSearchProviders( adapters: PeopleSearchAdapters, ): readonly HousekeepingSearchProvider[] { return [ createProvider( "people.users", anyCapability(PERMS.USERS_VIEW, PERMS.MOD_USERS_VIEW), adapters.users, ), createProvider( "people.guilds", anyCapability(PERMS.USERS_VIEW), adapters.guilds, ), createProvider( "people.tickets", anyCapability(PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW), adapters.tickets, ), ]; } const productionAdapters: PeopleSearchAdapters = { async users(context, term, limit) { const result = await peopleUsersQuery.run(context, { routeId: "people.users.list", list: { search: term, pageSize: limit, offset: 0, sort: "username" }, }); if (!result.ok || result.data.kind !== "users") { if (!result.ok) throw new Error(result.error.code); return []; } return result.data.page.items.map((user) => ({ id: `user-${user.id}`, title: user.username, description: `User #${user.id}`, href: user.href, capability: anyCapability(PERMS.USERS_VIEW, PERMS.MOD_USERS_VIEW), })); }, async guilds(context, term, limit) { const result = await peopleCommunityQuery.run(context, { routeId: "people.community.guilds", list: { search: term, pageSize: limit, offset: 0, sort: "name" }, }); if (!result.ok || result.data.kind !== "guilds") { if (!result.ok) throw new Error(result.error.code); return []; } return result.data.page.items.map((guild) => ({ id: `guild-${guild.id}`, title: guild.name, description: `${guild.memberCount} members`, href: guild.href, capability: anyCapability(PERMS.USERS_VIEW), })); }, async tickets(context, term, limit) { const result = await peopleSupportQuery.run(context, { routeId: "people.support.tickets", list: { search: term, pageSize: limit, offset: 0, sort: "id" }, }); if (!result.ok || result.data.kind !== "tickets") { if (!result.ok) throw new Error(result.error.code); return []; } return result.data.page.items.map((ticket) => ({ id: `${ticket.source}-ticket-${ticket.id}`, title: ticket.subject, description: `${ticket.status} ยท ${ticket.creatorUsername ?? "unknown"}`, href: ticket.href, capability: anyCapability(PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW), })); }, }; export const PEOPLE_SEARCH_PROVIDERS = createPeopleSearchProviders(productionAdapters);