import { UnicodeString } from "@atproto/api"; /** * Parse a route parameter as BigInt. * Returns null if the value cannot be parsed. */ export function parseBigIntParam(value: string): bigint | null { try { return BigInt(value); } catch (error) { // BigInt throws RangeError or SyntaxError for invalid input if (error instanceof RangeError || error instanceof SyntaxError) { return null; } // Re-throw unexpected errors throw error; } } /** * Validate post text according to lexicon constraints. * - Max 300 graphemes (user-perceived characters) * - Non-empty after trimming whitespace */ export function validatePostText(text: unknown): { valid: boolean; trimmed?: string; error?: string; } { // Type guard: ensure text is a string if (typeof text !== "string") { return { valid: false, error: "Text is required and must be a string" }; } const trimmed = text.trim(); if (trimmed.length === 0) { return { valid: false, error: "Text cannot be empty" }; } const graphemeLength = new UnicodeString(trimmed).graphemeLength; if (graphemeLength > 300) { return { valid: false, error: "Text must be 300 characters or less", }; } return { valid: true, trimmed }; } /** * Validate topic title according to lexicon constraints. * - Max 120 graphemes (user-perceived characters) * - Non-empty after trimming whitespace */ export function validateTopicTitle(title: unknown): { valid: boolean; trimmed?: string; error?: string; } { if (typeof title !== "string") { return { valid: false, error: "Title is required and must be a string" }; } const trimmed = title.trim(); if (trimmed.length === 0) { return { valid: false, error: "Title cannot be empty" }; } const graphemeLength = new UnicodeString(trimmed).graphemeLength; if (graphemeLength > 120) { return { valid: false, error: "Title must be 120 characters or less", }; } return { valid: true, trimmed }; } /** * Validate that a parent post belongs to the same thread as the root. * * Rules: * - Parent can BE the root (replying directly to topic) * - Parent can be a reply in the same thread (parent.rootPostId === rootId) * - Parent cannot belong to a different thread */ export function validateReplyParent( root: { id: bigint; rootPostId: bigint | null }, parent: { id: bigint; rootPostId: bigint | null }, rootId: bigint ): { valid: boolean; error?: string } { // Parent IS the root (replying to topic) if (parent.id === rootId && parent.rootPostId === null) { return { valid: true }; } // Parent is a reply in the same thread if (parent.rootPostId === rootId) { return { valid: true }; } // Parent belongs to a different thread return { valid: false, error: "Parent post does not belong to this thread", }; }