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
@@ -21,6 +21,53 @@ export interface ModerationResult {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export type ModerationAction =
|
||||
| { readonly action: "kick"; readonly userId: number }
|
||||
| {
|
||||
readonly action: "mute";
|
||||
readonly userId: number;
|
||||
readonly duration: number;
|
||||
}
|
||||
| { readonly action: "unmute"; readonly userId: number }
|
||||
| {
|
||||
readonly action: "alert";
|
||||
readonly userId: number;
|
||||
readonly message: string;
|
||||
}
|
||||
| { readonly action: "room-kick"; readonly roomId: number }
|
||||
| {
|
||||
readonly action: "broadcast";
|
||||
readonly message: string;
|
||||
readonly type: "hotel" | "staff";
|
||||
};
|
||||
|
||||
export interface ModerationActionTransport {
|
||||
disconnectUser(userId: number): Promise<boolean>;
|
||||
muteUser(userId: number, duration: number): Promise<boolean>;
|
||||
unmuteUser(userId: number): Promise<boolean>;
|
||||
alertUser(userId: number, message: string): Promise<boolean>;
|
||||
kickAll(roomId: number): Promise<boolean>;
|
||||
hotelAlert(message: string): Promise<boolean>;
|
||||
staffAlert(message: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
// Execute one already-authorized moderation effect and expose delivery truth.
|
||||
export async function executeModerationAction(
|
||||
transport: ModerationActionTransport,
|
||||
input: ModerationAction,
|
||||
): Promise<boolean> {
|
||||
if (input.action === "kick") return transport.disconnectUser(input.userId);
|
||||
if (input.action === "mute")
|
||||
return transport.muteUser(input.userId, input.duration);
|
||||
if (input.action === "unmute") return transport.unmuteUser(input.userId);
|
||||
if (input.action === "alert")
|
||||
return transport.alertUser(input.userId, input.message);
|
||||
if (input.action === "room-kick") return transport.kickAll(input.roomId);
|
||||
return input.type === "hotel"
|
||||
? transport.hotelAlert(input.message)
|
||||
: transport.staffAlert(input.message);
|
||||
}
|
||||
|
||||
const OPENAI_MODERATIONS_URL = "https://api.openai.com/v1/moderations";
|
||||
// Bound the AI call so a slow/hung endpoint can't stall a server action.
|
||||
const OPENAI_TIMEOUT_MS = 5_000;
|
||||
|
||||
@@ -19,6 +19,28 @@ export interface TicketReplyDb {
|
||||
touchTicket(ticketId: bigint, updatedAt: Date): Promise<unknown>;
|
||||
}
|
||||
|
||||
const MAX_UNSIGNED_BIGINT = 18_446_744_073_709_551_615n;
|
||||
|
||||
// Canonicalize a SQL BIGINT identifier without passing through Number.
|
||||
export function canonicalTicketId(value: string | number | bigint): bigint {
|
||||
let parsed: bigint;
|
||||
try {
|
||||
if (
|
||||
typeof value === "number" &&
|
||||
(!Number.isSafeInteger(value) || value <= 0)
|
||||
) {
|
||||
throw new Error("unsafe identifier");
|
||||
}
|
||||
parsed = BigInt(String(value));
|
||||
} catch {
|
||||
throw new Error("invalid ticket identifier");
|
||||
}
|
||||
if (parsed <= 0n || parsed > MAX_UNSIGNED_BIGINT) {
|
||||
throw new Error("invalid ticket identifier");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function createOwnedTicketReply(
|
||||
db: TicketReplyDb,
|
||||
input: { ticketId: bigint; userId: number; content: string },
|
||||
|
||||
Reference in new issue
Block a user