diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index 5b4e03753fa..7ed2c4b0993 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -45,6 +45,20 @@ export type MonitorTlonOpts = { accountId?: string | null; }; +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" ? (value as Record) : null; +} + +function readString(record: Record | null, key: string): string | undefined { + const value = record?.[key]; + return typeof value === "string" ? value : undefined; +} + +function readNumber(record: Record | null, key: string): number | undefined { + const value = record?.[key]; + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + function formatErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } @@ -683,9 +697,10 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + const handleChannelsFirehose = async (event: unknown) => { try { - const nest = event?.nest; + const eventRecord = asRecord(event); + const nest = readString(eventRecord, "nest"); if (!nest) { return; } @@ -695,27 +710,36 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise(); - const handleChatFirehose = async (event: any) => { + const handleChatFirehose = async (event: unknown) => { try { // Handle DM invite lists (arrays) if (Array.isArray(event)) { @@ -874,16 +896,20 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + event: (event: unknown) => { try { + const eventRecord = asRecord(event); // Look for self profile updates - if (event?.self) { - const selfUpdate = event.self; - if (selfUpdate?.contact?.nickname?.value !== undefined) { - const newNickname = selfUpdate.contact.nickname.value || null; + if (eventRecord?.self) { + const selfUpdate = asRecord(eventRecord.self); + const contact = asRecord(selfUpdate?.contact); + const nickname = asRecord(contact?.nickname); + if (nickname && "value" in nickname) { + const newNickname = readString(nickname, "value") ?? null; if (newNickname !== botNickname) { botNickname = newNickname; runtime.log?.(`[tlon] Nickname updated: ${botNickname}`); } } } - } catch (error: any) { + } catch (error: unknown) { runtime.error?.(`[tlon] Error handling contacts event: ${formatErrorMessage(error)}`); } }, @@ -1103,14 +1132,15 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + event: async (event: unknown) => { try { + const eventRecord = asRecord(event); // Handle group/channel join events // Event structure: { group: { flag: "~host/group-name", ... }, channels: { ... } } - if (event && typeof event === "object") { + if (eventRecord) { // Check for new channels being added to groups - if (event.channels && typeof event.channels === "object") { - const channels = event.channels as Record; + const channels = asRecord(eventRecord.channels); + if (channels) { for (const [channelNest, _channelData] of Object.entries(channels)) { // Only monitor chat channels if (!channelNest.startsWith("chat/")) { @@ -1156,10 +1186,14 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise 0) { + for (const channelNest of joinChannels) { + if (typeof channelNest !== "string") { + continue; + } if (!channelNest.startsWith("chat/")) { continue; } @@ -1198,7 +1232,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { try { await processPendingInvites(data as Foreigns); - } catch (error: any) { + } catch (error: unknown) { runtime.error?.( `[tlon] Error handling foreigns event: ${formatErrorMessage(error)}`, ); @@ -1383,7 +1417,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise | null { + return value && typeof value === "object" ? (value as Record) : null; +} + +function readString(record: Record, key: string): string | undefined { + const value = record[key]; + return typeof value === "string" ? value : undefined; +} + // Helper to recursively extract text from inline content function renderInlineItem( - item: any, + item: unknown, options?: { linkMode?: "content-or-href" | "href"; allowBreak?: boolean; @@ -193,44 +202,52 @@ function renderInlineItem( if (typeof item === "string") { return item; } - if (!item || typeof item !== "object") { + const record = asRecord(item); + if (!record) { return ""; } - if (item.ship) { - return item.ship; + const ship = readString(record, "ship"); + if (ship) { + return ship; } - if ("sect" in item) { - return `@${item.sect || "all"}`; + const sect = readString(record, "sect"); + if (sect !== undefined) { + return `@${sect || "all"}`; } - if (options?.allowBreak && item.break !== undefined) { + if (options?.allowBreak && "break" in record) { return "\n"; } - if (item["inline-code"]) { - return `\`${item["inline-code"]}\``; + const inlineCode = readString(record, "inline-code"); + if (inlineCode) { + return `\`${inlineCode}\``; } - if (item.code) { - return `\`${item.code}\``; + const code = readString(record, "code"); + if (code) { + return `\`${code}\``; } - if (item.link && item.link.href) { - return options?.linkMode === "href" ? item.link.href : item.link.content || item.link.href; + const link = asRecord(record.link); + const linkHref = link ? readString(link, "href") : undefined; + if (linkHref) { + const linkContent = readString(link, "content"); + return options?.linkMode === "href" ? linkHref : linkContent || linkHref; } - if (item.bold && Array.isArray(item.bold)) { - return `**${extractInlineText(item.bold)}**`; + if (Array.isArray(record.bold)) { + return `**${extractInlineText(record.bold)}**`; } - if (item.italics && Array.isArray(item.italics)) { - return `*${extractInlineText(item.italics)}*`; + if (Array.isArray(record.italics)) { + return `*${extractInlineText(record.italics)}*`; } - if (item.strike && Array.isArray(item.strike)) { - return `~~${extractInlineText(item.strike)}~~`; + if (Array.isArray(record.strike)) { + return `~~${extractInlineText(record.strike)}~~`; } - if (options?.allowBlockquote && item.blockquote && Array.isArray(item.blockquote)) { - return `> ${extractInlineText(item.blockquote)}`; + if (options?.allowBlockquote && Array.isArray(record.blockquote)) { + return `> ${extractInlineText(record.blockquote)}`; } return ""; } -function extractInlineText(items: any[]): string { - return items.map((item: any) => renderInlineItem(item)).join(""); +function extractInlineText(items: readonly unknown[]): string { + return items.map((item) => renderInlineItem(item)).join(""); } export function extractMessageText(content: unknown): string { @@ -239,11 +256,16 @@ export function extractMessageText(content: unknown): string { } return content - .map((verse: any) => { + .map((verse) => { + const verseRecord = asRecord(verse); + if (!verseRecord) { + return ""; + } + // Handle inline content (text, ships, links, etc.) - if (verse.inline && Array.isArray(verse.inline)) { - return verse.inline - .map((item: any) => + if (Array.isArray(verseRecord.inline)) { + return verseRecord.inline + .map((item) => renderInlineItem(item, { linkMode: "href", allowBreak: true, @@ -254,38 +276,46 @@ export function extractMessageText(content: unknown): string { } // Handle block content (images, code blocks, etc.) - if (verse.block && typeof verse.block === "object") { - const block = verse.block; + const block = asRecord(verseRecord.block); + if (block) { + const image = asRecord(block.image); // Image blocks - if (block.image && block.image.src) { - const alt = block.image.alt ? ` (${block.image.alt})` : ""; - return `\n${block.image.src}${alt}\n`; + if (image) { + const imageSrc = readString(image, "src"); + if (imageSrc) { + const altText = readString(image, "alt"); + const alt = altText ? ` (${altText})` : ""; + return `\n${imageSrc}${alt}\n`; + } } // Code blocks - if (block.code && typeof block.code === "object") { - const lang = block.code.lang || ""; - const code = block.code.code || ""; + const codeBlock = asRecord(block.code); + if (codeBlock) { + const lang = readString(codeBlock, "lang") ?? ""; + const code = readString(codeBlock, "code") ?? ""; return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`; } // Header blocks - if (block.header && typeof block.header === "object") { + const header = asRecord(block.header); + if (header) { + const headerContent = Array.isArray(header.content) ? header.content : []; const text = - block.header.content - ?.map((item: any) => (typeof item === "string" ? item : "")) - .join("") || ""; + headerContent.map((item) => (typeof item === "string" ? item : "")).join("") || ""; return `\n## ${text}\n`; } // Cite/quote blocks - parse the reference structure - if (block.cite && typeof block.cite === "object") { - const cite = block.cite; + const cite = asRecord(block.cite); + if (cite) { + const chanCite = asRecord(cite.chan); // ChanCite - reference to a channel message - if (cite.chan && typeof cite.chan === "object") { - const { nest, where } = cite.chan; + if (chanCite) { + const nest = readString(chanCite, "nest"); + const where = readString(chanCite, "where"); // where is typically /msg/~author/timestamp const whereMatch = where?.match(/\/msg\/(~[a-z-]+)\/(.+)/); if (whereMatch) { @@ -296,18 +326,28 @@ export function extractMessageText(content: unknown): string { } // GroupCite - reference to a group - if (cite.group && typeof cite.group === "string") { - return `\n> [ref: group ${cite.group}]\n`; + const group = readString(cite, "group"); + if (group) { + return `\n> [ref: group ${group}]\n`; } // DeskCite - reference to an app/desk - if (cite.desk && typeof cite.desk === "object") { - return `\n> [ref: ${cite.desk.flag}]\n`; + const desk = asRecord(cite.desk); + if (desk) { + const flag = readString(desk, "flag"); + if (flag) { + return `\n> [ref: ${flag}]\n`; + } } // BaitCite - reference with group+graph context - if (cite.bait && typeof cite.bait === "object") { - return `\n> [ref: ${cite.bait.graph} in ${cite.bait.group}]\n`; + const bait = asRecord(cite.bait); + if (bait) { + const graph = readString(bait, "graph"); + const groupName = readString(bait, "group"); + if (graph && groupName) { + return `\n> [ref: ${graph} in ${groupName}]\n`; + } } return `\n> [quoted message]\n`;