fix(plugins): clean tlon lint types

This commit is contained in:
Vincent Koc
2026-04-06 15:05:17 +01:00
parent 04e360e7e8
commit 1b309fff71
2 changed files with 167 additions and 93 deletions

View File

@@ -45,6 +45,20 @@ export type MonitorTlonOpts = {
accountId?: string | null;
};
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function readString(record: Record<string, unknown> | null, key: string): string | undefined {
const value = record?.[key];
return typeof value === "string" ? value : undefined;
}
function readNumber(record: Record<string, unknown> | 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<v
});
// Firehose handler for all channel messages (/v2)
const handleChannelsFirehose = async (event: any) => {
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<v
return;
}
const response = event?.response;
const response = asRecord(eventRecord?.response);
if (!response) {
return;
}
// Handle post responses (new posts and replies)
const essay = response?.post?.["r-post"]?.set?.essay;
const memo = response?.post?.["r-post"]?.reply?.["r-reply"]?.set?.memo;
const post = asRecord(response.post);
const rPost = asRecord(post?.["r-post"]);
const set = asRecord(rPost?.set);
const reply = asRecord(rPost?.reply);
const replyPayload = asRecord(reply?.["r-reply"]);
const replySet = asRecord(replyPayload?.set);
const essay = asRecord(set?.essay);
const memo = asRecord(replySet?.memo);
if (!essay && !memo) {
return;
}
const content = memo || essay;
const isThreadReply = Boolean(memo);
const messageId = isThreadReply ? response?.post?.["r-post"]?.reply?.id : response?.post?.id;
const messageId = isThreadReply ? readString(reply, "id") : readString(post, "id");
if (!messageId) {
return;
}
if (!processedTracker.mark(messageId)) {
return;
}
const senderShip = normalizeShip(content.author ?? "");
const senderShip = normalizeShip(readString(content, "author") ?? "");
if (!senderShip || senderShip === botShipName) {
return;
}
@@ -728,15 +752,13 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
cacheMessage(nest, {
author: senderShip,
content: rawText,
timestamp: content.sent || Date.now(),
timestamp: readNumber(content, "sent") ?? Date.now(),
id: messageId,
});
// Get thread info early for participation check
const seal = isThreadReply
? response?.post?.["r-post"]?.reply?.["r-reply"]?.set?.seal
: response?.post?.["r-post"]?.set?.seal;
const parentId = seal?.["parent-id"] || seal?.parent || null;
const seal = isThreadReply ? asRecord(replySet?.seal) : asRecord(set?.seal);
const parentId = readString(seal, "parent-id") ?? readString(seal, "parent") ?? null;
// Check if we should respond:
// 1. Direct mention always triggers response
@@ -773,7 +795,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
messageId: messageId ?? "",
messageText: rawText,
messageContent: content.content,
timestamp: content.sent || Date.now(),
timestamp: readNumber(content, "sent") ?? Date.now(),
parentId: parentId ?? undefined,
isThreadReply,
},
@@ -806,11 +828,11 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
channelNest: nest,
hostShip: parsed?.hostShip,
channelName: parsed?.channelName,
timestamp: content.sent || Date.now(),
timestamp: readNumber(content, "sent") ?? Date.now(),
parentId,
isThreadReply,
});
} catch (error: any) {
} catch (error: unknown) {
runtime.error?.(`[tlon] Error handling channel firehose event: ${formatErrorMessage(error)}`);
}
};
@@ -819,7 +841,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
// Track which DM invites we've already processed to avoid duplicate accepts
const processedDmInvites = new Set<string>();
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<v
}
return;
}
if (!("whom" in event) || !("response" in event)) {
const eventRecord = asRecord(event);
if (!eventRecord) {
return;
}
const whom = event.whom; // DM partner ship or club ID
const messageId = event.id;
const response = event.response;
const whom = eventRecord.whom; // DM partner ship or club ID
const messageId = readString(eventRecord, "id");
const response = asRecord(eventRecord.response);
if (!messageId || !response) {
return;
}
// Handle add events (new messages)
const essay = response?.add?.essay;
const essay = asRecord(asRecord(response.add)?.essay);
if (!essay) {
return;
}
@@ -892,7 +918,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
return;
}
const authorShip = normalizeShip(essay.author ?? "");
const authorShip = normalizeShip(readString(essay, "author") ?? "");
const partnerShip = extractDmPartnerShip(whom);
const senderShip = partnerShip || authorShip;
@@ -950,7 +976,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
messageText: resolvedMessageText,
messageContent: essay.content,
isGroup: false,
timestamp: essay.sent || Date.now(),
timestamp: readNumber(essay, "sent") ?? Date.now(),
});
return;
}
@@ -967,7 +993,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
messageId: messageId ?? "",
messageText,
messageContent: essay.content,
timestamp: essay.sent || Date.now(),
timestamp: readNumber(essay, "sent") ?? Date.now(),
},
});
await queueApprovalRequest(approval);
@@ -988,9 +1014,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
senderShip,
messageContent: essay.content, // Pass raw content for media extraction
isGroup: false,
timestamp: essay.sent || Date.now(),
timestamp: readNumber(essay, "sent") ?? Date.now(),
});
} catch (error: any) {
} catch (error: unknown) {
runtime.error?.(`[tlon] Error handling chat firehose event: ${formatErrorMessage(error)}`);
}
};
@@ -1030,20 +1056,23 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
await api.subscribe({
app: "contacts",
path: "/v1/news",
event: (event: any) => {
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<v
await api.subscribe({
app: "groups",
path: "/groups/ui",
event: async (event: any) => {
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<string, any>;
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<v
}
// Also check for the "join" event structure
if (event.join && typeof event.join === "object") {
const join = event.join as { group?: string; channels?: string[] };
if (join.channels) {
for (const channelNest of join.channels) {
const join = asRecord(eventRecord.join);
if (join) {
const joinChannels = Array.isArray(join.channels) ? join.channels : [];
if (joinChannels.length > 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<v
}
}
}
} catch (error: any) {
} catch (error: unknown) {
runtime.error?.(`[tlon] Error handling groups-ui event: ${formatErrorMessage(error)}`);
}
},
@@ -1329,7 +1363,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
void (async () => {
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<v
}
}
}
} catch (error: any) {
} catch (error: unknown) {
runtime.error?.(`[tlon] Channel refresh error: ${formatErrorMessage(error)}`);
}
}
@@ -1409,7 +1443,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
} finally {
try {
await api?.close();
} catch (error: any) {
} catch (error: unknown) {
runtime.error?.(`[tlon] Cleanup error: ${formatErrorMessage(error)}`);
}
}

View File

@@ -181,9 +181,18 @@ export async function resolveAuthorizedMessageText(params: {
return citedContent + rawText;
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function readString(record: Record<string, unknown>, 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`;