feat(housekeeping): complete people moderation parity
This commit is contained in:
1 parent
3d385d1869
commit
29fe22297b
41 files changed
+3465
-580
No files matched your search
@@ -0,0 +1,185 @@
|
||||
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<readonly PeopleSearchCandidate[]>;
|
||||
guilds(
|
||||
context: HousekeepingCapabilityContext,
|
||||
term: string,
|
||||
limit: number,
|
||||
): Promise<readonly PeopleSearchCandidate[]>;
|
||||
tickets(
|
||||
context: HousekeepingCapabilityContext,
|
||||
term: string,
|
||||
limit: number,
|
||||
): Promise<readonly PeopleSearchCandidate[]>;
|
||||
}
|
||||
|
||||
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);
|
||||
Reference in new issue
Block a user