import { JetstreamCommitEventSchema, JetstreamIdentityEventSchema, JetstreamAccountEventSchema, PostRecordSchema, } from "./types.js"; import type { JetstreamCommitEvent, JetstreamIdentityEvent, JetstreamAccountEvent, PostData, PostUpdateData, PostDeleteData, KeywordPost, UserPost, UserRegistry, IdentityPayload, AccountPayload, PostView, PostViewInfo, } from "./types.js"; import { toBskyUrl } from "./formatting.js"; // --- Facet extraction --- export const extractFeatures = ( facets: any[] | undefined, type: string, field: string ): readonly string[] => facets ?.flatMap((f: any) => f.features ?? []) ?.filter((f: any) => f.$type === type) ?.map((f: any) => f[field]) ?? []; export const extractLinks = (facets: any[] | undefined): readonly string[] => extractFeatures(facets, "app.bsky.richtext.facet#link", "uri"); export const extractMentions = (facets: any[] | undefined): readonly string[] => extractFeatures(facets, "app.bsky.richtext.facet#mention", "did"); // --- Event kind guards (zod-backed) --- export const isCommitEvent = (event: unknown): event is JetstreamCommitEvent => JetstreamCommitEventSchema.safeParse(event).success; export const isIdentityEvent = ( event: unknown ): event is JetstreamIdentityEvent => JetstreamIdentityEventSchema.safeParse(event).success; export const isAccountEvent = ( event: unknown ): event is JetstreamAccountEvent => JetstreamAccountEventSchema.safeParse(event).success; // --- Collection-generic operation guards --- export const isCreate = (event: unknown, collection: string): boolean => { const result = JetstreamCommitEventSchema.safeParse(event); return ( result.success && result.data.commit.operation === "create" && result.data.commit.collection === collection ); }; export const isUpdate = (event: unknown, collection: string): boolean => { const result = JetstreamCommitEventSchema.safeParse(event); return ( result.success && result.data.commit.operation === "update" && result.data.commit.collection === collection ); }; export const isDelete = (event: unknown, collection: string): boolean => { const result = JetstreamCommitEventSchema.safeParse(event); return ( result.success && result.data.commit.operation === "delete" && result.data.commit.collection === collection ); }; // --- Post-specific guards --- export const isPostCreate = (event: unknown): boolean => isCreate(event, "app.bsky.feed.post"); export const isPostUpdate = (event: unknown): boolean => isUpdate(event, "app.bsky.feed.post"); export const isPostDelete = (event: unknown): boolean => isDelete(event, "app.bsky.feed.post"); // --- Shared post-field extraction --- const extractPostFields = ( did: string, rkey: string, record: Record ): Omit | null => { const parsed = PostRecordSchema.safeParse(record); if (!parsed.success) return null; return { text: parsed.data.text, langs: parsed.data.langs ?? [], links: [...extractLinks(parsed.data.facets)], mentionCount: extractMentions(parsed.data.facets).length, embedType: parsed.data.embed?.$type ?? null, createdAt: parsed.data.createdAt, }; }; // --- Post parsing --- export const parsePost = (event: unknown): PostData | null => { const result = JetstreamCommitEventSchema.safeParse(event); if (!result.success) return null; const { did, commit } = result.data; if (commit.operation !== "create" || commit.collection !== "app.bsky.feed.post") return null; const fields = extractPostFields(did, commit.rkey, commit.record); if (!fields) return null; return { did, rkey: commit.rkey, uri: buildAtUri(did, "app.bsky.feed.post", commit.rkey), ...fields, }; }; export const parsePostUpdate = (event: unknown): PostUpdateData | null => { const result = JetstreamCommitEventSchema.safeParse(event); if (!result.success) return null; const { did, commit } = result.data; if (commit.operation !== "update" || commit.collection !== "app.bsky.feed.post") return null; const fields = extractPostFields(did, commit.rkey, commit.record); if (!fields) return null; return { did, rkey: commit.rkey, uri: buildAtUri(did, "app.bsky.feed.post", commit.rkey), cid: commit.cid, ...fields, }; }; export const parsePostDelete = (event: unknown): PostDeleteData | null => { const result = JetstreamCommitEventSchema.safeParse(event); if (!result.success) return null; const { did, commit } = result.data; if (commit.operation !== "delete" || commit.collection !== "app.bsky.feed.post") return null; return { did, collection: commit.collection, rkey: commit.rkey, uri: buildAtUri(did, commit.collection, commit.rkey), }; }; // --- Identity & Account parsers --- export const parseIdentityEvent = (event: unknown): IdentityPayload | null => { const result = JetstreamIdentityEventSchema.safeParse(event); if (!result.success) return null; return result.data.identity; }; export const parseAccountEvent = (event: unknown): AccountPayload | null => { const result = JetstreamAccountEventSchema.safeParse(event); if (!result.success) return null; return result.data.account; }; // --- Keyword matching --- const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); export const findMatchingKeywords = ( text: string, keywords: readonly string[] ): string[] => keywords.filter((kw) => new RegExp(`\\b${escapeRegex(kw)}\\b`, "i").test(text) ); export const parseKeywordPost = ( event: unknown, keywords: readonly string[] ): KeywordPost | null => { const post = parsePost(event); if (!post) return null; const matched = findMatchingKeywords(post.text, keywords); if (matched.length === 0) return null; return { ...post, matchedKeywords: matched }; }; // --- User matching --- export const parseUserPost = ( event: unknown, registry: UserRegistry ): UserPost | null => { const result = JetstreamCommitEventSchema.safeParse(event); if (!result.success) return null; const { did, commit } = result.data; if (commit.operation !== "create" || commit.collection !== "app.bsky.feed.post") return null; const user = registry.get(did); if (!user) return null; const fields = extractPostFields(did, commit.rkey, commit.record); if (!fields) return null; return { did, rkey: commit.rkey, uri: buildAtUri(did, "app.bsky.feed.post", commit.rkey), ...fields, displayName: user.displayName || "???", handle: user.handle || "unknown", }; }; // --- User merging --- export const mergeUsers = ( existing: UserRegistry, actors: readonly any[] ): { merged: UserRegistry; newCount: number } => { const merged = new Map(existing); let newCount = 0; for (const actor of actors) { if (!merged.has(actor.did)) { merged.set(actor.did, { displayName: actor.displayName ?? "", handle: actor.handle, }); newCount++; } } return { merged, newCount }; }; // --- AT URI helpers --- export const buildAtUri = ( did: string, collection: string, rkey: string ): string => `at://${did}/${collection}/${rkey}`; export const parseAtUri = ( uri: string ): { did: string; collection: string; rkey: string } | null => { const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/); if (!match) return null; return { did: match[1], collection: match[2], rkey: match[3] }; }; // --- bsky.app URL parsing --- export const parseBskyUrl = ( url: string ): { id: string; rkey: string } | null => { const match = url.match(/bsky\.app\/profile\/([^/]+)\/post\/([^/?#]+)/); return match ? { id: match[1], rkey: match[2] } : null; }; // --- PostView info extraction --- export const extractPostViewInfo = (post: PostView): PostViewInfo => { const text: string = (post.record as any)?.text ?? "(no text)"; const displayName = post.author?.displayName || post.author?.handle || "???"; const handle = post.author?.handle || "unknown"; const did = post.author?.did ?? ""; const parsed = parseAtUri(post.uri ?? ""); const rkey = parsed?.rkey ?? ""; const bskyUrl = parsed ? toBskyUrl(parsed.did, parsed.rkey) : post.uri ?? ""; return { text, displayName, handle, did, rkey, bskyUrl, uri: post.uri }; };