WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 61a234b52b0d602c516e23eabd1dfe5a0f87fca6 111 lines 2.9 kB view raw
1import { UnicodeString } from "@atproto/api"; 2 3/** 4 * Parse a route parameter as BigInt. 5 * Returns null if the value cannot be parsed. 6 */ 7export function parseBigIntParam(value: string): bigint | null { 8 try { 9 return BigInt(value); 10 } catch (error) { 11 // BigInt throws RangeError or SyntaxError for invalid input 12 if (error instanceof RangeError || error instanceof SyntaxError) { 13 return null; 14 } 15 // Re-throw unexpected errors 16 throw error; 17 } 18} 19 20/** 21 * Validate post text according to lexicon constraints. 22 * - Max 300 graphemes (user-perceived characters) 23 * - Non-empty after trimming whitespace 24 */ 25export function validatePostText(text: unknown): { 26 valid: boolean; 27 trimmed?: string; 28 error?: string; 29} { 30 // Type guard: ensure text is a string 31 if (typeof text !== "string") { 32 return { valid: false, error: "Text is required and must be a string" }; 33 } 34 35 const trimmed = text.trim(); 36 37 if (trimmed.length === 0) { 38 return { valid: false, error: "Text cannot be empty" }; 39 } 40 41 const graphemeLength = new UnicodeString(trimmed).graphemeLength; 42 if (graphemeLength > 300) { 43 return { 44 valid: false, 45 error: "Text must be 300 characters or less", 46 }; 47 } 48 49 return { valid: true, trimmed }; 50} 51 52/** 53 * Validate topic title according to lexicon constraints. 54 * - Max 120 graphemes (user-perceived characters) 55 * - Non-empty after trimming whitespace 56 */ 57export function validateTopicTitle(title: unknown): { 58 valid: boolean; 59 trimmed?: string; 60 error?: string; 61} { 62 if (typeof title !== "string") { 63 return { valid: false, error: "Title is required and must be a string" }; 64 } 65 66 const trimmed = title.trim(); 67 68 if (trimmed.length === 0) { 69 return { valid: false, error: "Title cannot be empty" }; 70 } 71 72 const graphemeLength = new UnicodeString(trimmed).graphemeLength; 73 if (graphemeLength > 120) { 74 return { 75 valid: false, 76 error: "Title must be 120 characters or less", 77 }; 78 } 79 80 return { valid: true, trimmed }; 81} 82 83/** 84 * Validate that a parent post belongs to the same thread as the root. 85 * 86 * Rules: 87 * - Parent can BE the root (replying directly to topic) 88 * - Parent can be a reply in the same thread (parent.rootPostId === rootId) 89 * - Parent cannot belong to a different thread 90 */ 91export function validateReplyParent( 92 root: { id: bigint; rootPostId: bigint | null }, 93 parent: { id: bigint; rootPostId: bigint | null }, 94 rootId: bigint 95): { valid: boolean; error?: string } { 96 // Parent IS the root (replying to topic) 97 if (parent.id === rootId && parent.rootPostId === null) { 98 return { valid: true }; 99 } 100 101 // Parent is a reply in the same thread 102 if (parent.rootPostId === rootId) { 103 return { valid: true }; 104 } 105 106 // Parent belongs to a different thread 107 return { 108 valid: false, 109 error: "Parent post does not belong to this thread", 110 }; 111}