diff --git a/.oxlintrc.json b/.oxlintrc.json index 67cfab083..d5dfde5b4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -31,7 +31,6 @@ "src/canvas-host/a2ui/a2ui.bundle.js", "Swabble/", "vendor/", - "extensions/", "ui/" ] } diff --git a/.pi/extensions/diff.ts b/.pi/extensions/diff.ts index 5f673d57f..037fa240a 100644 --- a/.pi/extensions/diff.ts +++ b/.pi/extensions/diff.ts @@ -50,20 +50,30 @@ export default function (pi: ExtensionAPI) { const files: FileInfo[] = []; for (const line of lines) { - if (line.length < 4) continue; // Need at least "XY f" + if (line.length < 4) { + continue; + } // Need at least "XY f" const status = line.slice(0, 2); const file = line.slice(2).trimStart(); // Translate status codes to short labels let statusLabel: string; - if (status.includes("M")) statusLabel = "M"; - else if (status.includes("A")) statusLabel = "A"; - else if (status.includes("D")) statusLabel = "D"; - else if (status.includes("?")) statusLabel = "?"; - else if (status.includes("R")) statusLabel = "R"; - else if (status.includes("C")) statusLabel = "C"; - else statusLabel = status.trim() || "~"; + if (status.includes("M")) { + statusLabel = "M"; + } else if (status.includes("A")) { + statusLabel = "A"; + } else if (status.includes("D")) { + statusLabel = "D"; + } else if (status.includes("?")) { + statusLabel = "?"; + } else if (status.includes("R")) { + statusLabel = "R"; + } else if (status.includes("C")) { + statusLabel = "C"; + } else { + statusLabel = status.trim() || "~"; + } files.push({ status: statusLabel, statusLabel, file }); } diff --git a/.pi/extensions/files.ts b/.pi/extensions/files.ts index 02aa4b4aa..bba2760d0 100644 --- a/.pi/extensions/files.ts +++ b/.pi/extensions/files.ts @@ -40,7 +40,9 @@ export default function (pi: ExtensionAPI) { const toolCalls = new Map(); for (const entry of branch) { - if (entry.type !== "message") continue; + if (entry.type !== "message") { + continue; + } const msg = entry.message; if (msg.role === "assistant" && Array.isArray(msg.content)) { @@ -62,12 +64,16 @@ export default function (pi: ExtensionAPI) { const fileMap = new Map(); for (const entry of branch) { - if (entry.type !== "message") continue; + if (entry.type !== "message") { + continue; + } const msg = entry.message; if (msg.role === "toolResult") { const toolCall = toolCalls.get(msg.toolCallId); - if (!toolCall) continue; + if (!toolCall) { + continue; + } const { path, name } = toolCall; const timestamp = msg.timestamp; @@ -94,7 +100,9 @@ export default function (pi: ExtensionAPI) { } // Sort by most recent first - const files = Array.from(fileMap.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp); + const files = Array.from(fileMap.values()).toSorted( + (a, b) => b.lastTimestamp - a.lastTimestamp, + ); const openSelected = async (file: FileEntry): Promise => { try { @@ -118,9 +126,15 @@ export default function (pi: ExtensionAPI) { // Build select items with colored operations const items: SelectItem[] = files.map((f) => { const ops: string[] = []; - if (f.operations.has("read")) ops.push(theme.fg("muted", "R")); - if (f.operations.has("write")) ops.push(theme.fg("success", "W")); - if (f.operations.has("edit")) ops.push(theme.fg("warning", "E")); + if (f.operations.has("read")) { + ops.push(theme.fg("muted", "R")); + } + if (f.operations.has("write")) { + ops.push(theme.fg("success", "W")); + } + if (f.operations.has("edit")) { + ops.push(theme.fg("warning", "E")); + } const opsLabel = ops.join(""); return { value: f, diff --git a/.pi/extensions/prompt-url-widget.ts b/.pi/extensions/prompt-url-widget.ts index a988f937d..2bb56b104 100644 --- a/.pi/extensions/prompt-url-widget.ts +++ b/.pi/extensions/prompt-url-widget.ts @@ -47,7 +47,9 @@ async function fetchGhMetadata( try { const result = await pi.exec("gh", args); - if (result.code !== 0 || !result.stdout) return undefined; + if (result.code !== 0 || !result.stdout) { + return undefined; + } return JSON.parse(result.stdout) as GhMetadata; } catch { return undefined; @@ -55,12 +57,20 @@ async function fetchGhMetadata( } function formatAuthor(author?: GhMetadata["author"]): string | undefined { - if (!author) return undefined; + if (!author) { + return undefined; + } const name = author.name?.trim(); const login = author.login?.trim(); - if (name && login) return `${name} (@${login})`; - if (login) return `@${login}`; - if (name) return name; + if (name && login) { + return `${name} (@${login})`; + } + if (login) { + return `@${login}`; + } + if (name) { + return name; + } return undefined; } @@ -77,7 +87,9 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) { const urlLine = thm.fg("dim", match.url); const lines = [titleText]; - if (authorLine) lines.push(authorLine); + if (authorLine) { + lines.push(authorLine); + } lines.push(urlLine); const container = new Container(); @@ -103,7 +115,9 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) { }; pi.on("before_agent_start", async (event, ctx) => { - if (!ctx.hasUI) return; + if (!ctx.hasUI) { + return; + } const match = extractPromptMatch(event.prompt); if (!match) { return; @@ -124,8 +138,12 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) { }); const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => { - if (!content) return ""; - if (typeof content === "string") return content; + if (!content) { + return ""; + } + if (typeof content === "string") { + return content; + } return ( content .filter((block): block is { type: "text"; text: string } => block.type === "text") @@ -135,11 +153,15 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) { }; const rebuildFromSession = (ctx: ExtensionContext) => { - if (!ctx.hasUI) return; + if (!ctx.hasUI) { + return; + } const entries = ctx.sessionManager.getEntries(); - const lastMatch = [...entries].reverse().find((entry) => { - if (entry.type !== "message" || entry.message.role !== "user") return false; + const lastMatch = [...entries].toReversed().find((entry) => { + if (entry.type !== "message" || entry.message.role !== "user") { + return false; + } const text = getUserText(entry.message.content); return !!extractPromptMatch(text); }); diff --git a/.pi/extensions/redraws.ts b/.pi/extensions/redraws.ts index a1c45b8fa..6331f5eab 100644 --- a/.pi/extensions/redraws.ts +++ b/.pi/extensions/redraws.ts @@ -11,7 +11,9 @@ export default function (pi: ExtensionAPI) { pi.registerCommand("tui", { description: "Show TUI stats", handler: async (_args, ctx) => { - if (!ctx.hasUI) return; + if (!ctx.hasUI) { + return; + } let redraws = 0; await ctx.ui.custom((tui, _theme, _keybindings, done) => { redraws = tui.fullRedraws; diff --git a/extensions/bluebubbles/src/accounts.ts b/extensions/bluebubbles/src/accounts.ts index 290081075..04320701e 100644 --- a/extensions/bluebubbles/src/accounts.ts +++ b/extensions/bluebubbles/src/accounts.ts @@ -13,19 +13,25 @@ export type ResolvedBlueBubblesAccount = { function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.bluebubbles?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string { const ids = listBlueBubblesAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -34,7 +40,9 @@ function resolveAccountConfig( accountId: string, ): BlueBubblesAccountConfig | undefined { const accounts = cfg.channels?.bluebubbles?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as BlueBubblesAccountConfig | undefined; } diff --git a/extensions/bluebubbles/src/actions.ts b/extensions/bluebubbles/src/actions.ts index fa5bc1ff5..4d03d980d 100644 --- a/extensions/bluebubbles/src/actions.ts +++ b/extensions/bluebubbles/src/actions.ts @@ -9,7 +9,6 @@ import { type ChannelMessageActionAdapter, type ChannelMessageActionName, type ChannelToolSend, - type OpenClawConfig, } from "openclaw/plugin-sdk"; import { resolveBlueBubblesAccount } from "./accounts.js"; @@ -34,8 +33,12 @@ const providerId = "bluebubbles"; function mapTarget(raw: string): BlueBubblesSendTarget { const parsed = parseBlueBubblesTarget(raw); - if (parsed.kind === "chat_guid") return { kind: "chat_guid", chatGuid: parsed.chatGuid }; - if (parsed.kind === "chat_id") return { kind: "chat_id", chatId: parsed.chatId }; + if (parsed.kind === "chat_guid") { + return { kind: "chat_guid", chatGuid: parsed.chatGuid }; + } + if (parsed.kind === "chat_id") { + return { kind: "chat_id", chatId: parsed.chatId }; + } if (parsed.kind === "chat_identifier") { return { kind: "chat_identifier", chatIdentifier: parsed.chatIdentifier }; } @@ -52,11 +55,17 @@ function readMessageText(params: Record): string | undefined { function readBooleanParam(params: Record, key: string): boolean | undefined { const raw = params[key]; - if (typeof raw === "boolean") return raw; + if (typeof raw === "boolean") { + return raw; + } if (typeof raw === "string") { const trimmed = raw.trim().toLowerCase(); - if (trimmed === "true") return true; - if (trimmed === "false") return false; + if (trimmed === "true") { + return true; + } + if (trimmed === "false") { + return false; + } } return undefined; } @@ -66,41 +75,55 @@ const SUPPORTED_ACTIONS = new Set(BLUEBUBBLES_ACTION_N export const bluebubblesMessageActions: ChannelMessageActionAdapter = { listActions: ({ cfg }) => { - const account = resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig }); - if (!account.enabled || !account.configured) return []; - const gate = createActionGate((cfg as OpenClawConfig).channels?.bluebubbles?.actions); + const account = resolveBlueBubblesAccount({ cfg: cfg }); + if (!account.enabled || !account.configured) { + return []; + } + const gate = createActionGate(cfg.channels?.bluebubbles?.actions); const actions = new Set(); const macOS26 = isMacOS26OrHigher(account.accountId); for (const action of BLUEBUBBLES_ACTION_NAMES) { const spec = BLUEBUBBLES_ACTIONS[action]; - if (!spec?.gate) continue; - if (spec.unsupportedOnMacOS26 && macOS26) continue; - if (gate(spec.gate)) actions.add(action); + if (!spec?.gate) { + continue; + } + if (spec.unsupportedOnMacOS26 && macOS26) { + continue; + } + if (gate(spec.gate)) { + actions.add(action); + } } return Array.from(actions); }, supportsAction: ({ action }) => SUPPORTED_ACTIONS.has(action), extractToolSend: ({ args }): ChannelToolSend | null => { const action = typeof args.action === "string" ? args.action.trim() : ""; - if (action !== "sendMessage") return null; + if (action !== "sendMessage") { + return null; + } const to = typeof args.to === "string" ? args.to : undefined; - if (!to) return null; + if (!to) { + return null; + } const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined; return { to, accountId }; }, handleAction: async ({ action, params, cfg, accountId, toolContext }) => { const account = resolveBlueBubblesAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId: accountId ?? undefined, }); const baseUrl = account.config.serverUrl?.trim(); const password = account.config.password?.trim(); - const opts = { cfg: cfg as OpenClawConfig, accountId: accountId ?? undefined }; + const opts = { cfg: cfg, accountId: accountId ?? undefined }; // Helper to resolve chatGuid from various params or session context const resolveChatGuid = async (): Promise => { const chatGuid = readStringParam(params, "chatGuid"); - if (chatGuid?.trim()) return chatGuid.trim(); + if (chatGuid?.trim()) { + return chatGuid.trim(); + } const chatIdentifier = readStringParam(params, "chatIdentifier"); const chatId = readNumberParam(params, "chatId", { integer: true }); @@ -185,8 +208,12 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = { readStringParam(params, "message"); if (!rawMessageId || !newText) { const missing: string[] = []; - if (!rawMessageId) missing.push("messageId (the message ID to edit)"); - if (!newText) missing.push("text (the new message content)"); + if (!rawMessageId) { + missing.push("messageId (the message ID to edit)"); + } + if (!newText) { + missing.push("text (the new message content)"); + } throw new Error( `BlueBubbles edit requires: ${missing.join(", ")}. ` + `Use action=edit with messageId=, text=.`, @@ -234,9 +261,15 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = { const to = readStringParam(params, "to") ?? readStringParam(params, "target"); if (!rawMessageId || !text || !to) { const missing: string[] = []; - if (!rawMessageId) missing.push("messageId (the message ID to reply to)"); - if (!text) missing.push("text or message (the reply message content)"); - if (!to) missing.push("to or target (the chat target)"); + if (!rawMessageId) { + missing.push("messageId (the message ID to reply to)"); + } + if (!text) { + missing.push("text or message (the reply message content)"); + } + if (!to) { + missing.push("to or target (the chat target)"); + } throw new Error( `BlueBubbles reply requires: ${missing.join(", ")}. ` + `Use action=reply with messageId=, message=, target=.`, @@ -262,12 +295,17 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = { const effectId = readStringParam(params, "effectId") ?? readStringParam(params, "effect"); if (!text || !to || !effectId) { const missing: string[] = []; - if (!text) missing.push("text or message (the message content)"); - if (!to) missing.push("to or target (the chat target)"); - if (!effectId) + if (!text) { + missing.push("text or message (the message content)"); + } + if (!to) { + missing.push("to or target (the chat target)"); + } + if (!effectId) { missing.push( "effectId or effect (e.g., slam, loud, gentle, invisible-ink, confetti, lasers, fireworks, balloons, heart)", ); + } throw new Error( `BlueBubbles sendWithEffect requires: ${missing.join(", ")}. ` + `Use action=sendWithEffect with message=, target=, effectId=.`, diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index 146bae833..8a9bce52e 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -31,7 +31,9 @@ function sanitizeFilename(input: string | undefined, fallback: string): string { function ensureExtension(filename: string, extension: string, fallbackBase: string): string { const currentExt = path.extname(filename); - if (currentExt.toLowerCase() === extension) return filename; + if (currentExt.toLowerCase() === extension) { + return filename; + } const base = currentExt ? filename.slice(0, -currentExt.length) : filename; return `${base || fallbackBase}${extension}`; } @@ -54,8 +56,12 @@ function resolveAccount(params: BlueBubblesAttachmentOpts) { }); const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim(); const password = params.password?.trim() || account.config.password?.trim(); - if (!baseUrl) throw new Error("BlueBubbles serverUrl is required"); - if (!password) throw new Error("BlueBubbles password is required"); + if (!baseUrl) { + throw new Error("BlueBubbles serverUrl is required"); + } + if (!password) { + throw new Error("BlueBubbles password is required"); + } return { baseUrl, password }; } @@ -64,7 +70,9 @@ export async function downloadBlueBubblesAttachment( opts: BlueBubblesAttachmentOpts & { maxBytes?: number } = {}, ): Promise<{ buffer: Uint8Array; contentType?: string }> { const guid = attachment.guid?.trim(); - if (!guid) throw new Error("BlueBubbles attachment guid is required"); + if (!guid) { + throw new Error("BlueBubbles attachment guid is required"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ baseUrl, @@ -110,7 +118,9 @@ function resolveSendTarget(raw: string): BlueBubblesSendTarget { } function extractMessageId(payload: unknown): string { - if (!payload || typeof payload !== "object") return "unknown"; + if (!payload || typeof payload !== "object") { + return "unknown"; + } const record = payload as Record; const data = record.data && typeof record.data === "object" @@ -125,8 +135,12 @@ function extractMessageId(payload: unknown): string { data?.id, ]; for (const candidate of candidates) { - if (typeof candidate === "string" && candidate.trim()) return candidate.trim(); - if (typeof candidate === "number" && Number.isFinite(candidate)) return String(candidate); + if (typeof candidate === "string" && candidate.trim()) { + return candidate.trim(); + } + if (typeof candidate === "number" && Number.isFinite(candidate)) { + return String(candidate); + } } return "unknown"; } @@ -274,7 +288,9 @@ export async function sendBlueBubblesAttachment(params: { } const responseBody = await res.text(); - if (!responseBody) return { messageId: "ok" }; + if (!responseBody) { + return { messageId: "ok" }; + } try { const parsed = JSON.parse(responseBody) as unknown; return { messageId: extractMessageId(parsed) }; diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index c9133e10f..d48f4313a 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -78,13 +78,12 @@ export const bluebubblesPlugin: ChannelPlugin = { configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema), onboarding: blueBubblesOnboardingAdapter, config: { - listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg as OpenClawConfig), - resolveAccount: (cfg, accountId) => - resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }), - defaultAccountId: (cfg) => resolveDefaultBlueBubblesAccountId(cfg as OpenClawConfig), + listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg: cfg, accountId }), + defaultAccountId: (cfg) => resolveDefaultBlueBubblesAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "bluebubbles", accountId, enabled, @@ -92,7 +91,7 @@ export const bluebubblesPlugin: ChannelPlugin = { }), deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "bluebubbles", accountId, clearBaseFields: ["serverUrl", "password", "name", "webhookPath"], @@ -106,9 +105,9 @@ export const bluebubblesPlugin: ChannelPlugin = { baseUrl: account.baseUrl, }), resolveAllowFrom: ({ cfg, accountId }) => - ( - resolveBlueBubblesAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? [] - ).map((entry) => String(entry)), + (resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => + String(entry), + ), formatAllowFrom: ({ allowFrom }) => allowFrom .map((entry) => String(entry).trim()) @@ -120,9 +119,7 @@ export const bluebubblesPlugin: ChannelPlugin = { security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean( - (cfg as OpenClawConfig).channels?.bluebubbles?.accounts?.[resolvedAccountId], - ); + const useAccountPath = Boolean(cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]); const basePath = useAccountPath ? `channels.bluebubbles.accounts.${resolvedAccountId}.` : "channels.bluebubbles."; @@ -137,7 +134,9 @@ export const bluebubblesPlugin: ChannelPlugin = { }, collectWarnings: ({ account }) => { const groupPolicy = account.config.groupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ `- BlueBubbles groups: groupPolicy="open" allows any member to trigger the bot. Set channels.bluebubbles.groupPolicy="allowlist" + channels.bluebubbles.groupAllowFrom to restrict senders.`, ]; @@ -151,19 +150,25 @@ export const bluebubblesPlugin: ChannelPlugin = { }, formatTargetDisplay: ({ target, display }) => { const shouldParseDisplay = (value: string): boolean => { - if (looksLikeBlueBubblesTargetId(value)) return true; + if (looksLikeBlueBubblesTargetId(value)) { + return true; + } return /^(bluebubbles:|chat_guid:|chat_id:|chat_identifier:)/i.test(value); }; // Helper to extract a clean handle from any BlueBubbles target format const extractCleanDisplay = (value: string | undefined): string | null => { const trimmed = value?.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } try { const parsed = parseBlueBubblesTarget(trimmed); if (parsed.kind === "chat_guid") { const handle = extractHandleFromChatGuid(parsed.chatGuid); - if (handle) return handle; + if (handle) { + return handle; + } } if (parsed.kind === "handle") { return normalizeBlueBubblesHandle(parsed.to); @@ -178,9 +183,13 @@ export const bluebubblesPlugin: ChannelPlugin = { .replace(/^chat_id:/i, "") .replace(/^chat_identifier:/i, ""); const handle = extractHandleFromChatGuid(stripped); - if (handle) return handle; + if (handle) { + return handle; + } // Don't return raw chat_guid formats - they contain internal routing info - if (stripped.includes(";-;") || stripped.includes(";+;")) return null; + if (stripped.includes(";-;") || stripped.includes(";+;")) { + return null; + } return stripped; }; @@ -191,12 +200,16 @@ export const bluebubblesPlugin: ChannelPlugin = { return trimmedDisplay; } const cleanDisplay = extractCleanDisplay(trimmedDisplay); - if (cleanDisplay) return cleanDisplay; + if (cleanDisplay) { + return cleanDisplay; + } } // Fall back to extracting from target const cleanTarget = extractCleanDisplay(target); - if (cleanTarget) return cleanTarget; + if (cleanTarget) { + return cleanTarget; + } // Last resort: return display or target as-is return display?.trim() || target?.trim() || ""; @@ -206,7 +219,7 @@ export const bluebubblesPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "bluebubbles", accountId, name, @@ -215,13 +228,17 @@ export const bluebubblesPlugin: ChannelPlugin = { if (!input.httpUrl && !input.password) { return "BlueBubbles requires --http-url and --password."; } - if (!input.httpUrl) return "BlueBubbles requires --http-url."; - if (!input.password) return "BlueBubbles requires --password."; + if (!input.httpUrl) { + return "BlueBubbles requires --http-url."; + } + if (!input.password) { + return "BlueBubbles requires --password."; + } return null; }, applyAccountConfig: ({ cfg, accountId, input }) => { const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "bluebubbles", accountId, name: input.name, @@ -256,9 +273,9 @@ export const bluebubblesPlugin: ChannelPlugin = { ...next.channels?.bluebubbles, enabled: true, accounts: { - ...(next.channels?.bluebubbles?.accounts ?? {}), + ...next.channels?.bluebubbles?.accounts, [accountId]: { - ...(next.channels?.bluebubbles?.accounts?.[accountId] ?? {}), + ...next.channels?.bluebubbles?.accounts?.[accountId], enabled: true, ...(input.httpUrl ? { serverUrl: input.httpUrl } : {}), ...(input.password ? { password: input.password } : {}), @@ -275,7 +292,7 @@ export const bluebubblesPlugin: ChannelPlugin = { normalizeAllowEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")), notifyApproval: async ({ cfg, id }) => { await sendMessageBlueBubbles(id, PAIRING_APPROVED_MESSAGE, { - cfg: cfg as OpenClawConfig, + cfg: cfg, }); }, }, @@ -299,7 +316,7 @@ export const bluebubblesPlugin: ChannelPlugin = { ? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true }) : ""; const result = await sendMessageBlueBubbles(to, text, { - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId: accountId ?? undefined, replyToMessageGuid: replyToMessageGuid || undefined, }); @@ -316,7 +333,7 @@ export const bluebubblesPlugin: ChannelPlugin = { }; const resolvedCaption = caption ?? text; const result = await sendBlueBubblesMedia({ - cfg: cfg as OpenClawConfig, + cfg: cfg, to, mediaUrl, mediaPath, @@ -387,7 +404,7 @@ export const bluebubblesPlugin: ChannelPlugin = { ctx.log?.info(`[${account.accountId}] starting provider (webhook=${webhookPath})`); return monitorBlueBubblesProvider({ account, - config: ctx.cfg as OpenClawConfig, + config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }), diff --git a/extensions/bluebubbles/src/chat.ts b/extensions/bluebubbles/src/chat.ts index d39fba4bb..9ca3fa507 100644 --- a/extensions/bluebubbles/src/chat.ts +++ b/extensions/bluebubbles/src/chat.ts @@ -18,8 +18,12 @@ function resolveAccount(params: BlueBubblesChatOpts) { }); const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim(); const password = params.password?.trim() || account.config.password?.trim(); - if (!baseUrl) throw new Error("BlueBubbles serverUrl is required"); - if (!password) throw new Error("BlueBubbles password is required"); + if (!baseUrl) { + throw new Error("BlueBubbles serverUrl is required"); + } + if (!password) { + throw new Error("BlueBubbles password is required"); + } return { baseUrl, password }; } @@ -28,7 +32,9 @@ export async function markBlueBubblesChatRead( opts: BlueBubblesChatOpts = {}, ): Promise { const trimmed = chatGuid.trim(); - if (!trimmed) return; + if (!trimmed) { + return; + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ baseUrl, @@ -48,7 +54,9 @@ export async function sendBlueBubblesTyping( opts: BlueBubblesChatOpts = {}, ): Promise { const trimmed = chatGuid.trim(); - if (!trimmed) return; + if (!trimmed) { + return; + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ baseUrl, @@ -76,9 +84,13 @@ export async function editBlueBubblesMessage( opts: BlueBubblesChatOpts & { partIndex?: number; backwardsCompatMessage?: string } = {}, ): Promise { const trimmedGuid = messageGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles edit requires messageGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles edit requires messageGuid"); + } const trimmedText = newText.trim(); - if (!trimmedText) throw new Error("BlueBubbles edit requires newText"); + if (!trimmedText) { + throw new Error("BlueBubbles edit requires newText"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ @@ -118,7 +130,9 @@ export async function unsendBlueBubblesMessage( opts: BlueBubblesChatOpts & { partIndex?: number } = {}, ): Promise { const trimmedGuid = messageGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles unsend requires messageGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles unsend requires messageGuid"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ @@ -156,7 +170,9 @@ export async function renameBlueBubblesChat( opts: BlueBubblesChatOpts = {}, ): Promise { const trimmedGuid = chatGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles rename requires chatGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles rename requires chatGuid"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ @@ -190,9 +206,13 @@ export async function addBlueBubblesParticipant( opts: BlueBubblesChatOpts = {}, ): Promise { const trimmedGuid = chatGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles addParticipant requires chatGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles addParticipant requires chatGuid"); + } const trimmedAddress = address.trim(); - if (!trimmedAddress) throw new Error("BlueBubbles addParticipant requires address"); + if (!trimmedAddress) { + throw new Error("BlueBubbles addParticipant requires address"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ @@ -226,9 +246,13 @@ export async function removeBlueBubblesParticipant( opts: BlueBubblesChatOpts = {}, ): Promise { const trimmedGuid = chatGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles removeParticipant requires chatGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles removeParticipant requires chatGuid"); + } const trimmedAddress = address.trim(); - if (!trimmedAddress) throw new Error("BlueBubbles removeParticipant requires address"); + if (!trimmedAddress) { + throw new Error("BlueBubbles removeParticipant requires address"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ @@ -263,7 +287,9 @@ export async function leaveBlueBubblesChat( opts: BlueBubblesChatOpts = {}, ): Promise { const trimmedGuid = chatGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles leaveChat requires chatGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles leaveChat requires chatGuid"); + } const { baseUrl, password } = resolveAccount(opts); const url = buildBlueBubblesApiUrl({ @@ -291,7 +317,9 @@ export async function setGroupIconBlueBubbles( opts: BlueBubblesChatOpts & { contentType?: string } = {}, ): Promise { const trimmedGuid = chatGuid.trim(); - if (!trimmedGuid) throw new Error("BlueBubbles setGroupIcon requires chatGuid"); + if (!trimmedGuid) { + throw new Error("BlueBubbles setGroupIcon requires chatGuid"); + } if (!buffer || buffer.length === 0) { throw new Error("BlueBubbles setGroupIcon requires image buffer"); } diff --git a/extensions/bluebubbles/src/media-send.ts b/extensions/bluebubbles/src/media-send.ts index 075bcfa72..f57421d73 100644 --- a/extensions/bluebubbles/src/media-send.ts +++ b/extensions/bluebubbles/src/media-send.ts @@ -12,15 +12,21 @@ const HTTP_URL_RE = /^https?:\/\//i; const MB = 1024 * 1024; function assertMediaWithinLimit(sizeBytes: number, maxBytes?: number): void { - if (typeof maxBytes !== "number" || maxBytes <= 0) return; - if (sizeBytes <= maxBytes) return; + if (typeof maxBytes !== "number" || maxBytes <= 0) { + return; + } + if (sizeBytes <= maxBytes) { + return; + } const maxLabel = (maxBytes / MB).toFixed(0); const sizeLabel = (sizeBytes / MB).toFixed(2); throw new Error(`Media exceeds ${maxLabel}MB limit (got ${sizeLabel}MB)`); } function resolveLocalMediaPath(source: string): string { - if (!source.startsWith("file://")) return source; + if (!source.startsWith("file://")) { + return source; + } try { return fileURLToPath(source); } catch { @@ -29,7 +35,9 @@ function resolveLocalMediaPath(source: string): string { } function resolveFilenameFromSource(source?: string): string | undefined { - if (!source) return undefined; + if (!source) { + return undefined; + } if (source.startsWith("file://")) { try { return path.basename(fileURLToPath(source)) || undefined; diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index b793ab591..cfb39cdb8 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -262,6 +262,7 @@ function createMockRequest( (req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "127.0.0.1" }; // Emit body data after a microtask + // oxlint-disable-next-line no-floating-promises Promise.resolve().then(() => { const bodyStr = typeof body === "string" ? body : JSON.stringify(body); req.emit("data", Buffer.from(bodyStr)); @@ -1225,7 +1226,9 @@ describe("BlueBubbles webhook monitor", () => { const flush = async (key: string) => { const bucket = buckets.get(key); - if (!bucket) return; + if (!bucket) { + return; + } if (bucket.timer) { clearTimeout(bucket.timer); bucket.timer = null; @@ -1253,7 +1256,9 @@ describe("BlueBubbles webhook monitor", () => { const existing = buckets.get(key); const bucket = existing ?? { items: [], timer: null }; bucket.items.push(item); - if (bucket.timer) clearTimeout(bucket.timer); + if (bucket.timer) { + clearTimeout(bucket.timer); + } bucket.timer = setTimeout(async () => { await flush(key); }, params.debounceMs); diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 302c067ff..5f96d9d48 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -112,7 +112,9 @@ function rememberBlueBubblesReplyCache( } while (blueBubblesReplyCacheByMessageId.size > REPLY_CACHE_MAX) { const oldest = blueBubblesReplyCacheByMessageId.keys().next().value as string | undefined; - if (!oldest) break; + if (!oldest) { + break; + } const oldEntry = blueBubblesReplyCacheByMessageId.get(oldest); blueBubblesReplyCacheByMessageId.delete(oldest); // Clean up short ID mappings for evicted entries @@ -134,12 +136,16 @@ export function resolveBlueBubblesMessageId( opts?: { requireKnownShortId?: boolean }, ): string { const trimmed = shortOrUuid.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } // If it looks like a short ID (numeric), try to resolve it if (/^\d+$/.test(trimmed)) { const uuid = blueBubblesShortIdToUuid.get(trimmed); - if (uuid) return uuid; + if (uuid) { + return uuid; + } if (opts?.requireKnownShortId) { throw new Error( `BlueBubbles short message id "${trimmed}" is no longer available. Use MessageSidFull.`, @@ -177,11 +183,17 @@ function resolveReplyContextFromCache(params: { chatId?: number; }): BlueBubblesReplyCacheEntry | null { const replyToId = params.replyToId.trim(); - if (!replyToId) return null; + if (!replyToId) { + return null; + } const cached = blueBubblesReplyCacheByMessageId.get(replyToId); - if (!cached) return null; - if (cached.accountId !== params.accountId) return null; + if (!cached) { + return null; + } + if (cached.accountId !== params.accountId) { + return null; + } const cutoff = Date.now() - REPLY_CACHE_TTL_MS; if (cached.timestamp < cutoff) { @@ -197,7 +209,9 @@ function resolveReplyContextFromCache(params: { const cachedChatId = typeof cached.chatId === "number" ? cached.chatId : undefined; // Avoid cross-chat collisions if we have identifiers. - if (chatGuid && cachedChatGuid && chatGuid !== cachedChatGuid) return null; + if (chatGuid && cachedChatGuid && chatGuid !== cachedChatGuid) { + return null; + } if ( !chatGuid && chatIdentifier && @@ -300,10 +314,14 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized for (const entry of entries) { const text = entry.message.text.trim(); - if (!text) continue; + if (!text) { + continue; + } // Skip duplicate text (URL might be in both text message and balloon) const normalizedText = text.toLowerCase(); - if (seenTexts.has(normalizedText)) continue; + if (seenTexts.has(normalizedText)) { + continue; + } seenTexts.add(normalizedText); textParts.push(text); } @@ -359,7 +377,9 @@ function resolveBlueBubblesDebounceMs( const inbound = config.messages?.inbound; const hasExplicitDebounce = typeof inbound?.debounceMs === "number" || typeof inbound?.byChannel?.bluebubbles === "number"; - if (!hasExplicitDebounce) return DEFAULT_INBOUND_DEBOUNCE_MS; + if (!hasExplicitDebounce) { + return DEFAULT_INBOUND_DEBOUNCE_MS; + } return core.channel.debounce.resolveInboundDebounceMs({ cfg: config, channel: "bluebubbles" }); } @@ -368,7 +388,9 @@ function resolveBlueBubblesDebounceMs( */ function getOrCreateDebouncer(target: WebhookTarget) { const existing = targetDebouncers.get(target); - if (existing) return existing; + if (existing) { + return existing; + } const { account, config, runtime, core } = target; @@ -402,15 +424,21 @@ function getOrCreateDebouncer(target: WebhookTarget) { shouldDebounce: (entry) => { const msg = entry.message; // Skip debouncing for from-me messages (they're just cached, not processed) - if (msg.fromMe) return false; + if (msg.fromMe) { + return false; + } // Skip debouncing for control commands - process immediately - if (core.channel.text.hasControlCommand(msg.text, config)) return false; + if (core.channel.text.hasControlCommand(msg.text, config)) { + return false; + } // Debounce all other messages to coalesce rapid-fire webhook events // (e.g., text+image arriving as separate webhooks for the same messageId) return true; }, onFlush: async (entries) => { - if (entries.length === 0) return; + if (entries.length === 0) { + return; + } // Use target from first entry (all entries have same target due to key structure) const flushTarget = entries[0].target; @@ -452,7 +480,9 @@ function removeDebouncer(target: WebhookTarget): void { function normalizeWebhookPath(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return "/"; + if (!trimmed) { + return "/"; + } const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; if (withSlash.length > 1 && withSlash.endsWith("/")) { return withSlash.slice(0, -1); @@ -527,30 +557,40 @@ function asRecord(value: unknown): Record | null { } function readString(record: Record | null, key: string): string | undefined { - if (!record) return undefined; + if (!record) { + return undefined; + } const value = record[key]; return typeof value === "string" ? value : undefined; } function readNumber(record: Record | null, key: string): number | undefined { - if (!record) return undefined; + if (!record) { + return undefined; + } const value = record[key]; return typeof value === "number" && Number.isFinite(value) ? value : undefined; } function readBoolean(record: Record | null, key: string): boolean | undefined { - if (!record) return undefined; + if (!record) { + return undefined; + } const value = record[key]; return typeof value === "boolean" ? value : undefined; } function extractAttachments(message: Record): BlueBubblesAttachment[] { const raw = message["attachments"]; - if (!Array.isArray(raw)) return []; + if (!Array.isArray(raw)) { + return []; + } const out: BlueBubblesAttachment[] = []; for (const entry of raw) { const record = asRecord(entry); - if (!record) continue; + if (!record) { + continue; + } out.push({ guid: readString(record, "guid"), uti: readString(record, "uti"), @@ -566,7 +606,9 @@ function extractAttachments(message: Record): BlueBubblesAttach } function buildAttachmentPlaceholder(attachments: BlueBubblesAttachment[]): string { - if (attachments.length === 0) return ""; + if (attachments.length === 0) { + return ""; + } const mimeTypes = attachments.map((entry) => entry.mimeType ?? ""); const allImages = mimeTypes.every((entry) => entry.startsWith("image/")); const allVideos = mimeTypes.every((entry) => entry.startsWith("video/")); @@ -585,8 +627,12 @@ function buildAttachmentPlaceholder(attachments: BlueBubblesAttachment[]): strin function buildMessagePlaceholder(message: NormalizedWebhookMessage): string { const attachmentPlaceholder = buildAttachmentPlaceholder(message.attachments ?? []); - if (attachmentPlaceholder) return attachmentPlaceholder; - if (message.balloonBundleId) return ""; + if (attachmentPlaceholder) { + return attachmentPlaceholder; + } + if (message.balloonBundleId) { + return ""; + } return ""; } @@ -594,17 +640,25 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string { function formatReplyTag(message: { replyToId?: string; replyToShortId?: string }): string | null { // Prefer short ID const rawId = message.replyToShortId || message.replyToId; - if (!rawId) return null; + if (!rawId) { + return null; + } return `[[reply_to:${rawId}]]`; } function readNumberLike(record: Record | null, key: string): number | undefined { - if (!record) return undefined; + if (!record) { + return undefined; + } const value = record[key]; - if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } if (typeof value === "string") { const parsed = Number.parseFloat(value); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } return undefined; } @@ -683,7 +737,9 @@ function extractReplyMetadata(message: Record): { function readFirstChatRecord(message: Record): Record | null { const chats = message["chats"]; - if (!Array.isArray(chats) || chats.length === 0) return null; + if (!Array.isArray(chats) || chats.length === 0) { + return null; + } const first = chats[0]; return asRecord(first); } @@ -691,12 +747,16 @@ function readFirstChatRecord(message: Record): Record(); const output: BlueBubblesParticipant[] = []; for (const entry of raw) { const normalized = normalizeParticipantEntry(entry); - if (!normalized?.id) continue; + if (!normalized?.id) { + continue; + } const key = normalized.id.toLowerCase(); - if (seen.has(key)) continue; + if (seen.has(key)) { + continue; + } seen.add(key); output.push(normalized); } @@ -743,37 +811,57 @@ function formatGroupMembers(params: { const seen = new Set(); const ordered: BlueBubblesParticipant[] = []; for (const entry of params.participants ?? []) { - if (!entry?.id) continue; + if (!entry?.id) { + continue; + } const key = entry.id.toLowerCase(); - if (seen.has(key)) continue; + if (seen.has(key)) { + continue; + } seen.add(key); ordered.push(entry); } if (ordered.length === 0 && params.fallback?.id) { ordered.push(params.fallback); } - if (ordered.length === 0) return undefined; + if (ordered.length === 0) { + return undefined; + } return ordered.map((entry) => (entry.name ? `${entry.name} (${entry.id})` : entry.id)).join(", "); } function resolveGroupFlagFromChatGuid(chatGuid?: string | null): boolean | undefined { const guid = chatGuid?.trim(); - if (!guid) return undefined; + if (!guid) { + return undefined; + } const parts = guid.split(";"); if (parts.length >= 3) { - if (parts[1] === "+") return true; - if (parts[1] === "-") return false; + if (parts[1] === "+") { + return true; + } + if (parts[1] === "-") { + return false; + } + } + if (guid.includes(";+;")) { + return true; + } + if (guid.includes(";-;")) { + return false; } - if (guid.includes(";+;")) return true; - if (guid.includes(";-;")) return false; return undefined; } function extractChatIdentifierFromChatGuid(chatGuid?: string | null): string | undefined { const guid = chatGuid?.trim(); - if (!guid) return undefined; + if (!guid) { + return undefined; + } const parts = guid.split(";"); - if (parts.length < 3) return undefined; + if (parts.length < 3) { + return undefined; + } const identifier = parts[2]?.trim(); return identifier || undefined; } @@ -784,11 +872,17 @@ function formatGroupAllowlistEntry(params: { chatIdentifier?: string; }): string | null { const guid = params.chatGuid?.trim(); - if (guid) return `chat_guid:${guid}`; + if (guid) { + return `chat_guid:${guid}`; + } const chatId = params.chatId; - if (typeof chatId === "number" && Number.isFinite(chatId)) return `chat_id:${chatId}`; + if (typeof chatId === "number" && Number.isFinite(chatId)) { + return `chat_id:${chatId}`; + } const identifier = params.chatIdentifier?.trim(); - if (identifier) return `chat_identifier:${identifier}`; + if (identifier) { + return `chat_identifier:${identifier}`; + } return null; } @@ -886,9 +980,15 @@ function isTapbackAssociatedType(type: number | undefined): boolean { } function resolveTapbackActionHint(type: number | undefined): "added" | "removed" | undefined { - if (typeof type !== "number" || !Number.isFinite(type)) return undefined; - if (type >= 3000 && type < 4000) return "removed"; - if (type >= 2000 && type < 3000) return "added"; + if (typeof type !== "number" || !Number.isFinite(type)) { + return undefined; + } + if (type >= 3000 && type < 4000) { + return "removed"; + } + if (type >= 2000 && type < 3000) { + return "added"; + } return undefined; } @@ -900,7 +1000,9 @@ function resolveTapbackContext(message: NormalizedWebhookMessage): { const associatedType = message.associatedMessageType; const hasTapbackType = isTapbackAssociatedType(associatedType); const hasTapbackMarker = Boolean(message.associatedMessageEmoji) || Boolean(message.isTapback); - if (!hasTapbackType && !hasTapbackMarker) return null; + if (!hasTapbackType && !hasTapbackMarker) { + return null; + } const replyToId = message.associatedMessageGuid?.trim() || message.replyToId?.trim() || undefined; const actionHint = resolveTapbackActionHint(associatedType); const emojiHint = @@ -921,7 +1023,9 @@ function parseTapbackText(params: { } | null { const trimmed = params.text.trim(); const lower = trimmed.toLowerCase(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } for (const [pattern, { emoji, action }] of TAPBACK_TEXT_MAP) { if (lower.startsWith(pattern)) { @@ -929,7 +1033,9 @@ function parseTapbackText(params: { const afterPattern = trimmed.slice(pattern.length).trim(); if (params.requireQuoted) { const strictMatch = afterPattern.match(/^[“"](.+)[”"]$/s); - if (!strictMatch) return null; + if (!strictMatch) { + return null; + } return { emoji, action, quotedText: strictMatch[1] }; } const quotedText = @@ -940,18 +1046,26 @@ function parseTapbackText(params: { if (lower.startsWith("reacted")) { const emoji = extractFirstEmoji(trimmed) ?? params.emojiHint; - if (!emoji) return null; + if (!emoji) { + return null; + } const quotedText = extractQuotedTapbackText(trimmed); - if (params.requireQuoted && !quotedText) return null; + if (params.requireQuoted && !quotedText) { + return null; + } const fallback = trimmed.slice("reacted".length).trim(); return { emoji, action: params.actionHint ?? "added", quotedText: quotedText ?? fallback }; } if (lower.startsWith("removed")) { const emoji = extractFirstEmoji(trimmed) ?? params.emojiHint; - if (!emoji) return null; + if (!emoji) { + return null; + } const quotedText = extractQuotedTapbackText(trimmed); - if (params.requireQuoted && !quotedText) return null; + if (params.requireQuoted && !quotedText) { + return null; + } const fallback = trimmed.slice("removed".length).trim(); return { emoji, action: params.actionHint ?? "removed", quotedText: quotedText ?? fallback }; } @@ -959,7 +1073,9 @@ function parseTapbackText(params: { } function maskSecret(value: string): string { - if (value.length <= 6) return "***"; + if (value.length <= 6) { + return "***"; + } return `${value.slice(0, 2)}***${value.slice(-2)}`; } @@ -970,7 +1086,9 @@ function resolveBlueBubblesAckReaction(params: { runtime: BlueBubblesRuntimeEnv; }): string | null { const raw = resolveAckReaction(params.cfg, params.agentId).trim(); - if (!raw) return null; + if (!raw) { + return null; + } try { normalizeBlueBubblesReactionInput(raw); return raw; @@ -997,7 +1115,9 @@ function extractMessagePayload(payload: Record): Record, ): NormalizedWebhookMessage | null { const message = extractMessagePayload(payload); - if (!message) return null; + if (!message) { + return null; + } const text = readString(message, "text") ?? @@ -1090,7 +1212,7 @@ function normalizeWebhookMessage( const isGroup = typeof groupFromChatGuid === "boolean" ? groupFromChatGuid - : (explicitIsGroup ?? (participantsCount > 2 ? true : false)); + : (explicitIsGroup ?? participantsCount > 2); const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me"); const messageId = @@ -1131,7 +1253,9 @@ function normalizeWebhookMessage( : undefined; const normalizedSender = normalizeBlueBubblesHandle(senderId); - if (!normalizedSender) return null; + if (!normalizedSender) { + return null; + } const replyMetadata = extractReplyMetadata(message); return { @@ -1163,7 +1287,9 @@ function normalizeWebhookReaction( payload: Record, ): NormalizedWebhookReaction | null { const message = extractMessagePayload(payload); - if (!message) return null; + if (!message) { + return null; + } const associatedGuid = readString(message, "associatedMessageGuid") ?? @@ -1172,7 +1298,9 @@ function normalizeWebhookReaction( const associatedType = readNumberLike(message, "associatedMessageType") ?? readNumberLike(message, "associated_message_type"); - if (!associatedGuid || associatedType === undefined) return null; + if (!associatedGuid || associatedType === undefined) { + return null; + } const mapping = REACTION_TYPE_MAP.get(associatedType); const associatedEmoji = @@ -1258,7 +1386,7 @@ function normalizeWebhookReaction( const isGroup = typeof groupFromChatGuid === "boolean" ? groupFromChatGuid - : (explicitIsGroup ?? (participantsCount > 2 ? true : false)); + : (explicitIsGroup ?? participantsCount > 2); const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me"); const timestampRaw = @@ -1273,7 +1401,9 @@ function normalizeWebhookReaction( : undefined; const normalizedSender = normalizeBlueBubblesHandle(senderId); - if (!normalizedSender) return null; + if (!normalizedSender) { + return null; + } return { action, @@ -1298,7 +1428,9 @@ export async function handleBlueBubblesWebhookRequest( const url = new URL(req.url ?? "/", "http://localhost"); const path = normalizeWebhookPath(url.pathname); const targets = webhookTargets.get(path); - if (!targets || targets.length === 0) return false; + if (!targets || targets.length === 0) { + return false; + } if (req.method !== "POST") { res.statusCode = 405; @@ -1368,7 +1500,9 @@ export async function handleBlueBubblesWebhookRequest( const matching = targets.filter((target) => { const token = target.account.config.password?.trim(); - if (!token) return true; + if (!token) { + return true; + } const guidParam = url.searchParams.get("guid") ?? url.searchParams.get("password"); const headerToken = req.headers["x-guid"] ?? @@ -1376,7 +1510,9 @@ export async function handleBlueBubblesWebhookRequest( req.headers["x-bluebubbles-guid"] ?? req.headers["authorization"]; const guid = (Array.isArray(headerToken) ? headerToken[0] : headerToken) ?? guidParam ?? ""; - if (guid && guid.trim() === token) return true; + if (guid && guid.trim() === token) { + return true; + } const remote = req.socket?.remoteAddress ?? ""; if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") { return true; @@ -1466,7 +1602,9 @@ async function processMessage( const cacheMessageId = message.messageId?.trim(); let messageShortId: string | undefined; const cacheInboundMessage = () => { - if (!cacheMessageId) return; + if (!cacheMessageId) { + return; + } const cacheEntry = rememberBlueBubblesReplyCache({ accountId: account.accountId, messageId: cacheMessageId, @@ -1743,7 +1881,9 @@ async function processMessage( logVerbose(core, runtime, "attachment download skipped (missing serverUrl/password)"); } else { for (const attachment of attachments) { - if (!attachment.guid) continue; + if (!attachment.guid) { + continue; + } if (attachment.totalBytes && attachment.totalBytes > maxBytes) { logVerbose( core, @@ -1797,8 +1937,12 @@ async function processMessage( chatId: message.chatId, }); if (cached) { - if (!replyToBody && cached.body) replyToBody = cached.body; - if (!replyToSender && cached.senderLabel) replyToSender = cached.senderLabel; + if (!replyToBody && cached.body) { + replyToBody = cached.body; + } + if (!replyToSender && cached.senderLabel) { + replyToSender = cached.senderLabel; + } replyToShortId = cached.shortId; if (core.logging.shouldLogVerbose()) { const preview = (cached.body ?? "").replace(/\s+/g, " ").slice(0, 120); @@ -1940,7 +2084,9 @@ async function processMessage( const maybeEnqueueOutboundMessageId = (messageId?: string, snippet?: string) => { const trimmed = messageId?.trim(); - if (!trimmed || trimmed === "ok" || trimmed === "unknown") return; + if (!trimmed || trimmed === "ok" || trimmed === "unknown") { + return; + } // Cache outbound message to get short ID const cacheEntry = rememberBlueBubblesReplyCache({ accountId: account.accountId, @@ -2059,8 +2205,12 @@ async function processMessage( chunkMode === "newline" ? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode) : core.channel.text.chunkMarkdownText(text, textLimit); - if (!chunks.length && text) chunks.push(text); - if (!chunks.length) return; + if (!chunks.length && text) { + chunks.push(text); + } + if (!chunks.length) { + return; + } for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; const result = await sendMessageBlueBubbles(outboundTarget, chunk, { @@ -2085,8 +2235,12 @@ async function processMessage( } }, onReplyStart: async () => { - if (!chatGuidForActions) return; - if (!baseUrl || !password) return; + if (!chatGuidForActions) { + return; + } + if (!baseUrl || !password) { + return; + } logVerbose(core, runtime, `typing start chatGuid=${chatGuidForActions}`); try { await sendBlueBubblesTyping(chatGuidForActions, true, { @@ -2098,8 +2252,12 @@ async function processMessage( } }, onIdle: async () => { - if (!chatGuidForActions) return; - if (!baseUrl || !password) return; + if (!chatGuidForActions) { + return; + } + if (!baseUrl || !password) { + return; + } try { await sendBlueBubblesTyping(chatGuidForActions, false, { cfg: config, @@ -2167,7 +2325,9 @@ async function processReaction( target: WebhookTarget, ): Promise { const { account, config, runtime, core } = target; - if (reaction.fromMe) return; + if (reaction.fromMe) { + return; + } const dmPolicy = account.config.dmPolicy ?? "pairing"; const groupPolicy = account.config.groupPolicy ?? "allowlist"; @@ -2187,9 +2347,13 @@ async function processReaction( .filter(Boolean); if (reaction.isGroup) { - if (groupPolicy === "disabled") return; + if (groupPolicy === "disabled") { + return; + } if (groupPolicy === "allowlist") { - if (effectiveGroupAllowFrom.length === 0) return; + if (effectiveGroupAllowFrom.length === 0) { + return; + } const allowed = isAllowedBlueBubblesSender({ allowFrom: effectiveGroupAllowFrom, sender: reaction.senderId, @@ -2197,10 +2361,14 @@ async function processReaction( chatGuid: reaction.chatGuid ?? undefined, chatIdentifier: reaction.chatIdentifier ?? undefined, }); - if (!allowed) return; + if (!allowed) { + return; + } } } else { - if (dmPolicy === "disabled") return; + if (dmPolicy === "disabled") { + return; + } if (dmPolicy !== "open") { const allowed = isAllowedBlueBubblesSender({ allowFrom: effectiveAllowFrom, @@ -2209,7 +2377,9 @@ async function processReaction( chatGuid: reaction.chatGuid ?? undefined, chatIdentifier: reaction.chatIdentifier ?? undefined, }); - if (!allowed) return; + if (!allowed) { + return; + } } } @@ -2293,6 +2463,8 @@ export async function monitorBlueBubblesProvider( export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string { const raw = config?.webhookPath?.trim(); - if (raw) return normalizeWebhookPath(raw); + if (raw) { + return normalizeWebhookPath(raw); + } return DEFAULT_WEBHOOK_PATH; } diff --git a/extensions/bluebubbles/src/onboarding.ts b/extensions/bluebubbles/src/onboarding.ts index 90de47887..c0d4e9222 100644 --- a/extensions/bluebubbles/src/onboarding.ts +++ b/extensions/bluebubbles/src/onboarding.ts @@ -18,7 +18,7 @@ import { resolveDefaultBlueBubblesAccountId, } from "./accounts.js"; import { normalizeBlueBubblesServerUrl } from "./types.js"; -import { parseBlueBubblesAllowTarget, normalizeBlueBubblesHandle } from "./targets.js"; +import { parseBlueBubblesAllowTarget } from "./targets.js"; const channel = "bluebubbles" as const; @@ -110,10 +110,14 @@ async function promptBlueBubblesAllowFrom(params: { initialValue: existing[0] ? String(existing[0]) : undefined, validate: (value) => { const raw = String(value ?? "").trim(); - if (!raw) return "Required"; + if (!raw) { + return "Required"; + } const parts = parseBlueBubblesAllowFromInput(raw); for (const part of parts) { - if (part === "*") continue; + if (part === "*") { + continue; + } const parsed = parseBlueBubblesAllowTarget(part); if (parsed.kind === "handle" && !parsed.handle) { return `Invalid entry: ${part}`; @@ -188,7 +192,9 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = { placeholder: "http://192.168.1.100:1234", validate: (value) => { const trimmed = String(value ?? "").trim(); - if (!trimmed) return "Required"; + if (!trimmed) { + return "Required"; + } try { const normalized = normalizeBlueBubblesServerUrl(trimmed); new URL(normalized); @@ -211,7 +217,9 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = { initialValue: serverUrl, validate: (value) => { const trimmed = String(value ?? "").trim(); - if (!trimmed) return "Required"; + if (!trimmed) { + return "Required"; + } try { const normalized = normalizeBlueBubblesServerUrl(trimmed); new URL(normalized); @@ -268,8 +276,12 @@ export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = { initialValue: existingWebhookPath || "/bluebubbles-webhook", validate: (value) => { const trimmed = String(value ?? "").trim(); - if (!trimmed) return "Required"; - if (!trimmed.startsWith("/")) return "Path must start with /"; + if (!trimmed) { + return "Required"; + } + if (!trimmed.startsWith("/")) { + return "Path must start with /"; + } return undefined; }, }); diff --git a/extensions/bluebubbles/src/probe.ts b/extensions/bluebubbles/src/probe.ts index c38f6ee7b..76e3b330e 100644 --- a/extensions/bluebubbles/src/probe.ts +++ b/extensions/bluebubbles/src/probe.ts @@ -36,7 +36,9 @@ export async function fetchBlueBubblesServerInfo(params: { }): Promise { const baseUrl = params.baseUrl?.trim(); const password = params.password?.trim(); - if (!baseUrl || !password) return null; + if (!baseUrl || !password) { + return null; + } const cacheKey = buildCacheKey(params.accountId); const cached = serverInfoCache.get(cacheKey); @@ -47,7 +49,9 @@ export async function fetchBlueBubblesServerInfo(params: { const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/server/info", password }); try { const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs ?? 5000); - if (!res.ok) return null; + if (!res.ok) { + return null; + } const payload = (await res.json().catch(() => null)) as Record | null; const data = payload?.data as BlueBubblesServerInfo | undefined; if (data) { @@ -76,7 +80,9 @@ export function getCachedBlueBubblesServerInfo(accountId?: string): BlueBubblesS * Parse macOS version string (e.g., "15.0.1" or "26.0") into major version number. */ export function parseMacOSMajorVersion(version?: string | null): number | null { - if (!version) return null; + if (!version) { + return null; + } const match = /^(\d+)/.exec(version.trim()); return match ? Number.parseInt(match[1], 10) : null; } @@ -87,7 +93,9 @@ export function parseMacOSMajorVersion(version?: string | null): number | null { */ export function isMacOS26OrHigher(accountId?: string): boolean { const info = getCachedBlueBubblesServerInfo(accountId); - if (!info?.os_version) return false; + if (!info?.os_version) { + return false; + } const major = parseMacOSMajorVersion(info.os_version); return major !== null && major >= 26; } @@ -104,8 +112,12 @@ export async function probeBlueBubbles(params: { }): Promise { const baseUrl = params.baseUrl?.trim(); const password = params.password?.trim(); - if (!baseUrl) return { ok: false, error: "serverUrl not configured" }; - if (!password) return { ok: false, error: "password not configured" }; + if (!baseUrl) { + return { ok: false, error: "serverUrl not configured" }; + } + if (!password) { + return { ok: false, error: "password not configured" }; + } const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/ping", password }); try { const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs); diff --git a/extensions/bluebubbles/src/reactions.ts b/extensions/bluebubbles/src/reactions.ts index 9dc1f73bd..64a55ac0d 100644 --- a/extensions/bluebubbles/src/reactions.ts +++ b/extensions/bluebubbles/src/reactions.ts @@ -117,16 +117,24 @@ function resolveAccount(params: BlueBubblesReactionOpts) { }); const baseUrl = params.serverUrl?.trim() || account.config.serverUrl?.trim(); const password = params.password?.trim() || account.config.password?.trim(); - if (!baseUrl) throw new Error("BlueBubbles serverUrl is required"); - if (!password) throw new Error("BlueBubbles password is required"); + if (!baseUrl) { + throw new Error("BlueBubbles serverUrl is required"); + } + if (!password) { + throw new Error("BlueBubbles password is required"); + } return { baseUrl, password }; } export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolean): string { const trimmed = emoji.trim(); - if (!trimmed) throw new Error("BlueBubbles reaction requires an emoji or name."); + if (!trimmed) { + throw new Error("BlueBubbles reaction requires an emoji or name."); + } let raw = trimmed.toLowerCase(); - if (raw.startsWith("-")) raw = raw.slice(1); + if (raw.startsWith("-")) { + raw = raw.slice(1); + } const aliased = REACTION_ALIASES.get(raw) ?? raw; const mapped = REACTION_EMOJIS.get(trimmed) ?? REACTION_EMOJIS.get(raw) ?? aliased; if (!REACTION_TYPES.has(mapped)) { @@ -145,8 +153,12 @@ export async function sendBlueBubblesReaction(params: { }): Promise { const chatGuid = params.chatGuid.trim(); const messageGuid = params.messageGuid.trim(); - if (!chatGuid) throw new Error("BlueBubbles reaction requires chatGuid."); - if (!messageGuid) throw new Error("BlueBubbles reaction requires messageGuid."); + if (!chatGuid) { + throw new Error("BlueBubbles reaction requires chatGuid."); + } + if (!messageGuid) { + throw new Error("BlueBubbles reaction requires messageGuid."); + } const reaction = normalizeBlueBubblesReactionInput(params.emoji, params.remove); const { baseUrl, password } = resolveAccount(params.opts ?? {}); const url = buildBlueBubblesApiUrl({ diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index f85cf416d..ee3aa9d4e 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -55,13 +55,21 @@ const EFFECT_MAP: Record = { }; function resolveEffectId(raw?: string): string | undefined { - if (!raw) return undefined; + if (!raw) { + return undefined; + } const trimmed = raw.trim().toLowerCase(); - if (EFFECT_MAP[trimmed]) return EFFECT_MAP[trimmed]; + if (EFFECT_MAP[trimmed]) { + return EFFECT_MAP[trimmed]; + } const normalized = trimmed.replace(/[\s_]+/g, "-"); - if (EFFECT_MAP[normalized]) return EFFECT_MAP[normalized]; + if (EFFECT_MAP[normalized]) { + return EFFECT_MAP[normalized]; + } const compact = trimmed.replace(/[\s_-]+/g, ""); - if (EFFECT_MAP[compact]) return EFFECT_MAP[compact]; + if (EFFECT_MAP[compact]) { + return EFFECT_MAP[compact]; + } return raw; } @@ -84,7 +92,9 @@ function resolveSendTarget(raw: string): BlueBubblesSendTarget { } function extractMessageId(payload: unknown): string { - if (!payload || typeof payload !== "object") return "unknown"; + if (!payload || typeof payload !== "object") { + return "unknown"; + } const record = payload as Record; const data = record.data && typeof record.data === "object" @@ -104,8 +114,12 @@ function extractMessageId(payload: unknown): string { data?.id, ]; for (const candidate of candidates) { - if (typeof candidate === "string" && candidate.trim()) return candidate.trim(); - if (typeof candidate === "number" && Number.isFinite(candidate)) return String(candidate); + if (typeof candidate === "string" && candidate.trim()) { + return candidate.trim(); + } + if (typeof candidate === "number" && Number.isFinite(candidate)) { + return String(candidate); + } } return "unknown"; } @@ -122,7 +136,9 @@ function extractChatGuid(chat: BlueBubblesChatRecord): string | null { chat.chat_identifier, ]; for (const candidate of candidates) { - if (typeof candidate === "string" && candidate.trim()) return candidate.trim(); + if (typeof candidate === "string" && candidate.trim()) { + return candidate.trim(); + } } return null; } @@ -130,14 +146,18 @@ function extractChatGuid(chat: BlueBubblesChatRecord): string | null { function extractChatId(chat: BlueBubblesChatRecord): number | null { const candidates = [chat.chatId, chat.id, chat.chat_id]; for (const candidate of candidates) { - if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate; + if (typeof candidate === "number" && Number.isFinite(candidate)) { + return candidate; + } } return null; } function extractChatIdentifierFromChatGuid(chatGuid: string): string | null { const parts = chatGuid.split(";"); - if (parts.length < 3) return null; + if (parts.length < 3) { + return null; + } const identifier = parts[2]?.trim(); return identifier ? identifier : null; } @@ -147,7 +167,9 @@ function extractParticipantAddresses(chat: BlueBubblesChatRecord): string[] { (Array.isArray(chat.participants) ? chat.participants : null) ?? (Array.isArray(chat.handles) ? chat.handles : null) ?? (Array.isArray(chat.participantHandles) ? chat.participantHandles : null); - if (!raw) return []; + if (!raw) { + return []; + } const out: string[] = []; for (const entry of raw) { if (typeof entry === "string") { @@ -161,7 +183,9 @@ function extractParticipantAddresses(chat: BlueBubblesChatRecord): string[] { (typeof record.handle === "string" && record.handle) || (typeof record.id === "string" && record.id) || (typeof record.identifier === "string" && record.identifier); - if (candidate) out.push(candidate); + if (candidate) { + out.push(candidate); + } } } return out; @@ -192,7 +216,9 @@ async function queryChats(params: { }, params.timeoutMs, ); - if (!res.ok) return []; + if (!res.ok) { + return []; + } const payload = (await res.json().catch(() => null)) as Record | null; const data = payload && typeof payload.data !== "undefined" ? (payload.data as unknown) : null; return Array.isArray(data) ? (data as BlueBubblesChatRecord[]) : []; @@ -204,7 +230,9 @@ export async function resolveChatGuidForTarget(params: { timeoutMs?: number; target: BlueBubblesSendTarget; }): Promise { - if (params.target.kind === "chat_guid") return params.target.chatGuid; + if (params.target.kind === "chat_guid") { + return params.target.chatGuid; + } const normalizedHandle = params.target.kind === "handle" ? normalizeBlueBubblesHandle(params.target.address) : ""; @@ -222,7 +250,9 @@ export async function resolveChatGuidForTarget(params: { offset, limit, }); - if (chats.length === 0) break; + if (chats.length === 0) { + break; + } for (const chat of chats) { if (targetChatId != null) { const chatId = extractChatId(chat); @@ -234,12 +264,16 @@ export async function resolveChatGuidForTarget(params: { const guid = extractChatGuid(chat); if (guid) { // Back-compat: some callers might pass a full chat GUID. - if (guid === targetChatIdentifier) return guid; + if (guid === targetChatIdentifier) { + return guid; + } // Primary match: BlueBubbles `chat_identifier:*` targets correspond to the // third component of the chat GUID: `service;(+|-) ;identifier`. const guidIdentifier = extractChatIdentifierFromChatGuid(guid); - if (guidIdentifier && guidIdentifier === targetChatIdentifier) return guid; + if (guidIdentifier && guidIdentifier === targetChatIdentifier) { + return guid; + } } const identifier = @@ -250,7 +284,9 @@ export async function resolveChatGuidForTarget(params: { : typeof chat.chat_identifier === "string" ? chat.chat_identifier : ""; - if (identifier && identifier === targetChatIdentifier) return guid ?? extractChatGuid(chat); + if (identifier && identifier === targetChatIdentifier) { + return guid ?? extractChatGuid(chat); + } } if (normalizedHandle) { const guid = extractChatGuid(chat); @@ -322,7 +358,9 @@ async function createNewChatWithMessage(params: { throw new Error(`BlueBubbles create chat failed (${res.status}): ${errorText || "unknown"}`); } const body = await res.text(); - if (!body) return { messageId: "ok" }; + if (!body) { + return { messageId: "ok" }; + } try { const parsed = JSON.parse(body) as unknown; return { messageId: extractMessageId(parsed) }; @@ -347,8 +385,12 @@ export async function sendMessageBlueBubbles( }); const baseUrl = opts.serverUrl?.trim() || account.config.serverUrl?.trim(); const password = opts.password?.trim() || account.config.password?.trim(); - if (!baseUrl) throw new Error("BlueBubbles serverUrl is required"); - if (!password) throw new Error("BlueBubbles password is required"); + if (!baseUrl) { + throw new Error("BlueBubbles serverUrl is required"); + } + if (!password) { + throw new Error("BlueBubbles password is required"); + } const target = resolveSendTarget(to); const chatGuid = await resolveChatGuidForTarget({ @@ -414,7 +456,9 @@ export async function sendMessageBlueBubbles( throw new Error(`BlueBubbles send failed (${res.status}): ${errorText || "unknown"}`); } const body = await res.text(); - if (!body) return { messageId: "ok" }; + if (!body) { + return { messageId: "ok" }; + } try { const parsed = JSON.parse(body) as unknown; return { messageId: extractMessageId(parsed) }; diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index 11cf43e9f..738e144da 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -25,14 +25,22 @@ const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i; function parseRawChatGuid(value: string): string | null { const trimmed = value.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const parts = trimmed.split(";"); - if (parts.length !== 3) return null; + if (parts.length !== 3) { + return null; + } const service = parts[0]?.trim(); const separator = parts[1]?.trim(); const identifier = parts[2]?.trim(); - if (!service || !identifier) return null; - if (separator !== "+" && separator !== "-") return null; + if (!service || !identifier) { + return null; + } + if (separator !== "+" && separator !== "-") { + return null; + } return `${service};${separator};${identifier}`; } @@ -42,26 +50,44 @@ function stripPrefix(value: string, prefix: string): string { function stripBlueBubblesPrefix(value: string): string { const trimmed = value.trim(); - if (!trimmed) return ""; - if (!trimmed.toLowerCase().startsWith("bluebubbles:")) return trimmed; + if (!trimmed) { + return ""; + } + if (!trimmed.toLowerCase().startsWith("bluebubbles:")) { + return trimmed; + } return trimmed.slice("bluebubbles:".length).trim(); } function looksLikeRawChatIdentifier(value: string): boolean { const trimmed = value.trim(); - if (!trimmed) return false; - if (/^chat\d+$/i.test(trimmed)) return true; + if (!trimmed) { + return false; + } + if (/^chat\d+$/i.test(trimmed)) { + return true; + } return CHAT_IDENTIFIER_UUID_RE.test(trimmed) || CHAT_IDENTIFIER_HEX_RE.test(trimmed); } export function normalizeBlueBubblesHandle(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } const lowered = trimmed.toLowerCase(); - if (lowered.startsWith("imessage:")) return normalizeBlueBubblesHandle(trimmed.slice(9)); - if (lowered.startsWith("sms:")) return normalizeBlueBubblesHandle(trimmed.slice(4)); - if (lowered.startsWith("auto:")) return normalizeBlueBubblesHandle(trimmed.slice(5)); - if (trimmed.includes("@")) return trimmed.toLowerCase(); + if (lowered.startsWith("imessage:")) { + return normalizeBlueBubblesHandle(trimmed.slice(9)); + } + if (lowered.startsWith("sms:")) { + return normalizeBlueBubblesHandle(trimmed.slice(4)); + } + if (lowered.startsWith("auto:")) { + return normalizeBlueBubblesHandle(trimmed.slice(5)); + } + if (trimmed.includes("@")) { + return trimmed.toLowerCase(); + } return trimmed.replace(/\s+/g, ""); } @@ -75,30 +101,44 @@ export function extractHandleFromChatGuid(chatGuid: string): string | null { // DM format: service;-;handle (3 parts, middle is "-") if (parts.length === 3 && parts[1] === "-") { const handle = parts[2]?.trim(); - if (handle) return normalizeBlueBubblesHandle(handle); + if (handle) { + return normalizeBlueBubblesHandle(handle); + } } return null; } export function normalizeBlueBubblesMessagingTarget(raw: string): string | undefined { let trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } trimmed = stripBlueBubblesPrefix(trimmed); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } try { const parsed = parseBlueBubblesTarget(trimmed); - if (parsed.kind === "chat_id") return `chat_id:${parsed.chatId}`; + if (parsed.kind === "chat_id") { + return `chat_id:${parsed.chatId}`; + } if (parsed.kind === "chat_guid") { // For DM chat_guids, normalize to just the handle for easier comparison. // This allows "chat_guid:iMessage;-;+1234567890" to match "+1234567890". const handle = extractHandleFromChatGuid(parsed.chatGuid); - if (handle) return handle; + if (handle) { + return handle; + } // For group chats or unrecognized formats, keep the full chat_guid return `chat_guid:${parsed.chatGuid}`; } - if (parsed.kind === "chat_identifier") return `chat_identifier:${parsed.chatIdentifier}`; + if (parsed.kind === "chat_identifier") { + return `chat_identifier:${parsed.chatIdentifier}`; + } const handle = normalizeBlueBubblesHandle(parsed.to); - if (!handle) return undefined; + if (!handle) { + return undefined; + } return parsed.service === "auto" ? handle : `${parsed.service}:${handle}`; } catch { return trimmed; @@ -107,12 +147,20 @@ export function normalizeBlueBubblesMessagingTarget(raw: string): string | undef export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): boolean { const trimmed = raw.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } const candidate = stripBlueBubblesPrefix(trimmed); - if (!candidate) return false; - if (parseRawChatGuid(candidate)) return true; + if (!candidate) { + return false; + } + if (parseRawChatGuid(candidate)) { + return true; + } const lowered = candidate.toLowerCase(); - if (/^(imessage|sms|auto):/.test(lowered)) return true; + if (/^(imessage|sms|auto):/.test(lowered)) { + return true; + } if ( /^(chat_id|chatid|chat|chat_guid|chatguid|guid|chat_identifier|chatidentifier|chatident|group):/.test( lowered, @@ -121,14 +169,24 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): return true; } // Recognize chat patterns (e.g., "chat660250192681427962") as chat IDs - if (/^chat\d+$/i.test(candidate)) return true; - if (looksLikeRawChatIdentifier(candidate)) return true; - if (candidate.includes("@")) return true; + if (/^chat\d+$/i.test(candidate)) { + return true; + } + if (looksLikeRawChatIdentifier(candidate)) { + return true; + } + if (candidate.includes("@")) { + return true; + } const digitsOnly = candidate.replace(/[\s().-]/g, ""); - if (/^\+?\d{3,}$/.test(digitsOnly)) return true; + if (/^\+?\d{3,}$/.test(digitsOnly)) { + return true; + } if (normalized) { const normalizedTrimmed = normalized.trim(); - if (!normalizedTrimmed) return false; + if (!normalizedTrimmed) { + return false; + } const normalizedLower = normalizedTrimmed.toLowerCase(); if ( /^(imessage|sms|auto):/.test(normalizedLower) || @@ -142,13 +200,17 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { const trimmed = stripBlueBubblesPrefix(raw); - if (!trimmed) throw new Error("BlueBubbles target is required"); + if (!trimmed) { + throw new Error("BlueBubbles target is required"); + } const lower = trimmed.toLowerCase(); for (const { prefix, service } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); - if (!remainder) throw new Error(`${prefix} target is required`); + if (!remainder) { + throw new Error(`${prefix} target is required`); + } const remainderLower = remainder.toLowerCase(); const isChatTarget = CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) || @@ -176,7 +238,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (!value) throw new Error("chat_guid is required"); + if (!value) { + throw new Error("chat_guid is required"); + } return { kind: "chat_guid", chatGuid: value }; } } @@ -184,7 +248,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (!value) throw new Error("chat_identifier is required"); + if (!value) { + throw new Error("chat_identifier is required"); + } return { kind: "chat_identifier", chatIdentifier: value }; } } @@ -195,7 +261,9 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { if (Number.isFinite(chatId)) { return { kind: "chat_id", chatId }; } - if (!value) throw new Error("group target is required"); + if (!value) { + throw new Error("group target is required"); + } return { kind: "chat_guid", chatGuid: value }; } @@ -220,13 +288,17 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget { const trimmed = raw.trim(); - if (!trimmed) return { kind: "handle", handle: "" }; + if (!trimmed) { + return { kind: "handle", handle: "" }; + } const lower = trimmed.toLowerCase(); for (const { prefix } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); - if (!remainder) return { kind: "handle", handle: "" }; + if (!remainder) { + return { kind: "handle", handle: "" }; + } return parseBlueBubblesAllowTarget(remainder); } } @@ -235,29 +307,39 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); const chatId = Number.parseInt(value, 10); - if (Number.isFinite(chatId)) return { kind: "chat_id", chatId }; + if (Number.isFinite(chatId)) { + return { kind: "chat_id", chatId }; + } } } for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (value) return { kind: "chat_guid", chatGuid: value }; + if (value) { + return { kind: "chat_guid", chatGuid: value }; + } } } for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); - if (value) return { kind: "chat_identifier", chatIdentifier: value }; + if (value) { + return { kind: "chat_identifier", chatIdentifier: value }; + } } } if (lower.startsWith("group:")) { const value = stripPrefix(trimmed, "group:"); const chatId = Number.parseInt(value, 10); - if (Number.isFinite(chatId)) return { kind: "chat_id", chatId }; - if (value) return { kind: "chat_guid", chatGuid: value }; + if (Number.isFinite(chatId)) { + return { kind: "chat_id", chatId }; + } + if (value) { + return { kind: "chat_guid", chatGuid: value }; + } } // Handle chat pattern (e.g., "chat660250192681427962") as chat_identifier @@ -282,8 +364,12 @@ export function isAllowedBlueBubblesSender(params: { chatIdentifier?: string | null; }): boolean { const allowFrom = params.allowFrom.map((entry) => String(entry).trim()); - if (allowFrom.length === 0) return true; - if (allowFrom.includes("*")) return true; + if (allowFrom.length === 0) { + return true; + } + if (allowFrom.includes("*")) { + return true; + } const senderNormalized = normalizeBlueBubblesHandle(params.sender); const chatId = params.chatId ?? undefined; @@ -291,16 +377,26 @@ export function isAllowedBlueBubblesSender(params: { const chatIdentifier = params.chatIdentifier?.trim(); for (const entry of allowFrom) { - if (!entry) continue; + if (!entry) { + continue; + } const parsed = parseBlueBubblesAllowTarget(entry); if (parsed.kind === "chat_id" && chatId !== undefined) { - if (parsed.chatId === chatId) return true; + if (parsed.chatId === chatId) { + return true; + } } else if (parsed.kind === "chat_guid" && chatGuid) { - if (parsed.chatGuid === chatGuid) return true; + if (parsed.chatGuid === chatGuid) { + return true; + } } else if (parsed.kind === "chat_identifier" && chatIdentifier) { - if (parsed.chatIdentifier === chatIdentifier) return true; + if (parsed.chatIdentifier === chatIdentifier) { + return true; + } } else if (parsed.kind === "handle" && senderNormalized) { - if (parsed.handle === senderNormalized) return true; + if (parsed.handle === senderNormalized) { + return true; + } } } return false; @@ -315,8 +411,12 @@ export function formatBlueBubblesChatTarget(params: { return `chat_id:${params.chatId}`; } const guid = params.chatGuid?.trim(); - if (guid) return `chat_guid:${guid}`; + if (guid) { + return `chat_guid:${guid}`; + } const identifier = params.chatIdentifier?.trim(); - if (identifier) return `chat_identifier:${identifier}`; + if (identifier) { + return `chat_identifier:${identifier}`; + } return ""; } diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts index 0529b7bc5..ae674bd0d 100644 --- a/extensions/copilot-proxy/index.ts +++ b/extensions/copilot-proxy/index.ts @@ -21,10 +21,16 @@ const DEFAULT_MODEL_IDS = [ function normalizeBaseUrl(value: string): string { const trimmed = value.trim(); - if (!trimmed) return DEFAULT_BASE_URL; + if (!trimmed) { + return DEFAULT_BASE_URL; + } let normalized = trimmed; - while (normalized.endsWith("/")) normalized = normalized.slice(0, -1); - if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`; + while (normalized.endsWith("/")) { + normalized = normalized.slice(0, -1); + } + if (!normalized.endsWith("/v1")) { + normalized = `${normalized}/v1`; + } return normalized; } diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index ef7b2a0af..7598cb7f4 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -21,14 +21,22 @@ function normalizeEndpoint(endpoint?: string): string | undefined { } function resolveOtelUrl(endpoint: string | undefined, path: string): string | undefined { - if (!endpoint) return undefined; - if (endpoint.includes("/v1/")) return endpoint; + if (!endpoint) { + return undefined; + } + if (endpoint.includes("/v1/")) { + return endpoint; + } return `${endpoint}/${path}`; } function resolveSampleRate(value: number | undefined): number | undefined { - if (typeof value !== "number" || !Number.isFinite(value)) return undefined; - if (value < 0 || value > 1) return undefined; + if (typeof value !== "number" || !Number.isFinite(value)) { + return undefined; + } + if (value < 0 || value > 1) { + return undefined; + } return value; } @@ -43,7 +51,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { async start(ctx) { const cfg = ctx.config.diagnostics; const otel = cfg?.otel; - if (!cfg?.enabled || !otel?.enabled) return; + if (!cfg?.enabled || !otel?.enabled) { + return; + } const protocol = otel.protocol ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL ?? "http/protobuf"; if (protocol !== "http/protobuf") { @@ -60,7 +70,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { const tracesEnabled = otel.traces !== false; const metricsEnabled = otel.metrics !== false; const logsEnabled = otel.logs === true; - if (!tracesEnabled && !metricsEnabled && !logsEnabled) return; + if (!tracesEnabled && !metricsEnabled && !logsEnabled) { + return; + } const resource = new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: serviceName, @@ -106,7 +118,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { : {}), }); - await sdk.start(); + sdk.start(); } const logSeverityMap: Record = { @@ -201,11 +213,12 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { }); logProvider = new LoggerProvider({ resource }); logProvider.addLogRecordProcessor( - new BatchLogRecordProcessor(logExporter, { - ...(typeof otel.flushIntervalMs === "number" + new BatchLogRecordProcessor( + logExporter, + typeof otel.flushIntervalMs === "number" ? { scheduledDelayMillis: Math.max(1000, otel.flushIntervalMs) } - : {}), - }), + : {}, + ), ); const otelLogger = logProvider.getLogger("openclaw"); @@ -237,7 +250,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { const numericArgs = Object.entries(logObj) .filter(([key]) => /^\d+$/.test(key)) - .sort((a, b) => Number(a[0]) - Number(b[0])) + .toSorted((a, b) => Number(a[0]) - Number(b[0])) .map(([, value]) => value); let bindings: Record | undefined; @@ -267,7 +280,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { const attributes: Record = { "openclaw.log.level": logLevelName, }; - if (meta?.name) attributes["openclaw.logger"] = meta.name; + if (meta?.name) { + attributes["openclaw.logger"] = meta.name; + } if (meta?.parentNames?.length) { attributes["openclaw.logger.parents"] = meta.parentNames.join("."); } @@ -287,9 +302,15 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { if (numericArgs.length > 0) { attributes["openclaw.log.args"] = safeStringify(numericArgs); } - if (meta?.path?.filePath) attributes["code.filepath"] = meta.path.filePath; - if (meta?.path?.fileLine) attributes["code.lineno"] = Number(meta.path.fileLine); - if (meta?.path?.method) attributes["code.function"] = meta.path.method; + if (meta?.path?.filePath) { + attributes["code.filepath"] = meta.path.filePath; + } + if (meta?.path?.fileLine) { + attributes["code.lineno"] = Number(meta.path.fileLine); + } + if (meta?.path?.method) { + attributes["code.function"] = meta.path.method; + } if (meta?.path?.filePathWithLine) { attributes["openclaw.code.location"] = meta.path.filePathWithLine; } @@ -326,30 +347,47 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { }; const usage = evt.usage; - if (usage.input) tokensCounter.add(usage.input, { ...attrs, "openclaw.token": "input" }); - if (usage.output) tokensCounter.add(usage.output, { ...attrs, "openclaw.token": "output" }); - if (usage.cacheRead) + if (usage.input) { + tokensCounter.add(usage.input, { ...attrs, "openclaw.token": "input" }); + } + if (usage.output) { + tokensCounter.add(usage.output, { ...attrs, "openclaw.token": "output" }); + } + if (usage.cacheRead) { tokensCounter.add(usage.cacheRead, { ...attrs, "openclaw.token": "cache_read" }); - if (usage.cacheWrite) + } + if (usage.cacheWrite) { tokensCounter.add(usage.cacheWrite, { ...attrs, "openclaw.token": "cache_write" }); - if (usage.promptTokens) + } + if (usage.promptTokens) { tokensCounter.add(usage.promptTokens, { ...attrs, "openclaw.token": "prompt" }); - if (usage.total) tokensCounter.add(usage.total, { ...attrs, "openclaw.token": "total" }); + } + if (usage.total) { + tokensCounter.add(usage.total, { ...attrs, "openclaw.token": "total" }); + } - if (evt.costUsd) costCounter.add(evt.costUsd, attrs); - if (evt.durationMs) durationHistogram.record(evt.durationMs, attrs); - if (evt.context?.limit) + if (evt.costUsd) { + costCounter.add(evt.costUsd, attrs); + } + if (evt.durationMs) { + durationHistogram.record(evt.durationMs, attrs); + } + if (evt.context?.limit) { contextHistogram.record(evt.context.limit, { ...attrs, "openclaw.context": "limit", }); - if (evt.context?.used) + } + if (evt.context?.used) { contextHistogram.record(evt.context.used, { ...attrs, "openclaw.context": "used", }); + } - if (!tracesEnabled) return; + if (!tracesEnabled) { + return; + } const spanAttrs: Record = { ...attrs, "openclaw.sessionKey": evt.sessionKey ?? "", @@ -385,9 +423,13 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { if (typeof evt.durationMs === "number") { webhookDurationHistogram.record(evt.durationMs, attrs); } - if (!tracesEnabled) return; + if (!tracesEnabled) { + return; + } const spanAttrs: Record = { ...attrs }; - if (evt.chatId !== undefined) spanAttrs["openclaw.chatId"] = String(evt.chatId); + if (evt.chatId !== undefined) { + spanAttrs["openclaw.chatId"] = String(evt.chatId); + } const span = spanWithDuration("openclaw.webhook.processed", spanAttrs, evt.durationMs); span.end(); }; @@ -400,12 +442,16 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { "openclaw.webhook": evt.updateType ?? "unknown", }; webhookErrorCounter.add(1, attrs); - if (!tracesEnabled) return; + if (!tracesEnabled) { + return; + } const spanAttrs: Record = { ...attrs, "openclaw.error": evt.error, }; - if (evt.chatId !== undefined) spanAttrs["openclaw.chatId"] = String(evt.chatId); + if (evt.chatId !== undefined) { + spanAttrs["openclaw.chatId"] = String(evt.chatId); + } const span = tracer.startSpan("openclaw.webhook.error", { attributes: spanAttrs, }); @@ -437,13 +483,25 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { if (typeof evt.durationMs === "number") { messageDurationHistogram.record(evt.durationMs, attrs); } - if (!tracesEnabled) return; + if (!tracesEnabled) { + return; + } const spanAttrs: Record = { ...attrs }; - if (evt.sessionKey) spanAttrs["openclaw.sessionKey"] = evt.sessionKey; - if (evt.sessionId) spanAttrs["openclaw.sessionId"] = evt.sessionId; - if (evt.chatId !== undefined) spanAttrs["openclaw.chatId"] = String(evt.chatId); - if (evt.messageId !== undefined) spanAttrs["openclaw.messageId"] = String(evt.messageId); - if (evt.reason) spanAttrs["openclaw.reason"] = evt.reason; + if (evt.sessionKey) { + spanAttrs["openclaw.sessionKey"] = evt.sessionKey; + } + if (evt.sessionId) { + spanAttrs["openclaw.sessionId"] = evt.sessionId; + } + if (evt.chatId !== undefined) { + spanAttrs["openclaw.chatId"] = String(evt.chatId); + } + if (evt.messageId !== undefined) { + spanAttrs["openclaw.messageId"] = String(evt.messageId); + } + if (evt.reason) { + spanAttrs["openclaw.reason"] = evt.reason; + } const span = spanWithDuration("openclaw.message.processed", spanAttrs, evt.durationMs); if (evt.outcome === "error") { span.setStatus({ code: SpanStatusCode.ERROR, message: evt.error }); @@ -474,7 +532,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { evt: Extract, ) => { const attrs: Record = { "openclaw.state": evt.state }; - if (evt.reason) attrs["openclaw.reason"] = evt.reason; + if (evt.reason) { + attrs["openclaw.reason"] = evt.reason; + } sessionStateCounter.add(1, attrs); }; @@ -486,10 +546,16 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { if (typeof evt.ageMs === "number") { sessionStuckAgeHistogram.record(evt.ageMs, attrs); } - if (!tracesEnabled) return; + if (!tracesEnabled) { + return; + } const spanAttrs: Record = { ...attrs }; - if (evt.sessionKey) spanAttrs["openclaw.sessionKey"] = evt.sessionKey; - if (evt.sessionId) spanAttrs["openclaw.sessionId"] = evt.sessionId; + if (evt.sessionKey) { + spanAttrs["openclaw.sessionKey"] = evt.sessionKey; + } + if (evt.sessionId) { + spanAttrs["openclaw.sessionId"] = evt.sessionId; + } spanAttrs["openclaw.queueDepth"] = evt.queueDepth ?? 0; spanAttrs["openclaw.ageMs"] = evt.ageMs; const span = tracer.startSpan("openclaw.session.stuck", { attributes: spanAttrs }); diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 893df2bf0..f28f7483b 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -331,7 +331,9 @@ export const discordPlugin: ChannelPlugin = { cfg, accountId: account.accountId, }); - if (!channelIds.length && unresolvedChannels === 0) return undefined; + if (!channelIds.length && unresolvedChannels === 0) { + return undefined; + } const botToken = account.token?.trim(); if (!botToken) { return { @@ -383,7 +385,9 @@ export const discordPlugin: ChannelPlugin = { includeApplication: true, }); const username = probe.ok ? probe.bot?.username?.trim() : null; - if (username) discordBotLabel = ` (@${username})`; + if (username) { + discordBotLabel = ` (@${username})`; + } ctx.setStatus({ accountId: account.accountId, bot: probe.bot, diff --git a/extensions/google-antigravity-auth/index.ts b/extensions/google-antigravity-auth/index.ts index 346c7602c..74f9406c4 100644 --- a/extensions/google-antigravity-auth/index.ts +++ b/extensions/google-antigravity-auth/index.ts @@ -49,7 +49,9 @@ function generatePkce(): { verifier: string; challenge: string } { } function isWSL(): boolean { - if (process.platform !== "linux") return false; + if (process.platform !== "linux") { + return false; + } try { const release = readFileSync("/proc/version", "utf8").toLowerCase(); return release.includes("microsoft") || release.includes("wsl"); @@ -59,7 +61,9 @@ function isWSL(): boolean { } function isWSL2(): boolean { - if (!isWSL()) return false; + if (!isWSL()) { + return false; + } try { const version = readFileSync("/proc/version", "utf8").toLowerCase(); return version.includes("wsl2") || version.includes("microsoft-standard"); @@ -88,14 +92,20 @@ function buildAuthUrl(params: { challenge: string; state: string }): string { function parseCallbackInput(input: string): { code: string; state: string } | { error: string } { const trimmed = input.trim(); - if (!trimmed) return { error: "No input provided" }; + if (!trimmed) { + return { error: "No input provided" }; + } try { const url = new URL(trimmed); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); - if (!code) return { error: "Missing 'code' parameter in URL" }; - if (!state) return { error: "Missing 'state' parameter in URL" }; + if (!code) { + return { error: "Missing 'code' parameter in URL" }; + } + if (!state) { + return { error: "Missing 'state' parameter in URL" }; + } return { code, state }; } catch { return { error: "Paste the full redirect URL (not just the code)." }; @@ -112,12 +122,16 @@ async function startCallbackServer(params: { timeoutMs: number }) { const callbackPromise = new Promise((resolve, reject) => { resolveCallback = (url) => { - if (settled) return; + if (settled) { + return; + } settled = true; resolve(url); }; rejectCallback = (err) => { - if (settled) return; + if (settled) { + return; + } settled = true; reject(err); }; @@ -204,8 +218,12 @@ async function exchangeCode(params: { const refresh = data.refresh_token?.trim(); const expiresIn = data.expires_in ?? 0; - if (!access) throw new Error("Token exchange returned no access_token"); - if (!refresh) throw new Error("Token exchange returned no refresh_token"); + if (!access) { + throw new Error("Token exchange returned no access_token"); + } + if (!refresh) { + throw new Error("Token exchange returned no refresh_token"); + } const expires = Date.now() + expiresIn * 1000 - 5 * 60 * 1000; return { access, refresh, expires }; @@ -216,7 +234,9 @@ async function fetchUserEmail(accessToken: string): Promise const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${accessToken}` }, }); - if (!response.ok) return undefined; + if (!response.ok) { + return undefined; + } const data = (await response.json()) as { email?: string }; return data.email; } catch { @@ -251,7 +271,9 @@ async function fetchProjectId(accessToken: string): Promise { }), }); - if (!response.ok) continue; + if (!response.ok) { + continue; + } const data = (await response.json()) as { cloudaicompanionProject?: string | { id?: string }; }; @@ -342,12 +364,16 @@ async function loginAntigravity(params: { params.progress.update("Waiting for redirect URL…"); const input = await params.prompt("Paste the redirect URL: "); const parsed = parseCallbackInput(input); - if ("error" in parsed) throw new Error(parsed.error); + if ("error" in parsed) { + throw new Error(parsed.error); + } code = parsed.code; returnedState = parsed.state; } - if (!code) throw new Error("Missing OAuth code"); + if (!code) { + throw new Error("Missing OAuth code"); + } if (returnedState !== state) { throw new Error("OAuth state mismatch. Please try again."); } diff --git a/extensions/google-gemini-cli-auth/oauth.test.ts b/extensions/google-gemini-cli-auth/oauth.test.ts index a6ee8ee98..fad1bc015 100644 --- a/extensions/google-gemini-cli-auth/oauth.test.ts +++ b/extensions/google-gemini-cli-auth/oauth.test.ts @@ -83,8 +83,12 @@ describe("extractGeminiCliCredentials", () => { mockExistsSync.mockImplementation((p: string) => { const normalized = normalizePath(p); - if (normalized === normalizePath(fakeGeminiPath)) return true; - if (normalized === normalizePath(fakeOauth2Path)) return true; + if (normalized === normalizePath(fakeGeminiPath)) { + return true; + } + if (normalized === normalizePath(fakeOauth2Path)) { + return true; + } return false; }); mockRealpathSync.mockReturnValue(fakeResolvedPath); @@ -160,8 +164,12 @@ describe("extractGeminiCliCredentials", () => { mockExistsSync.mockImplementation((p: string) => { const normalized = normalizePath(p); - if (normalized === normalizePath(fakeGeminiPath)) return true; - if (normalized === normalizePath(fakeOauth2Path)) return true; + if (normalized === normalizePath(fakeGeminiPath)) { + return true; + } + if (normalized === normalizePath(fakeOauth2Path)) { + return true; + } return false; }); mockRealpathSync.mockReturnValue(fakeResolvedPath); @@ -205,8 +213,12 @@ describe("extractGeminiCliCredentials", () => { mockExistsSync.mockImplementation((p: string) => { const normalized = normalizePath(p); - if (normalized === normalizePath(fakeGeminiPath)) return true; - if (normalized === normalizePath(fakeOauth2Path)) return true; + if (normalized === normalizePath(fakeGeminiPath)) { + return true; + } + if (normalized === normalizePath(fakeOauth2Path)) { + return true; + } return false; }); mockRealpathSync.mockReturnValue(fakeResolvedPath); diff --git a/extensions/google-gemini-cli-auth/oauth.ts b/extensions/google-gemini-cli-auth/oauth.ts index c385472cb..5d386f210 100644 --- a/extensions/google-gemini-cli-auth/oauth.ts +++ b/extensions/google-gemini-cli-auth/oauth.ts @@ -43,7 +43,9 @@ export type GeminiCliOAuthContext = { function resolveEnv(keys: string[]): string | undefined { for (const key of keys) { const value = process.env[key]?.trim(); - if (value) return value; + if (value) { + return value; + } } return undefined; } @@ -57,11 +59,15 @@ export function clearCredentialsCache(): void { /** Extracts OAuth credentials from the installed Gemini CLI's bundled oauth2.js. */ export function extractGeminiCliCredentials(): { clientId: string; clientSecret: string } | null { - if (cachedGeminiCliCredentials) return cachedGeminiCliCredentials; + if (cachedGeminiCliCredentials) { + return cachedGeminiCliCredentials; + } try { const geminiPath = findInPath("gemini"); - if (!geminiPath) return null; + if (!geminiPath) { + return null; + } const resolvedPath = realpathSync(geminiPath); const geminiCliDir = dirname(dirname(resolvedPath)); @@ -97,9 +103,13 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret: } if (!content) { const found = findFile(geminiCliDir, "oauth2.js", 10); - if (found) content = readFileSync(found, "utf8"); + if (found) { + content = readFileSync(found, "utf8"); + } + } + if (!content) { + return null; } - if (!content) return null; const idMatch = content.match(/(\d+-[a-z0-9]+\.apps\.googleusercontent\.com)/); const secretMatch = content.match(/(GOCSPX-[A-Za-z0-9_-]+)/); @@ -118,21 +128,29 @@ function findInPath(name: string): string | null { for (const dir of (process.env.PATH ?? "").split(delimiter)) { for (const ext of exts) { const p = join(dir, name + ext); - if (existsSync(p)) return p; + if (existsSync(p)) { + return p; + } } } return null; } function findFile(dir: string, name: string, depth: number): string | null { - if (depth <= 0) return null; + if (depth <= 0) { + return null; + } try { for (const e of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, e.name); - if (e.isFile() && e.name === name) return p; + if (e.isFile() && e.name === name) { + return p; + } if (e.isDirectory() && !e.name.startsWith(".")) { const found = findFile(p, name, depth - 1); - if (found) return found; + if (found) { + return found; + } } } } catch {} @@ -160,7 +178,9 @@ function resolveOAuthClientConfig(): { clientId: string; clientSecret?: string } } function isWSL(): boolean { - if (process.platform !== "linux") return false; + if (process.platform !== "linux") { + return false; + } try { const release = readFileSync("/proc/version", "utf8").toLowerCase(); return release.includes("microsoft") || release.includes("wsl"); @@ -170,7 +190,9 @@ function isWSL(): boolean { } function isWSL2(): boolean { - if (!isWSL()) return false; + if (!isWSL()) { + return false; + } try { const version = readFileSync("/proc/version", "utf8").toLowerCase(); return version.includes("wsl2") || version.includes("microsoft-standard"); @@ -210,17 +232,25 @@ function parseCallbackInput( expectedState: string, ): { code: string; state: string } | { error: string } { const trimmed = input.trim(); - if (!trimmed) return { error: "No input provided" }; + if (!trimmed) { + return { error: "No input provided" }; + } try { const url = new URL(trimmed); const code = url.searchParams.get("code"); const state = url.searchParams.get("state") ?? expectedState; - if (!code) return { error: "Missing 'code' parameter in URL" }; - if (!state) return { error: "Missing 'state' parameter. Paste the full URL." }; + if (!code) { + return { error: "Missing 'code' parameter in URL" }; + } + if (!state) { + return { error: "Missing 'state' parameter. Paste the full URL." }; + } return { code, state }; } catch { - if (!expectedState) return { error: "Paste the full redirect URL, not just the code." }; + if (!expectedState) { + return { error: "Paste the full redirect URL, not just the code." }; + } return { code: trimmed, state: expectedState }; } } @@ -289,7 +319,9 @@ async function waitForLocalCallback(params: { }); const finish = (err?: Error, result?: { code: string; state: string }) => { - if (timeout) clearTimeout(timeout); + if (timeout) { + clearTimeout(timeout); + } try { server.close(); } catch { @@ -427,14 +459,20 @@ async function discoverProject(accessToken: string): Promise { if (err instanceof Error) { throw err; } - throw new Error("loadCodeAssist failed"); + throw new Error("loadCodeAssist failed", { cause: err }); } if (data.currentTier) { const project = data.cloudaicompanionProject; - if (typeof project === "string" && project) return project; - if (typeof project === "object" && project?.id) return project.id; - if (envProject) return envProject; + if (typeof project === "string" && project) { + return project; + } + if (typeof project === "object" && project?.id) { + return project.id; + } + if (envProject) { + return envProject; + } throw new Error( "This account requires GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID to be set.", ); @@ -482,8 +520,12 @@ async function discoverProject(accessToken: string): Promise { } const projectId = lro.response?.cloudaicompanionProject?.id; - if (projectId) return projectId; - if (envProject) return envProject; + if (projectId) { + return projectId; + } + if (envProject) { + return envProject; + } throw new Error( "Could not discover or provision a Google Cloud project. Set GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID.", @@ -491,11 +533,17 @@ async function discoverProject(accessToken: string): Promise { } function isVpcScAffected(payload: unknown): boolean { - if (!payload || typeof payload !== "object") return false; + if (!payload || typeof payload !== "object") { + return false; + } const error = (payload as { error?: unknown }).error; - if (!error || typeof error !== "object") return false; + if (!error || typeof error !== "object") { + return false; + } const details = (error as { details?: unknown[] }).details; - if (!Array.isArray(details)) return false; + if (!Array.isArray(details)) { + return false; + } return details.some( (item) => typeof item === "object" && @@ -507,7 +555,9 @@ function isVpcScAffected(payload: unknown): boolean { function getDefaultTier( allowedTiers?: Array<{ id?: string; isDefault?: boolean }>, ): { id?: string } | undefined { - if (!allowedTiers?.length) return { id: TIER_LEGACY }; + if (!allowedTiers?.length) { + return { id: TIER_LEGACY }; + } return allowedTiers.find((tier) => tier.isDefault) ?? { id: TIER_LEGACY }; } @@ -520,12 +570,16 @@ async function pollOperation( const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, { headers, }); - if (!response.ok) continue; + if (!response.ok) { + continue; + } const data = (await response.json()) as { done?: boolean; response?: { cloudaicompanionProject?: { id?: string } }; }; - if (data.done) return data; + if (data.done) { + return data; + } } throw new Error("Operation polling timeout"); } @@ -558,7 +612,9 @@ export async function loginGeminiCliOAuth( ctx.progress.update("Waiting for you to paste the callback URL..."); const callbackInput = await ctx.prompt("Paste the redirect URL here: "); const parsed = parseCallbackInput(callbackInput, verifier); - if ("error" in parsed) throw new Error(parsed.error); + if ("error" in parsed) { + throw new Error(parsed.error); + } if (parsed.state !== verifier) { throw new Error("OAuth state mismatch - please try again"); } @@ -592,9 +648,11 @@ export async function loginGeminiCliOAuth( ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`); const callbackInput = await ctx.prompt("Paste the redirect URL here: "); const parsed = parseCallbackInput(callbackInput, verifier); - if ("error" in parsed) throw new Error(parsed.error); + if ("error" in parsed) { + throw new Error(parsed.error, { cause: err }); + } if (parsed.state !== verifier) { - throw new Error("OAuth state mismatch - please try again"); + throw new Error("OAuth state mismatch - please try again", { cause: err }); } ctx.progress.update("Exchanging authorization code for tokens..."); return exchangeCodeForTokens(parsed.code, verifier); diff --git a/extensions/googlechat/src/accounts.ts b/extensions/googlechat/src/accounts.ts index ade8a38ae..e81c86ff7 100644 --- a/extensions/googlechat/src/accounts.ts +++ b/extensions/googlechat/src/accounts.ts @@ -1,7 +1,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk"; -import type { GoogleChatAccountConfig, GoogleChatConfig } from "./types.config.js"; +import type { GoogleChatAccountConfig } from "./types.config.js"; export type GoogleChatCredentialSource = "file" | "inline" | "env" | "none"; @@ -19,22 +19,30 @@ const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT"; const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"; function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { - const accounts = (cfg.channels?.["googlechat"] as GoogleChatConfig | undefined)?.accounts; - if (!accounts || typeof accounts !== "object") return []; + const accounts = cfg.channels?.["googlechat"]?.accounts; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } export function listGoogleChatAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultGoogleChatAccountId(cfg: OpenClawConfig): string { - const channel = cfg.channels?.["googlechat"] as GoogleChatConfig | undefined; - if (channel?.defaultAccount?.trim()) return channel.defaultAccount.trim(); + const channel = cfg.channels?.["googlechat"]; + if (channel?.defaultAccount?.trim()) { + return channel.defaultAccount.trim(); + } const ids = listGoogleChatAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -42,8 +50,10 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): GoogleChatAccountConfig | undefined { - const accounts = (cfg.channels?.["googlechat"] as GoogleChatConfig | undefined)?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + const accounts = cfg.channels?.["googlechat"]?.accounts; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as GoogleChatAccountConfig | undefined; } @@ -51,17 +61,23 @@ function mergeGoogleChatAccountConfig( cfg: OpenClawConfig, accountId: string, ): GoogleChatAccountConfig { - const raw = (cfg.channels?.["googlechat"] ?? {}) as GoogleChatConfig; + const raw = cfg.channels?.["googlechat"] ?? {}; const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw; const account = resolveAccountConfig(cfg, accountId) ?? {}; return { ...base, ...account } as GoogleChatAccountConfig; } function parseServiceAccount(value: unknown): Record | null { - if (value && typeof value === "object") return value as Record; - if (typeof value !== "string") return null; + if (value && typeof value === "object") { + return value as Record; + } + if (typeof value !== "string") { + return null; + } const trimmed = value.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } try { return JSON.parse(trimmed) as Record; } catch { @@ -108,8 +124,7 @@ export function resolveGoogleChatAccount(params: { accountId?: string | null; }): ResolvedGoogleChatAccount { const accountId = normalizeAccountId(params.accountId); - const baseEnabled = - (params.cfg.channels?.["googlechat"] as GoogleChatConfig | undefined)?.enabled !== false; + const baseEnabled = params.cfg.channels?.["googlechat"]?.enabled !== false; const merged = mergeGoogleChatAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; diff --git a/extensions/googlechat/src/actions.ts b/extensions/googlechat/src/actions.ts index bbb928030..b62a53517 100644 --- a/extensions/googlechat/src/actions.ts +++ b/extensions/googlechat/src/actions.ts @@ -39,7 +39,9 @@ function isReactionsEnabled(accounts: ReturnType, cf boolean | undefined >, ); - if (gate("reactions")) return true; + if (gate("reactions")) { + return true; + } } return false; } @@ -50,11 +52,13 @@ function resolveAppUserNames(account: { config: { botUser?: string | null } }) { export const googlechatMessageActions: ChannelMessageActionAdapter = { listActions: ({ cfg }) => { - const accounts = listEnabledAccounts(cfg as OpenClawConfig); - if (accounts.length === 0) return []; + const accounts = listEnabledAccounts(cfg); + if (accounts.length === 0) { + return []; + } const actions = new Set([]); actions.add("send"); - if (isReactionsEnabled(accounts, cfg as OpenClawConfig)) { + if (isReactionsEnabled(accounts, cfg)) { actions.add("react"); actions.add("reactions"); } @@ -62,15 +66,19 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = { }, extractToolSend: ({ args }) => { const action = typeof args.action === "string" ? args.action.trim() : ""; - if (action !== "sendMessage") return null; + if (action !== "sendMessage") { + return null; + } const to = typeof args.to === "string" ? args.to : undefined; - if (!to) return null; + if (!to) { + return null; + } const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined; return { to, accountId }; }, handleAction: async ({ action, params, cfg, accountId }) => { const account = resolveGoogleChatAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, }); if (account.credentialSource === "none") { @@ -134,12 +142,18 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = { const appUsers = resolveAppUserNames(account); const toRemove = reactions.filter((reaction) => { const userName = reaction.user?.name?.trim(); - if (appUsers.size > 0 && !appUsers.has(userName ?? "")) return false; - if (emoji) return reaction.emoji?.unicode === emoji; + if (appUsers.size > 0 && !appUsers.has(userName ?? "")) { + return false; + } + if (emoji) { + return reaction.emoji?.unicode === emoji; + } return true; }); for (const reaction of toRemove) { - if (!reaction.name) continue; + if (!reaction.name) { + continue; + } await deleteGoogleChatReaction({ account, reactionName: reaction.name }); } return jsonResult({ ok: true, removed: toRemove.length }); diff --git a/extensions/googlechat/src/api.ts b/extensions/googlechat/src/api.ts index b487a2627..013b239f3 100644 --- a/extensions/googlechat/src/api.ts +++ b/extensions/googlechat/src/api.ts @@ -7,6 +7,13 @@ import type { GoogleChatReaction } from "./types.js"; const CHAT_API_BASE = "https://chat.googleapis.com/v1"; const CHAT_UPLOAD_BASE = "https://chat.googleapis.com/upload/v1"; +const headersToObject = (headers?: HeadersInit): Record => + headers instanceof Headers + ? Object.fromEntries(headers.entries()) + : Array.isArray(headers) + ? Object.fromEntries(headers) + : headers || {}; + async function fetchJson( account: ResolvedGoogleChatAccount, url: string, @@ -16,9 +23,9 @@ async function fetchJson( const res = await fetch(url, { ...init, headers: { + ...headersToObject(init.headers), Authorization: `Bearer ${token}`, "Content-Type": "application/json", - ...(init.headers ?? {}), }, }); if (!res.ok) { @@ -37,8 +44,8 @@ async function fetchOk( const res = await fetch(url, { ...init, headers: { + ...headersToObject(init.headers), Authorization: `Bearer ${token}`, - ...(init.headers ?? {}), }, }); if (!res.ok) { @@ -57,8 +64,8 @@ async function fetchBuffer( const res = await fetch(url, { ...init, headers: { + ...headersToObject(init?.headers), Authorization: `Bearer ${token}`, - ...(init?.headers ?? {}), }, }); if (!res.ok) { @@ -83,8 +90,12 @@ async function fetchBuffer( let total = 0; while (true) { const { done, value } = await reader.read(); - if (done) break; - if (!value) continue; + if (done) { + break; + } + if (!value) { + continue; + } total += value.length; if (total > maxBytes) { await reader.cancel(); @@ -106,8 +117,12 @@ export async function sendGoogleChatMessage(params: { }): Promise<{ messageName?: string } | null> { const { account, space, text, thread, attachments } = params; const body: Record = {}; - if (text) body.text = text; - if (thread) body.thread = { name: thread }; + if (text) { + body.text = text; + } + if (thread) { + body.thread = { name: thread }; + } if (attachments && attachments.length > 0) { body.attachment = attachments.map((item) => ({ attachmentDataRef: { attachmentUploadToken: item.attachmentUploadToken }, @@ -182,7 +197,9 @@ export async function uploadGoogleChatAttachment(params: { const payload = (await res.json()) as { attachmentDataRef?: { attachmentUploadToken?: string }; }; - return { attachmentUploadToken: payload.attachmentDataRef?.attachmentUploadToken }; + return { + attachmentUploadToken: payload.attachmentDataRef?.attachmentUploadToken, + }; } export async function downloadGoogleChatMedia(params: { @@ -215,7 +232,9 @@ export async function listGoogleChatReactions(params: { }): Promise { const { account, messageName, limit } = params; const url = new URL(`${CHAT_API_BASE}/${messageName}/reactions`); - if (limit && limit > 0) url.searchParams.set("pageSize", String(limit)); + if (limit && limit > 0) { + url.searchParams.set("pageSize", String(limit)); + } const result = await fetchJson<{ reactions?: GoogleChatReaction[] }>(account, url.toString(), { method: "GET", }); @@ -251,9 +270,14 @@ export async function probeGoogleChat(account: ResolvedGoogleChatAccount): Promi try { const url = new URL(`${CHAT_API_BASE}/spaces`); url.searchParams.set("pageSize", "1"); - await fetchJson>(account, url.toString(), { method: "GET" }); + await fetchJson>(account, url.toString(), { + method: "GET", + }); return { ok: true }; } catch (err) { - return { ok: false, error: err instanceof Error ? err.message : String(err) }; + return { + ok: false, + error: err instanceof Error ? err.message : String(err), + }; } } diff --git a/extensions/googlechat/src/auth.ts b/extensions/googlechat/src/auth.ts index 27cc2ae65..221d36f9e 100644 --- a/extensions/googlechat/src/auth.ts +++ b/extensions/googlechat/src/auth.ts @@ -15,15 +15,21 @@ const verifyClient = new OAuth2Client(); let cachedCerts: { fetchedAt: number; certs: Record } | null = null; function buildAuthKey(account: ResolvedGoogleChatAccount): string { - if (account.credentialsFile) return `file:${account.credentialsFile}`; - if (account.credentials) return `inline:${JSON.stringify(account.credentials)}`; + if (account.credentialsFile) { + return `file:${account.credentialsFile}`; + } + if (account.credentials) { + return `inline:${JSON.stringify(account.credentials)}`; + } return "none"; } function getAuthInstance(account: ResolvedGoogleChatAccount): GoogleAuth { const key = buildAuthKey(account); const cached = authCache.get(account.accountId); - if (cached && cached.key === key) return cached.auth; + if (cached && cached.key === key) { + return cached.auth; + } if (account.credentialsFile) { const auth = new GoogleAuth({ keyFile: account.credentialsFile, scopes: [CHAT_SCOPE] }); @@ -77,9 +83,13 @@ export async function verifyGoogleChatRequest(params: { audience?: string | null; }): Promise<{ ok: boolean; reason?: string }> { const bearer = params.bearer?.trim(); - if (!bearer) return { ok: false, reason: "missing token" }; + if (!bearer) { + return { ok: false, reason: "missing token" }; + } const audience = params.audience?.trim(); - if (!audience) return { ok: false, reason: "missing audience" }; + if (!audience) { + return { ok: false, reason: "missing audience" }; + } const audienceType = params.audienceType ?? null; if (audienceType === "app-url") { diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 7b7d42fdd..8c329c447 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -59,10 +59,9 @@ export const googlechatDock: ChannelDock = { outbound: { textChunkLimit: 4000 }, config: { resolveAllowFrom: ({ cfg, accountId }) => - ( - resolveGoogleChatAccount({ cfg: cfg as OpenClawConfig, accountId }).config.dm?.allowFrom ?? - [] - ).map((entry) => String(entry)), + (resolveGoogleChatAccount({ cfg: cfg, accountId }).config.dm?.allowFrom ?? []).map((entry) => + String(entry), + ), formatAllowFrom: ({ allowFrom }) => allowFrom .map((entry) => String(entry)) @@ -104,8 +103,10 @@ export const googlechatPlugin: ChannelPlugin = { idLabel: "googlechatUserId", normalizeAllowEntry: (entry) => formatAllowFromEntry(entry), notifyApproval: async ({ cfg, id }) => { - const account = resolveGoogleChatAccount({ cfg: cfg as OpenClawConfig }); - if (account.credentialSource === "none") return; + const account = resolveGoogleChatAccount({ cfg: cfg }); + if (account.credentialSource === "none") { + return; + } const user = normalizeGoogleChatTarget(id) ?? id; const target = isGoogleChatUserTarget(user) ? user : `users/${user}`; const space = await resolveGoogleChatOutboundSpace({ account, target }); @@ -130,13 +131,12 @@ export const googlechatPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.googlechat"] }, configSchema: buildChannelConfigSchema(GoogleChatConfigSchema), config: { - listAccountIds: (cfg) => listGoogleChatAccountIds(cfg as OpenClawConfig), - resolveAccount: (cfg, accountId) => - resolveGoogleChatAccount({ cfg: cfg as OpenClawConfig, accountId }), - defaultAccountId: (cfg) => resolveDefaultGoogleChatAccountId(cfg as OpenClawConfig), + listAccountIds: (cfg) => listGoogleChatAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveGoogleChatAccount({ cfg: cfg, accountId }), + defaultAccountId: (cfg) => resolveDefaultGoogleChatAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "googlechat", accountId, enabled, @@ -144,7 +144,7 @@ export const googlechatPlugin: ChannelPlugin = { }), deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "googlechat", accountId, clearBaseFields: [ @@ -169,7 +169,7 @@ export const googlechatPlugin: ChannelPlugin = { resolveAllowFrom: ({ cfg, accountId }) => ( resolveGoogleChatAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, }).config.dm?.allowFrom ?? [] ).map((entry) => String(entry)), @@ -182,9 +182,7 @@ export const googlechatPlugin: ChannelPlugin = { security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean( - (cfg as OpenClawConfig).channels?.["googlechat"]?.accounts?.[resolvedAccountId], - ); + const useAccountPath = Boolean(cfg.channels?.["googlechat"]?.accounts?.[resolvedAccountId]); const allowFromPath = useAccountPath ? `channels.googlechat.accounts.${resolvedAccountId}.dm.` : "channels.googlechat.dm."; @@ -233,7 +231,7 @@ export const googlechatPlugin: ChannelPlugin = { self: async () => null, listPeers: async ({ cfg, accountId, query, limit }) => { const account = resolveGoogleChatAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, }); const q = query?.trim().toLowerCase() || ""; @@ -253,7 +251,7 @@ export const googlechatPlugin: ChannelPlugin = { }, listGroups: async ({ cfg, accountId, query, limit }) => { const account = resolveGoogleChatAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, }); const groups = account.config.groups ?? {}; @@ -293,7 +291,7 @@ export const googlechatPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "googlechat", accountId, name, @@ -309,7 +307,7 @@ export const googlechatPlugin: ChannelPlugin = { }, applyAccountConfig: ({ cfg, accountId, input }) => { const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "googlechat", accountId, name: input.name, @@ -317,7 +315,7 @@ export const googlechatPlugin: ChannelPlugin = { const next = accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({ - cfg: namedConfig as OpenClawConfig, + cfg: namedConfig, channelKey: "googlechat", }) : namedConfig; @@ -345,7 +343,7 @@ export const googlechatPlugin: ChannelPlugin = { channels: { ...next.channels, googlechat: { - ...(next.channels?.["googlechat"] ?? {}), + ...next.channels?.["googlechat"], enabled: true, ...configPatch, }, @@ -357,12 +355,12 @@ export const googlechatPlugin: ChannelPlugin = { channels: { ...next.channels, googlechat: { - ...(next.channels?.["googlechat"] ?? {}), + ...next.channels?.["googlechat"], enabled: true, accounts: { - ...(next.channels?.["googlechat"]?.accounts ?? {}), + ...next.channels?.["googlechat"]?.accounts, [accountId]: { - ...(next.channels?.["googlechat"]?.accounts?.[accountId] ?? {}), + ...next.channels?.["googlechat"]?.accounts?.[accountId], enabled: true, ...configPatch, }, @@ -415,7 +413,7 @@ export const googlechatPlugin: ChannelPlugin = { }, sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => { const account = resolveGoogleChatAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, }); const space = await resolveGoogleChatOutboundSpace({ account, target: to }); @@ -437,14 +435,14 @@ export const googlechatPlugin: ChannelPlugin = { throw new Error("Google Chat mediaUrl is required."); } const account = resolveGoogleChatAccount({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, }); const space = await resolveGoogleChatOutboundSpace({ account, target: to }); const thread = (threadId ?? replyToId ?? undefined) as string | undefined; const runtime = getGoogleChatRuntime(); const maxBytes = resolveChannelMediaMaxBytes({ - cfg: cfg as OpenClawConfig, + cfg: cfg, resolveChannelLimitMb: ({ cfg, accountId }) => ( cfg.channels?.["googlechat"] as @@ -493,7 +491,9 @@ export const googlechatPlugin: ChannelPlugin = { const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID); const enabled = entry.enabled !== false; const configured = entry.configured === true; - if (!enabled || !configured) return []; + if (!enabled || !configured) { + return []; + } const issues = []; if (!entry.audience) { issues.push({ @@ -564,7 +564,7 @@ export const googlechatPlugin: ChannelPlugin = { }); const unregister = await startGoogleChatMonitor({ account, - config: ctx.cfg as OpenClawConfig, + config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, webhookPath: account.config.webhookPath, diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 505656b1d..144896de4 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -60,7 +60,9 @@ function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv, function normalizeWebhookPath(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return "/"; + if (!trimmed) { + return "/"; + } const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; if (withSlash.length > 1 && withSlash.endsWith("/")) { return withSlash.slice(0, -1); @@ -70,7 +72,9 @@ function normalizeWebhookPath(raw: string): string { function resolveWebhookPath(webhookPath?: string, webhookUrl?: string): string | null { const trimmedPath = webhookPath?.trim(); - if (trimmedPath) return normalizeWebhookPath(trimmedPath); + if (trimmedPath) { + return normalizeWebhookPath(trimmedPath); + } if (webhookUrl?.trim()) { try { const parsed = new URL(webhookUrl); @@ -88,7 +92,9 @@ async function readJsonBody(req: IncomingMessage, maxBytes: number) { return await new Promise<{ ok: boolean; value?: unknown; error?: string }>((resolve) => { let resolved = false; const doResolve = (value: { ok: boolean; value?: unknown; error?: string }) => { - if (resolved) return; + if (resolved) { + return; + } resolved = true; req.removeAllListeners(); resolve(value); @@ -158,7 +164,9 @@ export async function handleGoogleChatWebhookRequest( const url = new URL(req.url ?? "/", "http://localhost"); const path = normalizeWebhookPath(url.pathname); const targets = webhookTargets.get(path); - if (!targets || targets.length === 0) return false; + if (!targets || targets.length === 0) { + return false; + } if (req.method !== "POST") { res.statusCode = 405; @@ -279,8 +287,12 @@ export async function handleGoogleChatWebhookRequest( async function processGoogleChatEvent(event: GoogleChatEvent, target: WebhookTarget) { const eventType = event.type ?? (event as { eventType?: string }).eventType; - if (eventType !== "MESSAGE") return; - if (!event.message || !event.space) return; + if (eventType !== "MESSAGE") { + return; + } + if (!event.message || !event.space) { + return; + } await processMessageWithPipeline({ event, @@ -295,7 +307,9 @@ async function processGoogleChatEvent(event: GoogleChatEvent, target: WebhookTar function normalizeUserId(raw?: string | null): string { const trimmed = raw?.trim() ?? ""; - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } return trimmed.replace(/^users\//i, "").toLowerCase(); } @@ -304,16 +318,28 @@ export function isSenderAllowed( senderEmail: string | undefined, allowFrom: string[], ) { - if (allowFrom.includes("*")) return true; + if (allowFrom.includes("*")) { + return true; + } const normalizedSenderId = normalizeUserId(senderId); const normalizedEmail = senderEmail?.trim().toLowerCase() ?? ""; return allowFrom.some((entry) => { const normalized = String(entry).trim().toLowerCase(); - if (!normalized) return false; - if (normalized === normalizedSenderId) return true; - if (normalizedEmail && normalized === normalizedEmail) return true; - if (normalizedEmail && normalized.replace(/^users\//i, "") === normalizedEmail) return true; - if (normalized.replace(/^users\//i, "") === normalizedSenderId) return true; + if (!normalized) { + return false; + } + if (normalized === normalizedSenderId) { + return true; + } + if (normalizedEmail && normalized === normalizedEmail) { + return true; + } + if (normalizedEmail && normalized.replace(/^users\//i, "") === normalizedEmail) { + return true; + } + if (normalized.replace(/^users\//i, "") === normalizedSenderId) { + return true; + } if (normalized.replace(/^(googlechat|google-chat|gchat):/i, "") === normalizedSenderId) { return true; } @@ -357,8 +383,12 @@ function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: strin const botTargets = new Set(["users/app", botUser?.trim()].filter(Boolean) as string[]); const wasMentioned = mentionAnnotations.some((entry) => { const userName = entry.userMention?.user?.name; - if (!userName) return false; - if (botTargets.has(userName)) return true; + if (!userName) { + return false; + } + if (botTargets.has(userName)) { + return true; + } return normalizeUserId(userName) === "app"; }); return { hasAnyMention, wasMentioned }; @@ -376,9 +406,13 @@ function resolveBotDisplayName(params: { config: OpenClawConfig; }): string { const { accountName, agentId, config } = params; - if (accountName?.trim()) return accountName.trim(); + if (accountName?.trim()) { + return accountName.trim(); + } const agent = config.agents?.list?.find((a) => a.id === agentId); - if (agent?.name?.trim()) return agent.name.trim(); + if (agent?.name?.trim()) { + return agent.name.trim(); + } return "OpenClaw"; } @@ -394,10 +428,14 @@ async function processMessageWithPipeline(params: { const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params; const space = event.space; const message = event.message; - if (!space || !message) return; + if (!space || !message) { + return; + } const spaceId = space.name ?? ""; - if (!spaceId) return; + if (!spaceId) { + return; + } const spaceType = (space.type ?? "").toUpperCase(); const isGroup = spaceType !== "DM"; const sender = message.sender ?? event.user; @@ -421,7 +459,9 @@ async function processMessageWithPipeline(params: { const attachments = message.attachment ?? []; const hasMedia = attachments.length > 0; const rawBody = messageText || (hasMedia ? "" : ""); - if (!rawBody) return; + if (!rawBody) { + return; + } const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; @@ -721,7 +761,9 @@ async function downloadAttachment( core: GoogleChatCoreRuntime, ): Promise<{ path: string; contentType?: string } | null> { const resourceName = attachment.attachmentDataRef?.resourceName; - if (!resourceName) return null; + if (!resourceName) { + return null; + } const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024; const downloaded = await downloadGoogleChatMedia({ account, resourceName, maxBytes }); const saved = await core.channel.media.saveMediaBuffer( diff --git a/extensions/googlechat/src/onboarding.ts b/extensions/googlechat/src/onboarding.ts index fa0c20f2c..4e16b0159 100644 --- a/extensions/googlechat/src/onboarding.ts +++ b/extensions/googlechat/src/onboarding.ts @@ -32,9 +32,9 @@ function setGoogleChatDmPolicy(cfg: OpenClawConfig, policy: DmPolicy) { channels: { ...cfg.channels, googlechat: { - ...(cfg.channels?.["googlechat"] ?? {}), + ...cfg.channels?.["googlechat"], dm: { - ...(cfg.channels?.["googlechat"]?.dm ?? {}), + ...cfg.channels?.["googlechat"]?.dm, policy, ...(allowFrom ? { allowFrom } : {}), }, @@ -68,10 +68,10 @@ async function promptAllowFrom(params: { channels: { ...params.cfg.channels, googlechat: { - ...(params.cfg.channels?.["googlechat"] ?? {}), + ...params.cfg.channels?.["googlechat"], enabled: true, dm: { - ...(params.cfg.channels?.["googlechat"]?.dm ?? {}), + ...params.cfg.channels?.["googlechat"]?.dm, policy: "allowlist", allowFrom: unique, }, @@ -102,7 +102,7 @@ function applyAccountConfig(params: { channels: { ...cfg.channels, googlechat: { - ...(cfg.channels?.["googlechat"] ?? {}), + ...cfg.channels?.["googlechat"], enabled: true, ...patch, }, @@ -114,12 +114,12 @@ function applyAccountConfig(params: { channels: { ...cfg.channels, googlechat: { - ...(cfg.channels?.["googlechat"] ?? {}), + ...cfg.channels?.["googlechat"], enabled: true, accounts: { - ...(cfg.channels?.["googlechat"]?.accounts ?? {}), + ...cfg.channels?.["googlechat"]?.accounts, [accountId]: { - ...(cfg.channels?.["googlechat"]?.accounts?.[accountId] ?? {}), + ...cfg.channels?.["googlechat"]?.accounts?.[accountId], enabled: true, ...patch, }, @@ -193,14 +193,14 @@ async function promptAudience(params: { }); const currentType = account.config.audienceType ?? "app-url"; const currentAudience = account.config.audience ?? ""; - const audienceType = (await params.prompter.select({ + const audienceType = await params.prompter.select({ message: "Webhook audience type", options: [ { value: "app-url", label: "App URL (recommended)" }, { value: "project-number", label: "Project number" }, ], initialValue: currentType === "project-number" ? "project-number" : "app-url", - })) as "app-url" | "project-number"; + }); const audience = await params.prompter.text({ message: audienceType === "project-number" ? "Project number" : "App URL", placeholder: audienceType === "project-number" ? "1234567890" : "https://your.host/googlechat", diff --git a/extensions/googlechat/src/targets.ts b/extensions/googlechat/src/targets.ts index a294bf128..f4c5b3051 100644 --- a/extensions/googlechat/src/targets.ts +++ b/extensions/googlechat/src/targets.ts @@ -3,7 +3,9 @@ import { findGoogleChatDirectMessage } from "./api.js"; export function normalizeGoogleChatTarget(raw?: string | null): string | undefined { const trimmed = raw?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const withoutPrefix = trimmed.replace(/^(googlechat|google-chat|gchat):/i, ""); const normalized = withoutPrefix .replace(/^user:(users\/)?/i, "users/") @@ -12,8 +14,12 @@ export function normalizeGoogleChatTarget(raw?: string | null): string | undefin const suffix = normalized.slice("users/".length); return suffix.includes("@") ? `users/${suffix.toLowerCase()}` : normalized; } - if (isGoogleChatSpaceTarget(normalized)) return normalized; - if (normalized.includes("@")) return `users/${normalized.toLowerCase()}`; + if (isGoogleChatSpaceTarget(normalized)) { + return normalized; + } + if (normalized.includes("@")) { + return `users/${normalized.toLowerCase()}`; + } return normalized; } @@ -27,7 +33,9 @@ export function isGoogleChatSpaceTarget(value: string): boolean { function stripMessageSuffix(target: string): string { const index = target.indexOf("/messages/"); - if (index === -1) return target; + if (index === -1) { + return target; + } return target.slice(0, index); } @@ -40,7 +48,9 @@ export async function resolveGoogleChatOutboundSpace(params: { throw new Error("Missing Google Chat target."); } const base = stripMessageSuffix(normalized); - if (isGoogleChatSpaceTarget(base)) return base; + if (isGoogleChatSpaceTarget(base)) { + return base; + } if (isGoogleChatUserTarget(base)) { const dm = await findGoogleChatDirectMessage({ account: params.account, diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index 188a63f35..15a887d93 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -98,7 +98,9 @@ export const imessagePlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ `- iMessage groups: groupPolicy="open" allows any member to trigger the bot. Set channels.imessage.groupPolicy="allowlist" + channels.imessage.groupAllowFrom to restrict senders.`, ]; @@ -227,7 +229,9 @@ export const imessagePlugin: ChannelPlugin = { collectStatusIssues: (accounts) => accounts.flatMap((account) => { const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; - if (!lastError) return []; + if (!lastError) { + return []; + } return [ { channel: "imessage", diff --git a/extensions/line/src/card-command.ts b/extensions/line/src/card-command.ts index 627412c18..ff113b75e 100644 --- a/extensions/line/src/card-command.ts +++ b/extensions/line/src/card-command.ts @@ -38,7 +38,9 @@ function buildLineReply(lineData: LineChannelData): ReplyPayload { * Data can be a URL (uri action) or plain text (message action) or key=value (postback) */ function parseActions(actionsStr: string | undefined): CardAction[] { - if (!actionsStr) return []; + if (!actionsStr) { + return []; + } const results: CardAction[] = []; @@ -47,7 +49,9 @@ function parseActions(actionsStr: string | undefined): CardAction[] { .trim() .split("|") .map((s) => s.trim()); - if (!label) continue; + if (!label) { + continue; + } const actionData = data || label; @@ -158,12 +162,16 @@ export function registerLineCardCommand(api: OpenClawPluginApi): void { requireAuth: false, handler: async (ctx) => { const argsStr = ctx.args?.trim() ?? ""; - if (!argsStr) return { text: CARD_USAGE }; + if (!argsStr) { + return { text: CARD_USAGE }; + } const parsed = parseCardArgs(argsStr); const { type, args, flags } = parsed; - if (!type) return { text: CARD_USAGE }; + if (!type) { + return { text: CARD_USAGE }; + } // Only LINE supports rich cards; fallback to text elsewhere. if (ctx.channel !== "line") { diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 482db61de..a8f31cb4d 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -25,17 +25,6 @@ const meta = { systemImage: "message.fill", }; -function parseThreadId(threadId?: string | number | null): number | undefined { - if (threadId == null) return undefined; - if (typeof threadId === "number") { - return Number.isFinite(threadId) ? Math.trunc(threadId) : undefined; - } - const trimmed = threadId.trim(); - if (!trimmed) return undefined; - const parsed = Number.parseInt(trimmed, 10); - return Number.isFinite(parsed) ? parsed : undefined; -} - export const linePlugin: ChannelPlugin = { id: "line", meta: { @@ -108,7 +97,8 @@ export const linePlugin: ChannelPlugin = { deleteAccount: ({ cfg, accountId }) => { const lineConfig = (cfg.channels?.line ?? {}) as LineConfig; if (accountId === DEFAULT_ACCOUNT_ID) { - const { channelAccessToken, channelSecret, tokenFile, secretFile, ...rest } = lineConfig; + // oxlint-disable-next-line no-unused-vars + const { channelSecret, tokenFile, secretFile, ...rest } = lineConfig; return { ...cfg, channels: { @@ -173,7 +163,9 @@ export const linePlugin: ChannelPlugin = { const defaultGroupPolicy = (cfg.channels?.defaults as { groupPolicy?: string } | undefined) ?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ `- LINE groups: groupPolicy="open" allows any member in groups to trigger. Set channels.line.groupPolicy="allowlist" + channels.line.groupAllowFrom to restrict senders.`, ]; @@ -183,7 +175,9 @@ export const linePlugin: ChannelPlugin = { resolveRequireMention: ({ cfg, accountId, groupId }) => { const account = getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }); const groups = account.config.groups; - if (!groups) return false; + if (!groups) { + return false; + } const groupConfig = groups[groupId] ?? groups["*"]; return groupConfig?.requireMention ?? false; }, @@ -191,13 +185,17 @@ export const linePlugin: ChannelPlugin = { messaging: { normalizeTarget: (target) => { const trimmed = target.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, ""); }, targetResolver: { looksLikeId: (id) => { const trimmed = id?.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } // LINE user IDs are typically U followed by 32 hex characters // Group IDs are C followed by 32 hex characters // Room IDs are R followed by 32 hex characters @@ -356,7 +354,9 @@ export const linePlugin: ChannelPlugin = { : undefined; const sendMessageBatch = async (messages: Array>) => { - if (messages.length === 0) return; + if (messages.length === 0) { + return; + } for (let i = 0; i < messages.length; i += 5) { const result = await sendBatch(to, messages.slice(i, i + 5), { verbose: false, @@ -434,12 +434,12 @@ export const linePlugin: ChannelPlugin = { for (let i = 0; i < chunks.length; i += 1) { const isLast = i === chunks.length - 1; if (isLast && hasQuickReplies) { - lastResult = await sendQuickReplies(to, chunks[i]!, lineData.quickReplies!, { + lastResult = await sendQuickReplies(to, chunks[i], lineData.quickReplies!, { verbose: false, accountId: accountId ?? undefined, }); } else { - lastResult = await sendText(to, chunks[i]!, { + lastResult = await sendText(to, chunks[i], { verbose: false, accountId: accountId ?? undefined, }); @@ -478,7 +478,9 @@ export const linePlugin: ChannelPlugin = { } for (const url of mediaUrls) { const trimmed = url?.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } quickReplyMessages.push({ type: "image", originalContentUrl: trimmed, @@ -505,7 +507,9 @@ export const linePlugin: ChannelPlugin = { } } - if (lastResult) return { channel: "line", ...lastResult }; + if (lastResult) { + return { channel: "line", ...lastResult }; + } return { channel: "line", messageId: "empty", chatId: to }; }, sendText: async ({ to, text, accountId }) => { @@ -621,7 +625,9 @@ export const linePlugin: ChannelPlugin = { try { const probe = await getLineRuntime().channel.line.probeLineBot(token, 2500); const displayName = probe.ok ? probe.bot?.displayName?.trim() : null; - if (displayName) lineBotLabel = ` (${displayName})`; + if (displayName) { + lineBotLabel = ` (${displayName})`; + } } catch (err) { if (getLineRuntime().logging.shouldLogVerbose()) { ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`); diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 4cb246138..eb4cf89de 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -36,7 +36,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: JSON.stringify({ foo: "bar" }) }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); const res = await tool.execute("id", { prompt: "return foo" }); expect((res as any).details.json).toEqual({ foo: "bar" }); }); @@ -46,7 +46,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: '```json\n{"ok":true}\n```' }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); const res = await tool.execute("id", { prompt: "return ok" }); expect((res as any).details.json).toEqual({ ok: true }); }); @@ -56,7 +56,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: JSON.stringify({ foo: "bar" }) }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); const schema = { type: "object", properties: { foo: { type: "string" } }, @@ -72,7 +72,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: "not-json" }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); await expect(tool.execute("id", { prompt: "x" })).rejects.toThrow(/invalid json/i); }); @@ -81,7 +81,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: JSON.stringify({ foo: 1 }) }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); const schema = { type: "object", properties: { foo: { type: "string" } }, required: ["foo"] }; await expect(tool.execute("id", { prompt: "x", schema })).rejects.toThrow(/match schema/i); }); @@ -91,7 +91,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: JSON.stringify({ ok: true }) }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); await tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" }); const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0]; expect(call.provider).toBe("anthropic"); @@ -104,7 +104,7 @@ describe("llm-task tool (json-only)", () => { payloads: [{ text: JSON.stringify({ ok: true }) }], }); const tool = createLlmTaskTool( - fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }) as any, + fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }), ); await expect( tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" }), @@ -116,7 +116,7 @@ describe("llm-task tool (json-only)", () => { meta: {}, payloads: [{ text: JSON.stringify({ ok: true }) }], }); - const tool = createLlmTaskTool(fakeApi() as any); + const tool = createLlmTaskTool(fakeApi()); await tool.execute("id", { prompt: "x" }); const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0]; expect(call.disableTools).toBe(true); diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 2309139d7..370faec8c 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -18,24 +18,27 @@ async function loadRunEmbeddedPiAgent(): Promise { // Source checkout (tests/dev) try { const mod = await import("../../../src/agents/pi-embedded-runner.js"); - if (typeof (mod as any).runEmbeddedPiAgent === "function") + if (typeof (mod as any).runEmbeddedPiAgent === "function") { return (mod as any).runEmbeddedPiAgent; + } } catch { // ignore } // Bundled install (built) const mod = await import("../../../agents/pi-embedded-runner.js"); - if (typeof (mod as any).runEmbeddedPiAgent !== "function") { + if (typeof mod.runEmbeddedPiAgent !== "function") { throw new Error("Internal error: runEmbeddedPiAgent not available"); } - return (mod as any).runEmbeddedPiAgent; + return mod.runEmbeddedPiAgent; } function stripCodeFences(s: string): string { const trimmed = s.trim(); const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); - if (m) return (m[1] ?? "").trim(); + if (m) { + return (m[1] ?? "").trim(); + } return trimmed; } @@ -49,7 +52,9 @@ function collectText(payloads: Array<{ text?: string; isError?: boolean }> | und function toModelKey(provider?: string, model?: string): string | undefined { const p = provider?.trim(); const m = model?.trim(); - if (!p || !m) return undefined; + if (!p || !m) { + return undefined; + } return `${p}/${m}`; } @@ -84,8 +89,10 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { }), async execute(_id: string, params: Record) { - const prompt = String(params.prompt ?? ""); - if (!prompt.trim()) throw new Error("prompt required"); + const prompt = typeof params.prompt === "string" ? params.prompt : ""; + if (!prompt.trim()) { + throw new Error("prompt required"); + } const pluginCfg = (api.pluginConfig ?? {}) as PluginCfg; @@ -189,7 +196,9 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { }); const text = collectText((result as any).payloads); - if (!text) throw new Error("LLM returned empty output"); + if (!text) { + throw new Error("LLM returned empty output"); + } const raw = stripCodeFences(text); let parsed: unknown; diff --git a/extensions/lobster/index.ts b/extensions/lobster/index.ts index 85cbb0df9..c2864c733 100644 --- a/extensions/lobster/index.ts +++ b/extensions/lobster/index.ts @@ -5,7 +5,9 @@ import { createLobsterTool } from "./src/lobster-tool.js"; export default function register(api: OpenClawPluginApi) { api.registerTool( (ctx) => { - if (ctx.sandboxed) return null; + if (ctx.sandboxed) { + return null; + } return createLobsterTool(api); }, { optional: true }, diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index 78a7bbf5e..26fa37c45 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -133,7 +133,9 @@ describe("lobster plugin tool", () => { it("can be gated off in sandboxed contexts", async () => { const api = fakeApi(); const factoryTool = (ctx: OpenClawPluginToolContext) => { - if (ctx.sandboxed) return null; + if (ctx.sandboxed) { + return null; + } return createLobsterTool(api); }; diff --git a/extensions/lobster/src/lobster-tool.ts b/extensions/lobster/src/lobster-tool.ts index 34d81624d..5b8998fee 100644 --- a/extensions/lobster/src/lobster-tool.ts +++ b/extensions/lobster/src/lobster-tool.ts @@ -30,7 +30,9 @@ function resolveExecutablePath(lobsterPathRaw: string | undefined) { } function isWindowsSpawnEINVAL(err: unknown) { - if (!err || typeof err !== "object") return false; + if (!err || typeof err !== "object") { + return false; + } const code = (err as { code?: unknown }).code; return code === "EINVAL"; } @@ -186,8 +188,10 @@ export function createLobsterTool(api: OpenClawPluginApi) { maxStdoutBytes: Type.Optional(Type.Number()), }), async execute(_id: string, params: Record) { - const action = String(params.action || "").trim(); - if (!action) throw new Error("action required"); + const action = typeof params.action === "string" ? params.action.trim() : ""; + if (!action) { + throw new Error("action required"); + } const execPath = resolveExecutablePath( typeof params.lobsterPath === "string" ? params.lobsterPath : undefined, @@ -201,7 +205,9 @@ export function createLobsterTool(api: OpenClawPluginApi) { const argv = (() => { if (action === "run") { const pipeline = typeof params.pipeline === "string" ? params.pipeline : ""; - if (!pipeline.trim()) throw new Error("pipeline required"); + if (!pipeline.trim()) { + throw new Error("pipeline required"); + } const argv = ["run", "--mode", "tool", pipeline]; const argsJson = typeof params.argsJson === "string" ? params.argsJson : ""; if (argsJson.trim()) { @@ -211,9 +217,13 @@ export function createLobsterTool(api: OpenClawPluginApi) { } if (action === "resume") { const token = typeof params.token === "string" ? params.token : ""; - if (!token.trim()) throw new Error("token required"); + if (!token.trim()) { + throw new Error("token required"); + } const approve = params.approve; - if (typeof approve !== "boolean") throw new Error("approve required"); + if (typeof approve !== "boolean") { + throw new Error("approve required"); + } return ["resume", "--token", token, "--approve", approve ? "yes" : "no"]; } throw new Error(`Unknown action: ${action}`); diff --git a/extensions/matrix/src/actions.ts b/extensions/matrix/src/actions.ts index 113680a3b..2af7e951b 100644 --- a/extensions/matrix/src/actions.ts +++ b/extensions/matrix/src/actions.ts @@ -14,7 +14,9 @@ import type { CoreConfig } from "./types.js"; export const matrixMessageActions: ChannelMessageActionAdapter = { listActions: ({ cfg }) => { const account = resolveMatrixAccount({ cfg: cfg as CoreConfig }); - if (!account.enabled || !account.configured) return []; + if (!account.enabled || !account.configured) { + return []; + } const gate = createActionGate((cfg as CoreConfig).channels?.matrix?.actions); const actions = new Set(["send", "poll"]); if (gate("reactions")) { @@ -31,16 +33,24 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { actions.add("unpin"); actions.add("list-pins"); } - if (gate("memberInfo")) actions.add("member-info"); - if (gate("channelInfo")) actions.add("channel-info"); + if (gate("memberInfo")) { + actions.add("member-info"); + } + if (gate("channelInfo")) { + actions.add("channel-info"); + } return Array.from(actions); }, supportsAction: ({ action }) => action !== "poll", extractToolSend: ({ args }): ChannelToolSend | null => { const action = typeof args.action === "string" ? args.action.trim() : ""; - if (action !== "sendMessage") return null; + if (action !== "sendMessage") { + return null; + } const to = typeof args.to === "string" ? args.to : undefined; - if (!to) return null; + if (!to) { + return null; + } return { to }; }, handleAction: async (ctx: ChannelMessageActionContext) => { diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index ac280b20b..a0dd39373 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -45,7 +45,9 @@ const meta = { function normalizeMatrixMessagingTarget(raw: string): string | undefined { let normalized = raw.trim(); - if (!normalized) return undefined; + if (!normalized) { + return undefined; + } const lowered = normalized.toLowerCase(); if (lowered.startsWith("matrix:")) { normalized = normalized.slice("matrix:".length).trim(); @@ -161,7 +163,9 @@ export const matrixPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ '- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy="allowlist" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.', ]; @@ -188,8 +192,12 @@ export const matrixPlugin: ChannelPlugin = { targetResolver: { looksLikeId: (raw) => { const trimmed = raw.trim(); - if (!trimmed) return false; - if (/^(matrix:)?[!#@]/i.test(trimmed)) return true; + if (!trimmed) { + return false; + } + if (/^(matrix:)?[!#@]/i.test(trimmed)) { + return true; + } return trimmed.includes(":"); }, hint: "", @@ -204,13 +212,17 @@ export const matrixPlugin: ChannelPlugin = { for (const entry of account.config.dm?.allowFrom ?? []) { const raw = String(entry).trim(); - if (!raw || raw === "*") continue; + if (!raw || raw === "*") { + continue; + } ids.add(raw.replace(/^matrix:/i, "")); } for (const entry of account.config.groupAllowFrom ?? []) { const raw = String(entry).trim(); - if (!raw || raw === "*") continue; + if (!raw || raw === "*") { + continue; + } ids.add(raw.replace(/^matrix:/i, "")); } @@ -218,7 +230,9 @@ export const matrixPlugin: ChannelPlugin = { for (const room of Object.values(groups)) { for (const entry of room.users ?? []) { const raw = String(entry).trim(); - if (!raw || raw === "*") continue; + if (!raw || raw === "*") { + continue; + } ids.add(raw.replace(/^matrix:/i, "")); } } @@ -229,7 +243,9 @@ export const matrixPlugin: ChannelPlugin = { .map((raw) => { const lowered = raw.toLowerCase(); const cleaned = lowered.startsWith("user:") ? raw.slice("user:".length).trim() : raw; - if (cleaned.startsWith("@")) return `user:${cleaned}`; + if (cleaned.startsWith("@")) { + return `user:${cleaned}`; + } return cleaned; }) .filter((id) => (q ? id.toLowerCase().includes(q) : true)) @@ -254,8 +270,12 @@ export const matrixPlugin: ChannelPlugin = { .map((raw) => raw.replace(/^matrix:/i, "")) .map((raw) => { const lowered = raw.toLowerCase(); - if (lowered.startsWith("room:") || lowered.startsWith("channel:")) return raw; - if (raw.startsWith("!")) return `room:${raw}`; + if (lowered.startsWith("room:") || lowered.startsWith("channel:")) { + return raw; + } + if (raw.startsWith("!")) { + return `room:${raw}`; + } return raw; }) .filter((id) => (q ? id.toLowerCase().includes(q) : true)) @@ -283,8 +303,12 @@ export const matrixPlugin: ChannelPlugin = { name, }), validateInput: ({ input }) => { - if (input.useEnv) return null; - if (!input.homeserver?.trim()) return "Matrix requires --homeserver"; + if (input.useEnv) { + return null; + } + if (!input.homeserver?.trim()) { + return "Matrix requires --homeserver"; + } const accessToken = input.accessToken?.trim(); const password = input.password?.trim(); const userId = input.userId?.trim(); @@ -292,8 +316,12 @@ export const matrixPlugin: ChannelPlugin = { return "Matrix requires --access-token or --password"; } if (!accessToken) { - if (!userId) return "Matrix requires --user-id when using --password"; - if (!password) return "Matrix requires --password when using --user-id"; + if (!userId) { + return "Matrix requires --user-id when using --password"; + } + if (!password) { + return "Matrix requires --password when using --user-id"; + } } return null; }, @@ -338,7 +366,9 @@ export const matrixPlugin: ChannelPlugin = { collectStatusIssues: (accounts) => accounts.flatMap((account) => { const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; - if (!lastError) return []; + if (!lastError) { + return []; + } return [ { channel: "matrix", @@ -358,7 +388,7 @@ export const matrixPlugin: ChannelPlugin = { probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }), - probeAccount: async ({ account, timeoutMs, cfg }) => { + probeAccount: async ({ timeoutMs, cfg }) => { try { const auth = await resolveMatrixAuth({ cfg: cfg as CoreConfig }); return await probeMatrix({ diff --git a/extensions/matrix/src/directory-live.ts b/extensions/matrix/src/directory-live.ts index 67fc5e563..6870079ed 100644 --- a/extensions/matrix/src/directory-live.ts +++ b/extensions/matrix/src/directory-live.ts @@ -55,7 +55,9 @@ export async function listMatrixDirectoryPeersLive(params: { limit?: number | null; }): Promise { const query = normalizeQuery(params.query); - if (!query) return []; + if (!query) { + return []; + } const auth = await resolveMatrixAuth({ cfg: params.cfg as never }); const res = await fetchMatrixJson({ homeserver: auth.homeserver, @@ -71,7 +73,9 @@ export async function listMatrixDirectoryPeersLive(params: { return results .map((entry) => { const userId = entry.user_id?.trim(); - if (!userId) return null; + if (!userId) { + return null; + } return { kind: "user", id: userId, @@ -123,13 +127,17 @@ export async function listMatrixDirectoryGroupsLive(params: { limit?: number | null; }): Promise { const query = normalizeQuery(params.query); - if (!query) return []; + if (!query) { + return []; + } const auth = await resolveMatrixAuth({ cfg: params.cfg as never }); const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20; if (query.startsWith("#")) { const roomId = await resolveMatrixRoomAlias(auth.homeserver, auth.accessToken, query); - if (!roomId) return []; + if (!roomId) { + return []; + } return [ { kind: "group", @@ -160,15 +168,21 @@ export async function listMatrixDirectoryGroupsLive(params: { for (const roomId of rooms) { const name = await fetchMatrixRoomName(auth.homeserver, auth.accessToken, roomId); - if (!name) continue; - if (!name.toLowerCase().includes(query)) continue; + if (!name) { + continue; + } + if (!name.toLowerCase().includes(query)) { + continue; + } results.push({ kind: "group", id: roomId, name, handle: `#${name}`, }); - if (results.length >= limit) break; + if (results.length >= limit) { + break; + } } return results; diff --git a/extensions/matrix/src/group-mentions.ts b/extensions/matrix/src/group-mentions.ts index 05d63ce2d..d75ac529c 100644 --- a/extensions/matrix/src/group-mentions.ts +++ b/extensions/matrix/src/group-mentions.ts @@ -26,9 +26,15 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b name: groupChannel || undefined, }).config; if (resolved) { - if (resolved.autoReply === true) return false; - if (resolved.autoReply === false) return true; - if (typeof resolved.requireMention === "boolean") return resolved.requireMention; + if (resolved.autoReply === true) { + return false; + } + if (resolved.autoReply === false) { + return true; + } + if (typeof resolved.requireMention === "boolean") { + return resolved.requireMention; + } } return true; } diff --git a/extensions/matrix/src/matrix/accounts.ts b/extensions/matrix/src/matrix/accounts.ts index 802564241..99593b8a3 100644 --- a/extensions/matrix/src/matrix/accounts.ts +++ b/extensions/matrix/src/matrix/accounts.ts @@ -19,7 +19,9 @@ export function listMatrixAccountIds(_cfg: CoreConfig): string[] { export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string { const ids = listMatrixAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -28,7 +30,7 @@ export function resolveMatrixAccount(params: { accountId?: string | null; }): ResolvedMatrixAccount { const accountId = normalizeAccountId(params.accountId); - const base = (params.cfg.channels?.matrix ?? {}) as MatrixConfig; + const base = params.cfg.channels?.matrix ?? {}; const enabled = base.enabled !== false; const resolved = resolveMatrixConfig(params.cfg, process.env); const hasHomeserver = Boolean(resolved.homeserver); diff --git a/extensions/matrix/src/matrix/actions/client.ts b/extensions/matrix/src/matrix/actions/client.ts index efbd6d62b..514fb49ca 100644 --- a/extensions/matrix/src/matrix/actions/client.ts +++ b/extensions/matrix/src/matrix/actions/client.ts @@ -19,9 +19,13 @@ export async function resolveActionClient( opts: MatrixActionClientOpts = {}, ): Promise { ensureNodeRuntime(); - if (opts.client) return { client: opts.client, stopOnDone: false }; + if (opts.client) { + return { client: opts.client, stopOnDone: false }; + } const active = getActiveMatrixClient(); - if (active) return { client: active, stopOnDone: false }; + if (active) { + return { client: active, stopOnDone: false }; + } const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT); if (shouldShareClient) { const client = await resolveSharedMatrixClient({ diff --git a/extensions/matrix/src/matrix/actions/messages.ts b/extensions/matrix/src/matrix/actions/messages.ts index e8032eb8d..736c3ed53 100644 --- a/extensions/matrix/src/matrix/actions/messages.ts +++ b/extensions/matrix/src/matrix/actions/messages.ts @@ -36,7 +36,9 @@ export async function editMatrixMessage( opts: MatrixActionClientOpts = {}, ) { const trimmed = content.trim(); - if (!trimmed) throw new Error("Matrix edit requires content"); + if (!trimmed) { + throw new Error("Matrix edit requires content"); + } const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); @@ -56,7 +58,9 @@ export async function editMatrixMessage( const eventId = await client.sendMessage(resolvedRoom, payload); return { eventId: eventId ?? null }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } @@ -70,7 +74,9 @@ export async function deleteMatrixMessage( const resolvedRoom = await resolveMatrixRoomId(client, roomId); await client.redactEvent(resolvedRoom, messageId, opts.reason); } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } @@ -115,6 +121,8 @@ export async function readMatrixMessages( prevBatch: res.start ?? null, }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } diff --git a/extensions/matrix/src/matrix/actions/pins.ts b/extensions/matrix/src/matrix/actions/pins.ts index a29dfba45..3dbff7373 100644 --- a/extensions/matrix/src/matrix/actions/pins.ts +++ b/extensions/matrix/src/matrix/actions/pins.ts @@ -22,7 +22,9 @@ export async function pinMatrixMessage( await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); return { pinned: next }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } @@ -40,7 +42,9 @@ export async function unpinMatrixMessage( await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); return { pinned: next }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } @@ -65,6 +69,8 @@ export async function listMatrixPins( ).filter((event): event is MatrixMessageSummary => Boolean(event)); return { pinned, events }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } diff --git a/extensions/matrix/src/matrix/actions/reactions.ts b/extensions/matrix/src/matrix/actions/reactions.ts index 09ec2a4e9..9df5f4539 100644 --- a/extensions/matrix/src/matrix/actions/reactions.ts +++ b/extensions/matrix/src/matrix/actions/reactions.ts @@ -31,7 +31,9 @@ export async function listMatrixReactions( for (const event of res.chunk) { const content = event.content as ReactionEventContent; const key = content["m.relates_to"]?.key; - if (!key) continue; + if (!key) { + continue; + } const sender = event.sender ?? ""; const entry: MatrixReactionSummary = summaries.get(key) ?? { key, @@ -46,7 +48,9 @@ export async function listMatrixReactions( } return Array.from(summaries.values()); } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } @@ -64,21 +68,29 @@ export async function removeMatrixReactions( { dir: "b", limit: 200 }, )) as { chunk: MatrixRawEvent[] }; const userId = await client.getUserId(); - if (!userId) return { removed: 0 }; + if (!userId) { + return { removed: 0 }; + } const targetEmoji = opts.emoji?.trim(); const toRemove = res.chunk .filter((event) => event.sender === userId) .filter((event) => { - if (!targetEmoji) return true; + if (!targetEmoji) { + return true; + } const content = event.content as ReactionEventContent; return content["m.relates_to"]?.key === targetEmoji; }) .map((event) => event.event_id) .filter((id): id is string => Boolean(id)); - if (toRemove.length === 0) return { removed: 0 }; + if (toRemove.length === 0) { + return { removed: 0 }; + } await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id))); return { removed: toRemove.length }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } diff --git a/extensions/matrix/src/matrix/actions/room.ts b/extensions/matrix/src/matrix/actions/room.ts index d93509eac..a16dff619 100644 --- a/extensions/matrix/src/matrix/actions/room.ts +++ b/extensions/matrix/src/matrix/actions/room.ts @@ -25,7 +25,9 @@ export async function getMatrixMemberInfo( roomId: roomId ?? null, }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } @@ -76,6 +78,8 @@ export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClient memberCount, }; } finally { - if (stopOnDone) client.stop(); + if (stopOnDone) { + client.stop(); + } } } diff --git a/extensions/matrix/src/matrix/actions/summary.ts b/extensions/matrix/src/matrix/actions/summary.ts index 7191c2901..b01168b17 100644 --- a/extensions/matrix/src/matrix/actions/summary.ts +++ b/extensions/matrix/src/matrix/actions/summary.ts @@ -65,7 +65,9 @@ export async function fetchEventSummary( ): Promise { try { const raw = (await client.getEvent(roomId, eventId)) as MatrixRawEvent; - if (raw.unsigned?.redacted_because) return null; + if (raw.unsigned?.redacted_because) { + return null; + } return summarizeMatrixRawEvent(raw); } catch { // Event not found, redacted, or inaccessible - return null diff --git a/extensions/matrix/src/matrix/client/create-client.ts b/extensions/matrix/src/matrix/client/create-client.ts index de25ec859..20e532f21 100644 --- a/extensions/matrix/src/matrix/client/create-client.ts +++ b/extensions/matrix/src/matrix/client/create-client.ts @@ -16,7 +16,9 @@ import { } from "./storage.js"; function sanitizeUserIdList(input: unknown, label: string): string[] { - if (input == null) return []; + if (input == null) { + return []; + } if (!Array.isArray(input)) { LogService.warn( "MatrixClientLite", diff --git a/extensions/matrix/src/matrix/client/logging.ts b/extensions/matrix/src/matrix/client/logging.ts index 375c46090..c5ef702b0 100644 --- a/extensions/matrix/src/matrix/client/logging.ts +++ b/extensions/matrix/src/matrix/client/logging.ts @@ -4,15 +4,21 @@ let matrixSdkLoggingConfigured = false; const matrixSdkBaseLogger = new ConsoleLogger(); function shouldSuppressMatrixHttpNotFound(module: string, messageOrObject: unknown[]): boolean { - if (module !== "MatrixHttpClient") return false; + if (module !== "MatrixHttpClient") { + return false; + } return messageOrObject.some((entry) => { - if (!entry || typeof entry !== "object") return false; + if (!entry || typeof entry !== "object") { + return false; + } return (entry as { errcode?: string }).errcode === "M_NOT_FOUND"; }); } export function ensureMatrixSdkLoggingConfigured(): void { - if (matrixSdkLoggingConfigured) return; + if (matrixSdkLoggingConfigured) { + return; + } matrixSdkLoggingConfigured = true; LogService.setLogger({ @@ -21,7 +27,9 @@ export function ensureMatrixSdkLoggingConfigured(): void { info: (module, ...messageOrObject) => matrixSdkBaseLogger.info(module, ...messageOrObject), warn: (module, ...messageOrObject) => matrixSdkBaseLogger.warn(module, ...messageOrObject), error: (module, ...messageOrObject) => { - if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) return; + if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) { + return; + } matrixSdkBaseLogger.error(module, ...messageOrObject); }, }); diff --git a/extensions/matrix/src/matrix/client/shared.ts b/extensions/matrix/src/matrix/client/shared.ts index da10fc360..aa56e7150 100644 --- a/extensions/matrix/src/matrix/client/shared.ts +++ b/extensions/matrix/src/matrix/client/shared.ts @@ -55,7 +55,9 @@ async function ensureSharedClientStarted(params: { initialSyncLimit?: number; encryption?: boolean; }): Promise { - if (params.state.started) return; + if (params.state.started) { + return; + } if (sharedClientStartPromise) { await sharedClientStartPromise; return; diff --git a/extensions/matrix/src/matrix/client/storage.ts b/extensions/matrix/src/matrix/client/storage.ts index 7125f8ea0..fbc069e0e 100644 --- a/extensions/matrix/src/matrix/client/storage.ts +++ b/extensions/matrix/src/matrix/client/storage.ts @@ -21,7 +21,9 @@ function sanitizePathSegment(value: string): string { function resolveHomeserverKey(homeserver: string): string { try { const url = new URL(homeserver); - if (url.host) return sanitizePathSegment(url.host); + if (url.host) { + return sanitizePathSegment(url.host); + } } catch { // fall through } @@ -84,8 +86,12 @@ export function maybeMigrateLegacyStorage(params: { const hasNewStorage = fs.existsSync(params.storagePaths.storagePath) || fs.existsSync(params.storagePaths.cryptoPath); - if (!hasLegacyStorage && !hasLegacyCrypto) return; - if (hasNewStorage) return; + if (!hasLegacyStorage && !hasLegacyCrypto) { + return; + } + if (hasNewStorage) { + return; + } fs.mkdirSync(params.storagePaths.rootDir, { recursive: true }); if (hasLegacyStorage) { diff --git a/extensions/matrix/src/matrix/credentials.ts b/extensions/matrix/src/matrix/credentials.ts index 9ce4c842b..faebc8fda 100644 --- a/extensions/matrix/src/matrix/credentials.ts +++ b/extensions/matrix/src/matrix/credentials.ts @@ -33,7 +33,9 @@ export function loadMatrixCredentials( ): MatrixStoredCredentials | null { const credPath = resolveMatrixCredentialsPath(env); try { - if (!fs.existsSync(credPath)) return null; + if (!fs.existsSync(credPath)) { + return null; + } const raw = fs.readFileSync(credPath, "utf-8"); const parsed = JSON.parse(raw) as Partial; if ( @@ -72,7 +74,9 @@ export function saveMatrixCredentials( export function touchMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void { const existing = loadMatrixCredentials(env); - if (!existing) return; + if (!existing) { + return; + } existing.lastUsedAt = new Date().toISOString(); const credPath = resolveMatrixCredentialsPath(env); diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index 9f4bd9873..d838f4d4d 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -27,7 +27,9 @@ export async function ensureMatrixSdkInstalled(params: { runtime: RuntimeEnv; confirm?: (message: string) => Promise; }): Promise { - if (isMatrixSdkAvailable()) return; + if (isMatrixSdkAvailable()) { + return; + } const confirm = params.confirm; if (confirm) { const ok = await confirm("Matrix requires @vector-im/matrix-bot-sdk. Install now?"); diff --git a/extensions/matrix/src/matrix/monitor/allowlist.ts b/extensions/matrix/src/matrix/monitor/allowlist.ts index 373b48000..b110dc9ef 100644 --- a/extensions/matrix/src/matrix/monitor/allowlist.ts +++ b/extensions/matrix/src/matrix/monitor/allowlist.ts @@ -22,7 +22,9 @@ export function resolveMatrixAllowListMatch(params: { userName?: string; }): MatrixAllowListMatch { const allowList = params.allowList; - if (allowList.length === 0) return { allowed: false }; + if (allowList.length === 0) { + return { allowed: false }; + } if (allowList.includes("*")) { return { allowed: true, matchKey: "*", matchSource: "wildcard" }; } @@ -37,7 +39,9 @@ export function resolveMatrixAllowListMatch(params: { { value: localPart, source: "localpart" }, ]; for (const candidate of candidates) { - if (!candidate.value) continue; + if (!candidate.value) { + continue; + } if (allowList.includes(candidate.value)) { return { allowed: true, diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index 77d15296e..f49405037 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -13,7 +13,9 @@ export function registerMatrixAutoJoin(params: { const { client, cfg, runtime } = params; const core = getMatrixRuntime(); const logVerbose = (message: string) => { - if (!core.logging.shouldLogVerbose()) return; + if (!core.logging.shouldLogVerbose()) { + return; + } runtime.log?.(message); }; const autoJoin = cfg.channels?.matrix?.autoJoin ?? "always"; @@ -32,7 +34,9 @@ export function registerMatrixAutoJoin(params: { // For "allowlist" mode, handle invites manually client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => { - if (autoJoin !== "allowlist") return; + if (autoJoin !== "allowlist") { + return; + } // Get room alias if available let alias: string | undefined; diff --git a/extensions/matrix/src/matrix/monitor/direct.ts b/extensions/matrix/src/matrix/monitor/direct.ts index 8afc8893d..5cd6e8875 100644 --- a/extensions/matrix/src/matrix/monitor/direct.ts +++ b/extensions/matrix/src/matrix/monitor/direct.ts @@ -19,7 +19,9 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr const memberCountCache = new Map(); const ensureSelfUserId = async (): Promise => { - if (cachedSelfUserId) return cachedSelfUserId; + if (cachedSelfUserId) { + return cachedSelfUserId; + } try { cachedSelfUserId = await client.getUserId(); } catch { @@ -30,7 +32,9 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr const refreshDmCache = async (): Promise => { const now = Date.now(); - if (now - lastDmUpdateMs < DM_CACHE_TTL_MS) return; + if (now - lastDmUpdateMs < DM_CACHE_TTL_MS) { + return; + } lastDmUpdateMs = now; try { await client.dms.update(); @@ -58,7 +62,9 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr const hasDirectFlag = async (roomId: string, userId?: string): Promise => { const target = userId?.trim(); - if (!target) return false; + if (!target) { + return false; + } try { const state = await client.getRoomStateEvent(roomId, "m.room.member", target); return state?.is_direct === true; diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 4eb427f67..27cb5e3a5 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -126,15 +126,23 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const isLocationEvent = eventType === EventType.Location || (eventType === EventType.RoomMessage && locationContent.msgtype === EventType.Location); - if (eventType !== EventType.RoomMessage && !isPollEvent && !isLocationEvent) return; + if (eventType !== EventType.RoomMessage && !isPollEvent && !isLocationEvent) { + return; + } logVerboseMessage( `matrix: room.message recv room=${roomId} type=${eventType} id=${event.event_id ?? "unknown"}`, ); - if (event.unsigned?.redacted_because) return; + if (event.unsigned?.redacted_because) { + return; + } const senderId = event.sender; - if (!senderId) return; + if (!senderId) { + return; + } const selfUserId = await client.getUserId(); - if (senderId === selfUserId) return; + if (senderId === selfUserId) { + return; + } const eventTs = event.origin_server_ts; const eventAge = event.unsigned?.age; if (typeof eventTs === "number" && eventTs < startupMs - startupGraceMs) { @@ -179,7 +187,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const relates = content["m.relates_to"]; if (relates && "rel_type" in relates) { - if (relates.rel_type === RelationType.Replace) return; + if (relates.rel_type === RelationType.Replace) { + return; + } } const isDirectMessage = await directTracker.isDirectMessage({ @@ -189,7 +199,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam }); const isRoom = !isDirectMessage; - if (isRoom && groupPolicy === "disabled") return; + if (isRoom && groupPolicy === "disabled") { + return; + } const roomConfigInfo = isRoom ? resolveMatrixRoomConfig({ @@ -234,7 +246,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const groupAllowConfigured = effectiveGroupAllowFrom.length > 0; if (isDirectMessage) { - if (!dmEnabled || dmPolicy === "disabled") return; + if (!dmEnabled || dmPolicy === "disabled") { + return; + } if (dmPolicy !== "open") { const allowMatch = resolveMatrixAllowListMatch({ allowList: effectiveAllowFrom, @@ -356,7 +370,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam } const bodyText = rawBody || media?.placeholder || ""; - if (!bodyText) return; + if (!bodyText) { + return; + } const { wasMentioned, hasExplicitMention } = resolveMentions({ content, @@ -497,7 +513,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam MediaPath: media?.path, MediaType: media?.contentType, MediaUrl: media?.path, - ...(locationPayload?.context ?? {}), + ...locationPayload?.context, CommandAuthorized: commandAuthorized, CommandSource: "text" as const, OriginatingChannel: "matrix" as const, @@ -633,7 +649,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam }, }); markDispatchIdle(); - if (!queuedFinal) return; + if (!queuedFinal) { + return; + } didSendReply = true; const finalCount = counts.final; logVerboseMessage( diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index c7260ff8f..857206457 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -34,7 +34,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } const core = getMatrixRuntime(); let cfg = core.config.loadConfig() as CoreConfig; - if (cfg.channels?.matrix?.enabled === false) return; + if (cfg.channels?.matrix?.enabled === false) { + return; + } const logger = core.logging.getChildLogger({ module: "matrix-auto-reply" }); const formatRuntimeMessage = (...args: Parameters) => format(...args); @@ -50,7 +52,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi }, }; const logVerboseMessage = (message: string) => { - if (!core.logging.shouldLogVerbose()) return; + if (!core.logging.shouldLogVerbose()) { + return; + } logger.debug(message); }; @@ -115,7 +119,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const pending: Array<{ input: string; query: string }> = []; for (const entry of entries) { const trimmed = entry.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } const cleaned = normalizeRoomEntry(trimmed); if (cleaned.startsWith("!") && cleaned.includes(":")) { if (!nextRooms[cleaned]) { @@ -135,7 +141,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi }); resolved.forEach((entry, index) => { const source = pending[index]; - if (!source) return; + if (!source) { + return; + } if (entry.resolved && entry.id) { if (!nextRooms[entry.id]) { nextRooms[entry.id] = roomsConfig[source.input]; diff --git a/extensions/matrix/src/matrix/monitor/location.ts b/extensions/matrix/src/matrix/monitor/location.ts index 8d7aecc13..319490a4f 100644 --- a/extensions/matrix/src/matrix/monitor/location.ts +++ b/extensions/matrix/src/matrix/monitor/location.ts @@ -20,25 +20,37 @@ type GeoUriParams = { function parseGeoUri(value: string): GeoUriParams | null { const trimmed = value.trim(); - if (!trimmed) return null; - if (!trimmed.toLowerCase().startsWith("geo:")) return null; + if (!trimmed) { + return null; + } + if (!trimmed.toLowerCase().startsWith("geo:")) { + return null; + } const payload = trimmed.slice(4); const [coordsPart, ...paramParts] = payload.split(";"); const coords = coordsPart.split(","); - if (coords.length < 2) return null; + if (coords.length < 2) { + return null; + } const latitude = Number.parseFloat(coords[0] ?? ""); const longitude = Number.parseFloat(coords[1] ?? ""); - if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null; + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { + return null; + } const params = new Map(); for (const part of paramParts) { const segment = part.trim(); - if (!segment) continue; + if (!segment) { + continue; + } const eqIndex = segment.indexOf("="); const rawKey = eqIndex === -1 ? segment : segment.slice(0, eqIndex); const rawValue = eqIndex === -1 ? "" : segment.slice(eqIndex + 1); const key = rawKey.trim().toLowerCase(); - if (!key) continue; + if (!key) { + continue; + } const valuePart = rawValue.trim(); params.set(key, valuePart ? decodeURIComponent(valuePart) : ""); } @@ -61,11 +73,17 @@ export function resolveMatrixLocation(params: { const isLocation = eventType === EventType.Location || (eventType === EventType.RoomMessage && content.msgtype === EventType.Location); - if (!isLocation) return null; + if (!isLocation) { + return null; + } const geoUri = typeof content.geo_uri === "string" ? content.geo_uri.trim() : ""; - if (!geoUri) return null; + if (!geoUri) { + return null; + } const parsed = parseGeoUri(geoUri); - if (!parsed) return null; + if (!parsed) { + return null; + } const caption = typeof content.body === "string" ? content.body.trim() : ""; const location: NormalizedLocation = { latitude: parsed.latitude, diff --git a/extensions/matrix/src/matrix/monitor/media.ts b/extensions/matrix/src/matrix/monitor/media.ts index 99c006f71..631e3813a 100644 --- a/extensions/matrix/src/matrix/monitor/media.ts +++ b/extensions/matrix/src/matrix/monitor/media.ts @@ -24,7 +24,9 @@ async function fetchMatrixMediaBuffer(params: { }): Promise<{ buffer: Buffer; headerType?: string } | null> { // @vector-im/matrix-bot-sdk provides mxcToHttp helper const url = params.client.mxcToHttp(params.mxcUrl); - if (!url) return null; + if (!url) { + return null; + } // Use the client's download method which handles auth try { @@ -34,7 +36,7 @@ async function fetchMatrixMediaBuffer(params: { } return { buffer: Buffer.from(buffer) }; } catch (err) { - throw new Error(`Matrix media download failed: ${String(err)}`); + throw new Error(`Matrix media download failed: ${String(err)}`, { cause: err }); } } @@ -94,7 +96,9 @@ export async function downloadMatrixMedia(params: { }); } - if (!fetched) return null; + if (!fetched) { + return null; + } const headerType = fetched.headerType ?? params.contentType ?? undefined; const saved = await getMatrixRuntime().channel.media.saveMediaBuffer( fetched.buffer, diff --git a/extensions/matrix/src/matrix/monitor/replies.ts b/extensions/matrix/src/matrix/monitor/replies.ts index a4640f3cc..4aef77b74 100644 --- a/extensions/matrix/src/matrix/monitor/replies.ts +++ b/extensions/matrix/src/matrix/monitor/replies.ts @@ -62,7 +62,9 @@ export async function deliverMatrixReplies(params: { chunkMode, )) { const trimmed = chunk.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } await sendMessageMatrix(params.roomId, trimmed, { client: params.client, replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined, diff --git a/extensions/matrix/src/matrix/monitor/room-info.ts b/extensions/matrix/src/matrix/monitor/room-info.ts index c36373037..764147d35 100644 --- a/extensions/matrix/src/matrix/monitor/room-info.ts +++ b/extensions/matrix/src/matrix/monitor/room-info.ts @@ -11,7 +11,9 @@ export function createMatrixRoomInfoResolver(client: MatrixClient) { const getRoomInfo = async (roomId: string): Promise => { const cached = roomInfoCache.get(roomId); - if (cached) return cached; + if (cached) { + return cached; + } let name: string | undefined; let canonicalAlias: string | undefined; let altAliases: string[] = []; diff --git a/extensions/matrix/src/matrix/monitor/threads.ts b/extensions/matrix/src/matrix/monitor/threads.ts index 4d618f329..a38495716 100644 --- a/extensions/matrix/src/matrix/monitor/threads.ts +++ b/extensions/matrix/src/matrix/monitor/threads.ts @@ -28,7 +28,9 @@ export function resolveMatrixThreadTarget(params: { isThreadRoot?: boolean; }): string | undefined { const { threadReplies, messageId, threadRootId } = params; - if (threadReplies === "off") return undefined; + if (threadReplies === "off") { + return undefined; + } const isThreadRoot = params.isThreadRoot === true; const hasInboundThread = Boolean(threadRootId && threadRootId !== messageId && !isThreadRoot); if (threadReplies === "inbound") { @@ -45,7 +47,9 @@ export function resolveMatrixThreadRootId(params: { content: RoomMessageEventContent; }): string | undefined { const relates = params.content["m.relates_to"]; - if (!relates || typeof relates !== "object") return undefined; + if (!relates || typeof relates !== "object") { + return undefined; + } if ("rel_type" in relates && relates.rel_type === RelationType.Thread) { if ("event_id" in relates && typeof relates.event_id === "string") { return relates.event_id; diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index 1d4ab79b9..29897d895 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -77,7 +77,9 @@ export function isPollStartType(eventType: string): boolean { } export function getTextContent(text?: TextContent): string { - if (!text) return ""; + if (!text) { + return ""; + } return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? ""; } @@ -86,10 +88,14 @@ export function parsePollStartContent(content: PollStartContent): PollSummary | (content as Record)[M_POLL_START] ?? (content as Record)[ORG_POLL_START] ?? (content as Record)["m.poll"]; - if (!poll) return null; + if (!poll) { + return null; + } const question = getTextContent(poll.question); - if (!question) return null; + if (!question) { + return null; + } const answers = poll.answers .map((answer) => getTextContent(answer)) @@ -125,7 +131,9 @@ function buildTextContent(body: string): TextContent { } function buildPollFallbackText(question: string, answers: string[]): string { - if (answers.length === 0) return question; + if (answers.length === 0) { + return question; + } return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`; } diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 8f853ff2a..51eb11060 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -123,7 +123,9 @@ export async function sendMessageMatrix( const followupRelation = threadId ? relation : undefined; for (const chunk of textChunks) { const text = chunk.trim(); - if (!text) continue; + if (!text) { + continue; + } const followup = buildTextContent(text, followupRelation); const followupEventId = await sendContent(followup); lastMessageId = followupEventId ?? lastMessageId; @@ -131,7 +133,9 @@ export async function sendMessageMatrix( } else { for (const chunk of chunks.length ? chunks : [""]) { const text = chunk.trim(); - if (!text) continue; + if (!text) { + continue; + } const content = buildTextContent(text, relation); const eventId = await sendContent(content); lastMessageId = eventId ?? lastMessageId; @@ -211,7 +215,9 @@ export async function sendReadReceiptMatrix( eventId: string, client?: MatrixClient, ): Promise { - if (!eventId?.trim()) return; + if (!eventId?.trim()) { + return; + } const { client: resolved, stopOnDone } = await resolveMatrixClient({ client, }); diff --git a/extensions/matrix/src/matrix/send/client.ts b/extensions/matrix/src/matrix/send/client.ts index 296a790fe..359d12672 100644 --- a/extensions/matrix/src/matrix/send/client.ts +++ b/extensions/matrix/src/matrix/send/client.ts @@ -31,9 +31,13 @@ export async function resolveMatrixClient(opts: { timeoutMs?: number; }): Promise<{ client: MatrixClient; stopOnDone: boolean }> { ensureNodeRuntime(); - if (opts.client) return { client: opts.client, stopOnDone: false }; + if (opts.client) { + return { client: opts.client, stopOnDone: false }; + } const active = getActiveMatrixClient(); - if (active) return { client: active, stopOnDone: false }; + if (active) { + return { client: active, stopOnDone: false }; + } const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT); if (shouldShareClient) { const client = await resolveSharedMatrixClient({ diff --git a/extensions/matrix/src/matrix/send/formatting.ts b/extensions/matrix/src/matrix/send/formatting.ts index 532eae793..52f229d18 100644 --- a/extensions/matrix/src/matrix/send/formatting.ts +++ b/extensions/matrix/src/matrix/send/formatting.ts @@ -30,14 +30,18 @@ export function buildTextContent(body: string, relation?: MatrixRelation): Matri export function applyMatrixFormatting(content: MatrixFormattedContent, body: string): void { const formatted = markdownToMatrixHtml(body ?? ""); - if (!formatted) return; + if (!formatted) { + return; + } content.format = "org.matrix.custom.html"; content.formatted_body = formatted; } export function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined { const trimmed = replyToId?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } return { "m.in_reply_to": { event_id: trimmed } }; } @@ -70,7 +74,9 @@ export function resolveMatrixVoiceDecision(opts: { contentType?: string; fileName?: string; }): { useVoice: boolean } { - if (!opts.wantsVoice) return { useVoice: false }; + if (!opts.wantsVoice) { + return { useVoice: false }; + } if ( getCore().media.isVoiceCompatibleAudio({ contentType: opts.contentType, diff --git a/extensions/matrix/src/matrix/send/media.ts b/extensions/matrix/src/matrix/send/media.ts index 8b607881f..93598847e 100644 --- a/extensions/matrix/src/matrix/send/media.ts +++ b/extensions/matrix/src/matrix/send/media.ts @@ -54,7 +54,9 @@ export function buildMatrixMediaInfo(params: { }; return timedInfo; } - if (Object.keys(base).length === 0) return undefined; + if (Object.keys(base).length === 0) { + return undefined; + } return base; } @@ -116,7 +118,9 @@ export async function prepareImageInfo(params: { const meta = await getCore() .media.getImageMetadata(params.buffer) .catch(() => null); - if (!meta) return undefined; + if (!meta) { + return undefined; + } const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height }; const maxDim = Math.max(meta.width, meta.height); if (maxDim > THUMBNAIL_MAX_SIDE) { @@ -157,7 +161,9 @@ export async function resolveMediaDurationMs(params: { fileName?: string; kind: MediaKind; }): Promise { - if (params.kind !== "audio" && params.kind !== "video") return undefined; + if (params.kind !== "audio" && params.kind !== "video") { + return undefined; + } try { const fileInfo: IFileInfo | string | undefined = params.contentType || params.fileName diff --git a/extensions/matrix/src/matrix/send/targets.test.ts b/extensions/matrix/src/matrix/send/targets.test.ts index 0000be68d..39949473c 100644 --- a/extensions/matrix/src/matrix/send/targets.test.ts +++ b/extensions/matrix/src/matrix/send/targets.test.ts @@ -26,7 +26,9 @@ describe("resolveMatrixRoomId", () => { const roomId = await resolveMatrixRoomId(client, userId); expect(roomId).toBe("!room:example.org"); + // oxlint-disable-next-line typescript/unbound-method expect(client.getJoinedRooms).not.toHaveBeenCalled(); + // oxlint-disable-next-line typescript/unbound-method expect(client.setAccountData).not.toHaveBeenCalled(); }); diff --git a/extensions/matrix/src/matrix/send/targets.ts b/extensions/matrix/src/matrix/send/targets.ts index f2b508408..ee697c3f7 100644 --- a/extensions/matrix/src/matrix/send/targets.ts +++ b/extensions/matrix/src/matrix/send/targets.ts @@ -11,7 +11,9 @@ function normalizeTarget(raw: string): string { } export function normalizeThreadId(raw?: string | number | null): string | null { - if (raw === undefined || raw === null) return null; + if (raw === undefined || raw === null) { + return null; + } const trimmed = String(raw).trim(); return trimmed ? trimmed : null; } @@ -25,15 +27,15 @@ async function persistDirectRoom( ): Promise { let directContent: MatrixDirectAccountData | null = null; try { - directContent = (await client.getAccountData( - EventType.Direct, - )) as MatrixDirectAccountData | null; + directContent = await client.getAccountData(EventType.Direct); } catch { // Ignore fetch errors and fall back to an empty map. } const existing = directContent && !Array.isArray(directContent) ? directContent : {}; const current = Array.isArray(existing[userId]) ? existing[userId] : []; - if (current[0] === roomId) return; + if (current[0] === roomId) { + return; + } const next = [roomId, ...current.filter((id) => id !== roomId)]; try { await client.setAccountData(EventType.Direct, { @@ -52,13 +54,13 @@ async function resolveDirectRoomId(client: MatrixClient, userId: string): Promis } const cached = directRoomCache.get(trimmed); - if (cached) return cached; + if (cached) { + return cached; + } // 1) Fast path: use account data (m.direct) for *this* logged-in user (the bot). try { - const directContent = (await client.getAccountData( - EventType.Direct, - )) as MatrixDirectAccountData | null; + const directContent = await client.getAccountData(EventType.Direct); const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : []; if (list.length > 0) { directRoomCache.set(trimmed, list[0]); @@ -80,7 +82,9 @@ async function resolveDirectRoomId(client: MatrixClient, userId: string): Promis } catch { continue; } - if (!members.includes(trimmed)) continue; + if (!members.includes(trimmed)) { + continue; + } // Prefer classic 1:1 rooms, but allow larger rooms if requested. if (members.length === 2) { directRoomCache.set(trimmed, roomId); diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index f4fde614d..2a31aa6ce 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -249,8 +249,12 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { initialValue: existing.homeserver ?? envHomeserver, validate: (value) => { const raw = String(value ?? "").trim(); - if (!raw) return "Required"; - if (!/^https?:\/\//i.test(raw)) return "Use a full URL (https://...)"; + if (!raw) { + return "Required"; + } + if (!/^https?:\/\//i.test(raw)) { + return "Use a full URL (https://...)"; + } return undefined; }, }), @@ -274,13 +278,13 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { if (!accessToken && !password) { // Ask auth method FIRST before asking for user ID - const authMode = (await prompter.select({ + const authMode = await prompter.select({ message: "Matrix auth method", options: [ { value: "token", label: "Access token (user ID fetched automatically)" }, { value: "password", label: "Password (requires user ID)" }, ], - })) as "token" | "password"; + }); if (authMode === "token") { accessToken = String( @@ -300,9 +304,15 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { initialValue: existing.userId ?? envUserId, validate: (value) => { const raw = String(value ?? "").trim(); - if (!raw) return "Required"; - if (!raw.startsWith("@")) return "Matrix user IDs should start with @"; - if (!raw.includes(":")) return "Matrix user IDs should include a server (:server)"; + if (!raw) { + return "Required"; + } + if (!raw.startsWith("@")) { + return "Matrix user IDs should start with @"; + } + if (!raw.includes(":")) { + return "Matrix user IDs should include a server (:server)"; + } return undefined; }, }), @@ -370,7 +380,9 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { const unresolved: string[] = []; for (const entry of accessConfig.entries) { const trimmed = entry.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } const cleaned = trimmed.replace(/^(room|channel):/i, "").trim(); if (cleaned.startsWith("!") && cleaned.includes(":")) { resolvedIds.push(cleaned); diff --git a/extensions/matrix/src/resolve-targets.ts b/extensions/matrix/src/resolve-targets.ts index 03c45dd00..ccb790e42 100644 --- a/extensions/matrix/src/resolve-targets.ts +++ b/extensions/matrix/src/resolve-targets.ts @@ -11,7 +11,9 @@ function pickBestGroupMatch( matches: ChannelDirectoryEntry[], query: string, ): ChannelDirectoryEntry | undefined { - if (matches.length === 0) return undefined; + if (matches.length === 0) { + return undefined; + } const normalized = query.trim().toLowerCase(); if (normalized) { const exact = matches.find((match) => { @@ -20,7 +22,9 @@ function pickBestGroupMatch( const id = match.id.trim().toLowerCase(); return name === normalized || handle === normalized || id === normalized; }); - if (exact) return exact; + if (exact) { + return exact; + } } return matches[0]; } diff --git a/extensions/matrix/src/tool-actions.ts b/extensions/matrix/src/tool-actions.ts index e26c27755..7e3c03158 100644 --- a/extensions/matrix/src/tool-actions.ts +++ b/extensions/matrix/src/tool-actions.ts @@ -29,8 +29,12 @@ const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]); function readRoomId(params: Record, required = true): string { const direct = readStringParam(params, "roomId") ?? readStringParam(params, "channelId"); - if (direct) return direct; - if (!required) return readStringParam(params, "to") ?? ""; + if (direct) { + return direct; + } + if (!required) { + return readStringParam(params, "to") ?? ""; + } return readStringParam(params, "to", { required: true }); } diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index c31b603ca..8f6c08355 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -6,7 +6,9 @@ describe("mattermostPlugin", () => { describe("messaging", () => { it("keeps @username targets", () => { const normalize = mattermostPlugin.messaging?.normalizeTarget; - if (!normalize) return; + if (!normalize) { + return; + } expect(normalize("@Alice")).toBe("@Alice"); expect(normalize("@alice")).toBe("@alice"); @@ -14,7 +16,9 @@ describe("mattermostPlugin", () => { it("normalizes mattermost: prefix to user:", () => { const normalize = mattermostPlugin.messaging?.normalizeTarget; - if (!normalize) return; + if (!normalize) { + return; + } expect(normalize("mattermost:USER123")).toBe("user:USER123"); }); @@ -23,7 +27,9 @@ describe("mattermostPlugin", () => { describe("pairing", () => { it("normalizes allowlist entries", () => { const normalize = mattermostPlugin.pairing?.normalizeAllowEntry; - if (!normalize) return; + if (!normalize) { + return; + } expect(normalize("@Alice")).toBe("alice"); expect(normalize("user:USER123")).toBe("user123"); diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index fcda66d30..75521d6ea 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -49,7 +49,9 @@ function normalizeAllowEntry(entry: string): string { function formatAllowEntry(entry: string): string { const trimmed = entry.trim(); - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } if (trimmed.startsWith("@")) { const username = trimmed.slice(1).trim(); return username ? `@${username.toLowerCase()}` : ""; @@ -134,7 +136,9 @@ export const mattermostPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ `- Mattermost channels: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.mattermost.groupPolicy="allowlist" + channels.mattermost.groupAllowFrom to restrict senders.`, ]; diff --git a/extensions/mattermost/src/group-mentions.ts b/extensions/mattermost/src/group-mentions.ts index ed71aa41b..416d36852 100644 --- a/extensions/mattermost/src/group-mentions.ts +++ b/extensions/mattermost/src/group-mentions.ts @@ -9,6 +9,8 @@ export function resolveMattermostGroupRequireMention( cfg: params.cfg, accountId: params.accountId, }); - if (typeof account.requireMention === "boolean") return account.requireMention; + if (typeof account.requireMention === "boolean") { + return account.requireMention; + } return true; } diff --git a/extensions/mattermost/src/mattermost/accounts.ts b/extensions/mattermost/src/mattermost/accounts.ts index e611fb522..e56084316 100644 --- a/extensions/mattermost/src/mattermost/accounts.ts +++ b/extensions/mattermost/src/mattermost/accounts.ts @@ -26,19 +26,25 @@ export type ResolvedMattermostAccount = { function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = cfg.channels?.mattermost?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } export function listMattermostAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultMattermostAccountId(cfg: OpenClawConfig): string { const ids = listMattermostAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -47,7 +53,9 @@ function resolveAccountConfig( accountId: string, ): MattermostAccountConfig | undefined { const accounts = cfg.channels?.mattermost?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as MattermostAccountConfig | undefined; } @@ -62,9 +70,15 @@ function mergeMattermostAccountConfig( } function resolveMattermostRequireMention(config: MattermostAccountConfig): boolean | undefined { - if (config.chatmode === "oncall") return true; - if (config.chatmode === "onmessage") return false; - if (config.chatmode === "onchar") return true; + if (config.chatmode === "oncall") { + return true; + } + if (config.chatmode === "onmessage") { + return false; + } + if (config.chatmode === "onchar") { + return true; + } return config.requireMention; } diff --git a/extensions/mattermost/src/mattermost/client.ts b/extensions/mattermost/src/mattermost/client.ts index 6b63f830f..a3e151834 100644 --- a/extensions/mattermost/src/mattermost/client.ts +++ b/extensions/mattermost/src/mattermost/client.ts @@ -42,14 +42,18 @@ export type MattermostFileInfo = { export function normalizeMattermostBaseUrl(raw?: string | null): string | undefined { const trimmed = raw?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const withoutTrailing = trimmed.replace(/\/+$/, ""); return withoutTrailing.replace(/\/api\/v4$/i, ""); } function buildMattermostApiUrl(baseUrl: string, path: string): string { const normalized = normalizeMattermostBaseUrl(baseUrl); - if (!normalized) throw new Error("Mattermost baseUrl is required"); + if (!normalized) { + throw new Error("Mattermost baseUrl is required"); + } const suffix = path.startsWith("/") ? path : `/${path}`; return `${normalized}/api/v4${suffix}`; } @@ -58,7 +62,9 @@ async function readMattermostError(res: Response): Promise { const contentType = res.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) { const data = (await res.json()) as { message?: string } | undefined; - if (data?.message) return data.message; + if (data?.message) { + return data.message; + } return JSON.stringify(data); } return await res.text(); @@ -70,7 +76,9 @@ export function createMattermostClient(params: { fetchImpl?: typeof fetch; }): MattermostClient { const baseUrl = normalizeMattermostBaseUrl(params.baseUrl); - if (!baseUrl) throw new Error("Mattermost baseUrl is required"); + if (!baseUrl) { + throw new Error("Mattermost baseUrl is required"); + } const apiBaseUrl = `${baseUrl}/api/v4`; const token = params.botToken.trim(); const fetchImpl = params.fetchImpl ?? fetch; @@ -128,7 +136,9 @@ export async function sendMattermostTyping( channel_id: params.channelId, }; const parentId = params.parentId?.trim(); - if (parentId) payload.parent_id = parentId; + if (parentId) { + payload.parent_id = parentId; + } await client.request>("/users/me/typing", { method: "POST", body: JSON.stringify(payload), @@ -158,7 +168,9 @@ export async function createMattermostPost( channel_id: params.channelId, message: params.message, }; - if (params.rootId) payload.root_id = params.rootId; + if (params.rootId) { + payload.root_id = params.rootId; + } if (params.fileIds?.length) { (payload as Record).file_ids = params.fileIds; } diff --git a/extensions/mattermost/src/mattermost/monitor-helpers.ts b/extensions/mattermost/src/mattermost/monitor-helpers.ts index 070354115..9e7e2a165 100644 --- a/extensions/mattermost/src/mattermost/monitor-helpers.ts +++ b/extensions/mattermost/src/mattermost/monitor-helpers.ts @@ -34,7 +34,9 @@ export function formatInboundFromLabel(params: { const directLabel = params.directLabel.trim(); const directId = params.directId?.trim(); - if (!directId || directId === directLabel) return directLabel; + if (!directId || directId === directLabel) { + return directLabel; + } return `${directLabel} id:${directId}`; } @@ -67,14 +69,18 @@ export function createDedupeCache(options: { ttlMs: number; maxSize: number }): } while (cache.size > maxSize) { const oldestKey = cache.keys().next().value as string | undefined; - if (!oldestKey) break; + if (!oldestKey) { + break; + } cache.delete(oldestKey); } }; return { check: (key, now = Date.now()) => { - if (!key) return false; + if (!key) { + return false; + } const existing = cache.get(key); if (existing !== undefined && (ttlMs <= 0 || now - existing < ttlMs)) { touch(key, now); @@ -91,9 +97,15 @@ export function rawDataToString( data: WebSocket.RawData, encoding: BufferEncoding = "utf8", ): string { - if (typeof data === "string") return data; - if (Buffer.isBuffer(data)) return data.toString(encoding); - if (Array.isArray(data)) return Buffer.concat(data).toString(encoding); + if (typeof data === "string") { + return data; + } + if (Buffer.isBuffer(data)) { + return data.toString(encoding); + } + if (Array.isArray(data)) { + return Buffer.concat(data).toString(encoding); + } if (data instanceof ArrayBuffer) { return Buffer.from(data).toString(encoding); } @@ -102,8 +114,12 @@ export function rawDataToString( function normalizeAgentId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); - if (!trimmed) return "main"; - if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed; + if (!trimmed) { + return "main"; + } + if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) { + return trimmed; + } return ( trimmed .toLowerCase() @@ -118,7 +134,9 @@ type AgentEntry = NonNullable["list"]>[num function listAgents(cfg: OpenClawConfig): AgentEntry[] { const list = cfg.agents?.list; - if (!Array.isArray(list)) return []; + if (!Array.isArray(list)) { + return []; + } return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object")); } diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 0d6e65025..b132d5760 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -96,7 +96,9 @@ function resolveRuntime(opts: MonitorMattermostOpts): RuntimeEnv { } function normalizeMention(text: string, mention: string | undefined): string { - if (!mention) return text.trim(); + if (!mention) { + return text.trim(); + } const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`@${escaped}\\b`, "gi"); return text.replace(re, " ").replace(/\s+/g, " ").trim(); @@ -113,7 +115,9 @@ function stripOncharPrefix( ): { triggered: boolean; stripped: string } { const trimmed = text.trimStart(); for (const prefix of prefixes) { - if (!prefix) continue; + if (!prefix) { + continue; + } if (trimmed.startsWith(prefix)) { return { triggered: true, @@ -130,23 +134,37 @@ function isSystemPost(post: MattermostPost): boolean { } function channelKind(channelType?: string | null): "dm" | "group" | "channel" { - if (!channelType) return "channel"; + if (!channelType) { + return "channel"; + } const normalized = channelType.trim().toUpperCase(); - if (normalized === "D") return "dm"; - if (normalized === "G") return "group"; + if (normalized === "D") { + return "dm"; + } + if (normalized === "G") { + return "group"; + } return "channel"; } function channelChatType(kind: "dm" | "group" | "channel"): "direct" | "group" | "channel" { - if (kind === "dm") return "direct"; - if (kind === "group") return "group"; + if (kind === "dm") { + return "direct"; + } + if (kind === "group") { + return "group"; + } return "channel"; } function normalizeAllowEntry(entry: string): string { const trimmed = entry.trim(); - if (!trimmed) return ""; - if (trimmed === "*") return "*"; + if (!trimmed) { + return ""; + } + if (trimmed === "*") { + return "*"; + } return trimmed .replace(/^(mattermost|user):/i, "") .replace(/^@/, "") @@ -164,8 +182,12 @@ function isSenderAllowed(params: { allowFrom: string[]; }): boolean { const allowFrom = params.allowFrom; - if (allowFrom.length === 0) return false; - if (allowFrom.includes("*")) return true; + if (allowFrom.length === 0) { + return false; + } + if (allowFrom.includes("*")) { + return true; + } const normalizedSenderId = normalizeAllowEntry(params.senderId); const normalizedSenderName = params.senderName ? normalizeAllowEntry(params.senderName) : ""; return allowFrom.some( @@ -181,7 +203,9 @@ type MattermostMediaInfo = { }; function buildMattermostAttachmentPlaceholder(mediaList: MattermostMediaInfo[]): string { - if (mediaList.length === 0) return ""; + if (mediaList.length === 0) { + return ""; + } if (mediaList.length === 1) { const kind = mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind; return ``; @@ -216,7 +240,9 @@ function buildMattermostMediaPayload(mediaList: MattermostMediaInfo[]): { function buildMattermostWsUrl(baseUrl: string): string { const normalized = normalizeMattermostBaseUrl(baseUrl); - if (!normalized) throw new Error("Mattermost baseUrl is required"); + if (!normalized) { + throw new Error("Mattermost baseUrl is required"); + } const wsBase = normalized.replace(/^http/i, "ws"); return `${wsBase}/api/v4/websocket`; } @@ -252,7 +278,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const userCache = new Map(); const logger = core.logging.getChildLogger({ module: "mattermost" }); const logVerboseMessage = (message: string) => { - if (!core.logging.shouldLogVerbose()) return; + if (!core.logging.shouldLogVerbose()) { + return; + } logger.debug?.(message); }; const mediaMaxBytes = @@ -276,8 +304,10 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const resolveMattermostMedia = async ( fileIds?: string[] | null, ): Promise => { - const ids = (fileIds ?? []).map((id) => id?.trim()).filter(Boolean) as string[]; - if (ids.length === 0) return []; + const ids = (fileIds ?? []).map((id) => id?.trim()).filter(Boolean); + if (ids.length === 0) { + return []; + } const out: MattermostMediaInfo[] = []; for (const fileId of ids) { try { @@ -312,7 +342,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const resolveChannelInfo = async (channelId: string): Promise => { const cached = channelCache.get(channelId); - if (cached && cached.expiresAt > Date.now()) return cached.value; + if (cached && cached.expiresAt > Date.now()) { + return cached.value; + } try { const info = await fetchMattermostChannel(client, channelId); channelCache.set(channelId, { @@ -332,7 +364,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const resolveUserInfo = async (userId: string): Promise => { const cached = userCache.get(userId); - if (cached && cached.expiresAt > Date.now()) return cached.value; + if (cached && cached.expiresAt > Date.now()) { + return cached.value; + } try { const info = await fetchMattermostUser(client, userId); userCache.set(userId, { @@ -356,19 +390,31 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} messageIds?: string[], ) => { const channelId = post.channel_id ?? payload.data?.channel_id ?? payload.broadcast?.channel_id; - if (!channelId) return; + if (!channelId) { + return; + } const allMessageIds = messageIds?.length ? messageIds : post.id ? [post.id] : []; - if (allMessageIds.length === 0) return; + if (allMessageIds.length === 0) { + return; + } const dedupeEntries = allMessageIds.map((id) => recentInboundMessages.check(`${account.accountId}:${id}`), ); - if (dedupeEntries.length > 0 && dedupeEntries.every(Boolean)) return; + if (dedupeEntries.length > 0 && dedupeEntries.every(Boolean)) { + return; + } const senderId = post.user_id ?? payload.broadcast?.user_id; - if (!senderId) return; - if (senderId === botUserId) return; - if (isSystemPost(post)) return; + if (!senderId) { + return; + } + if (senderId === botUserId) { + return; + } + if (isSystemPost(post)) { + return; + } const channelInfo = await resolveChannelInfo(channelId); const channelType = payload.data?.channel_type ?? channelInfo?.type ?? undefined; @@ -560,7 +606,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} channel: "mattermost", accountId: account.accountId, groupId: channelId, - }) !== false; + }); const shouldBypassMention = isControlCommand && shouldRequireMention && !wasMentioned && commandAuthorized; const effectiveWasMentioned = wasMentioned || shouldBypassMention || oncharTriggered; @@ -582,7 +628,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const bodySource = oncharTriggered ? oncharResult.stripped : rawText; const baseText = [bodySource, mediaPlaceholder].filter(Boolean).join("\n").trim(); const bodyText = normalizeMention(baseText, botUsername); - if (!bodyText) return; + if (!bodyText) { + return; + } core.channel.activity.record({ channel: "mattermost", @@ -743,7 +791,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} ); const chunks = core.channel.text.chunkMarkdownTextWithMode(text, textLimit, chunkMode); for (const chunk of chunks.length > 0 ? chunks : [text]) { - if (!chunk) continue; + if (!chunk) { + continue; + } await sendMessageMattermost(to, chunk, { accountId: account.accountId, replyToId: threadRootId, @@ -804,20 +854,28 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} entry.post.channel_id ?? entry.payload.data?.channel_id ?? entry.payload.broadcast?.channel_id; - if (!channelId) return null; + if (!channelId) { + return null; + } const threadId = entry.post.root_id?.trim(); const threadKey = threadId ? `thread:${threadId}` : "channel"; return `mattermost:${account.accountId}:${channelId}:${threadKey}`; }, shouldDebounce: (entry) => { - if (entry.post.file_ids && entry.post.file_ids.length > 0) return false; + if (entry.post.file_ids && entry.post.file_ids.length > 0) { + return false; + } const text = entry.post.message?.trim() ?? ""; - if (!text) return false; + if (!text) { + return false; + } return !core.channel.text.hasControlCommand(text, cfg); }, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } if (entries.length === 1) { await handlePost(last.post, last.payload); return; @@ -831,7 +889,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} message: combinedText, file_ids: [], }; - const ids = entries.map((entry) => entry.post.id).filter(Boolean) as string[]; + const ids = entries.map((entry) => entry.post.id).filter(Boolean); await handlePost(mergedPost, last.payload, ids.length > 0 ? ids : undefined); }, onError: (err) => { @@ -871,9 +929,13 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} } catch { return; } - if (payload.event !== "posted") return; + if (payload.event !== "posted") { + return; + } const postData = payload.data?.post; - if (!postData) return; + if (!postData) { + return; + } let post: MattermostPost | null = null; if (typeof postData === "string") { try { @@ -884,7 +946,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} } else if (typeof postData === "object") { post = postData as MattermostPost; } - if (!post) return; + if (!post) { + return; + } try { await debouncer.enqueue({ post, payload }); } catch (err) { @@ -917,7 +981,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} while (!opts.abortSignal?.aborted) { await connectOnce(); - if (opts.abortSignal?.aborted) return; + if (opts.abortSignal?.aborted) { + return; + } await new Promise((resolve) => setTimeout(resolve, 2000)); } } diff --git a/extensions/mattermost/src/mattermost/probe.ts b/extensions/mattermost/src/mattermost/probe.ts index c0fa8ae63..a02ca4935 100644 --- a/extensions/mattermost/src/mattermost/probe.ts +++ b/extensions/mattermost/src/mattermost/probe.ts @@ -12,7 +12,9 @@ async function readMattermostError(res: Response): Promise { const contentType = res.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) { const data = (await res.json()) as { message?: string } | undefined; - if (data?.message) return data.message; + if (data?.message) { + return data.message; + } return JSON.stringify(data); } return await res.text(); @@ -65,6 +67,8 @@ export async function probeMattermost( elapsedMs: Date.now() - start, }; } finally { - if (timer) clearTimeout(timer); + if (timer) { + clearTimeout(timer); + } } } diff --git a/extensions/mattermost/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts index cd205340d..b3e40e39c 100644 --- a/extensions/mattermost/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -49,21 +49,29 @@ function isHttpUrl(value: string): boolean { function parseMattermostTarget(raw: string): MattermostTarget { const trimmed = raw.trim(); - if (!trimmed) throw new Error("Recipient is required for Mattermost sends"); + if (!trimmed) { + throw new Error("Recipient is required for Mattermost sends"); + } const lower = trimmed.toLowerCase(); if (lower.startsWith("channel:")) { const id = trimmed.slice("channel:".length).trim(); - if (!id) throw new Error("Channel id is required for Mattermost sends"); + if (!id) { + throw new Error("Channel id is required for Mattermost sends"); + } return { kind: "channel", id }; } if (lower.startsWith("user:")) { const id = trimmed.slice("user:".length).trim(); - if (!id) throw new Error("User id is required for Mattermost sends"); + if (!id) { + throw new Error("User id is required for Mattermost sends"); + } return { kind: "user", id }; } if (lower.startsWith("mattermost:")) { const id = trimmed.slice("mattermost:".length).trim(); - if (!id) throw new Error("User id is required for Mattermost sends"); + if (!id) { + throw new Error("User id is required for Mattermost sends"); + } return { kind: "user", id }; } if (trimmed.startsWith("@")) { @@ -79,7 +87,9 @@ function parseMattermostTarget(raw: string): MattermostTarget { async function resolveBotUser(baseUrl: string, token: string): Promise { const key = cacheKey(baseUrl, token); const cached = botUserCache.get(key); - if (cached) return cached; + if (cached) { + return cached; + } const client = createMattermostClient({ baseUrl, botToken: token }); const user = await fetchMattermostMe(client); botUserCache.set(key, user); @@ -94,7 +104,9 @@ async function resolveUserIdByUsername(params: { const { baseUrl, token, username } = params; const key = `${cacheKey(baseUrl, token)}::${username.toLowerCase()}`; const cached = userByNameCache.get(key); - if (cached?.id) return cached.id; + if (cached?.id) { + return cached.id; + } const client = createMattermostClient({ baseUrl, botToken: token }); const user = await fetchMattermostUserByUsername(client, username); userByNameCache.set(key, user); @@ -106,7 +118,9 @@ async function resolveTargetChannelId(params: { baseUrl: string; token: string; }): Promise { - if (params.target.kind === "channel") return params.target.id; + if (params.target.kind === "channel") { + return params.target.id; + } const userId = params.target.id ? params.target.id : await resolveUserIdByUsername({ diff --git a/extensions/mattermost/src/normalize.ts b/extensions/mattermost/src/normalize.ts index b3318fe11..d8a8ee967 100644 --- a/extensions/mattermost/src/normalize.ts +++ b/extensions/mattermost/src/normalize.ts @@ -1,6 +1,8 @@ export function normalizeMattermostMessagingTarget(raw: string): string | undefined { const trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const lower = trimmed.toLowerCase(); if (lower.startsWith("channel:")) { const id = trimmed.slice("channel:".length).trim(); @@ -31,8 +33,14 @@ export function normalizeMattermostMessagingTarget(raw: string): string | undefi export function looksLikeMattermostTargetId(raw: string): boolean { const trimmed = raw.trim(); - if (!trimmed) return false; - if (/^(user|channel|group|mattermost):/i.test(trimmed)) return true; - if (/^[@#]/.test(trimmed)) return true; + if (!trimmed) { + return false; + } + if (/^(user|channel|group|mattermost):/i.test(trimmed)) { + return true; + } + if (/^[@#]/.test(trimmed)) { + return true; + } return /^[a-z0-9]{8,}$/i.test(trimmed); } diff --git a/extensions/mattermost/src/onboarding-helpers.ts b/extensions/mattermost/src/onboarding-helpers.ts index 59da610de..2c3bd5f41 100644 --- a/extensions/mattermost/src/onboarding-helpers.ts +++ b/extensions/mattermost/src/onboarding-helpers.ts @@ -13,7 +13,7 @@ type PromptAccountIdParams = { export async function promptAccountId(params: PromptAccountIdParams): Promise { const existingIds = params.listAccountIds(params.cfg); const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID; - const choice = (await params.prompter.select({ + const choice = await params.prompter.select({ message: `${params.label} account`, options: [ ...existingIds.map((id) => ({ @@ -23,9 +23,11 @@ export async function promptAccountId(params: PromptAccountIdParams): Promise = { function assertAllowedKeys(value: Record, allowed: string[], label: string) { const unknown = Object.keys(value).filter((key) => !allowed.includes(key)); - if (unknown.length === 0) return; + if (unknown.length === 0) { + return; + } throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`); } diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts index d3726554c..02692cc28 100644 --- a/extensions/memory-lancedb/index.test.ts +++ b/extensions/memory-lancedb/index.test.ts @@ -9,7 +9,6 @@ */ import { describe, test, expect, beforeEach, afterEach } from "vitest"; -import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; @@ -42,6 +41,7 @@ describe("memory plugin e2e", () => { expect(memoryPlugin.name).toBe("Memory (LanceDB)"); expect(memoryPlugin.kind).toBe("memory"); expect(memoryPlugin.configSchema).toBeDefined(); + // oxlint-disable-next-line typescript/unbound-method expect(memoryPlugin.register).toBeInstanceOf(Function); }); @@ -217,14 +217,16 @@ describeLive("memory plugin live tests", () => { registeredServices.push(service); }, on: (hookName: string, handler: any) => { - if (!registeredHooks[hookName]) registeredHooks[hookName] = []; + if (!registeredHooks[hookName]) { + registeredHooks[hookName] = []; + } registeredHooks[hookName].push(handler); }, resolvePath: (p: string) => p, }; // Register plugin - await memoryPlugin.register(mockApi as any); + memoryPlugin.register(mockApi as any); // Check registration expect(registeredTools.length).toBe(3); diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index 14d587dcf..54200295a 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -55,8 +55,12 @@ class MemoryDB { ) {} private async ensureInitialized(): Promise { - if (this.table) return; - if (this.initPromise) return this.initPromise; + if (this.table) { + return; + } + if (this.initPromise) { + return this.initPromise; + } this.initPromise = this.doInitialize(); return this.initPromise; @@ -73,7 +77,7 @@ class MemoryDB { { id: "__schema__", text: "", - vector: new Array(this.vectorDim).fill(0), + vector: Array.from({ length: this.vectorDim }).fill(0), importance: 0, category: "other", createdAt: 0, @@ -179,25 +183,43 @@ const MEMORY_TRIGGERS = [ ]; function shouldCapture(text: string): boolean { - if (text.length < 10 || text.length > 500) return false; + if (text.length < 10 || text.length > 500) { + return false; + } // Skip injected context from memory recall - if (text.includes("")) return false; + if (text.includes("")) { + return false; + } // Skip system-generated content - if (text.startsWith("<") && text.includes(" 3) return false; + if (emojiCount > 3) { + return false; + } return MEMORY_TRIGGERS.some((r) => r.test(text)); } function detectCategory(text: string): MemoryCategory { const lower = text.toLowerCase(); - if (/prefer|radši|like|love|hate|want/i.test(lower)) return "preference"; - if (/rozhodli|decided|will use|budeme/i.test(lower)) return "decision"; - if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower)) return "entity"; - if (/is|are|has|have|je|má|jsou/i.test(lower)) return "fact"; + if (/prefer|radši|like|love|hate|want/i.test(lower)) { + return "preference"; + } + if (/rozhodli|decided|will use|budeme/i.test(lower)) { + return "decision"; + } + if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower)) { + return "entity"; + } + if (/is|are|has|have|je|má|jsou/i.test(lower)) { + return "fact"; + } return "other"; } @@ -455,13 +477,17 @@ const memoryPlugin = { // Auto-recall: inject relevant memories before agent starts if (cfg.autoRecall) { api.on("before_agent_start", async (event) => { - if (!event.prompt || event.prompt.length < 5) return; + if (!event.prompt || event.prompt.length < 5) { + return; + } try { const vector = await embeddings.embed(event.prompt); const results = await db.search(vector, 3, 0.3); - if (results.length === 0) return; + if (results.length === 0) { + return; + } const memoryContext = results .map((r) => `- [${r.entry.category}] ${r.entry.text}`) @@ -490,12 +516,16 @@ const memoryPlugin = { const texts: string[] = []; for (const msg of event.messages) { // Type guard for message object - if (!msg || typeof msg !== "object") continue; + if (!msg || typeof msg !== "object") { + continue; + } const msgObj = msg as Record; // Only process user and assistant messages const role = msgObj.role; - if (role !== "user" && role !== "assistant") continue; + if (role !== "user" && role !== "assistant") { + continue; + } const content = msgObj.content; @@ -524,7 +554,9 @@ const memoryPlugin = { // Filter for capturable content const toCapture = texts.filter((text) => text && shouldCapture(text)); - if (toCapture.length === 0) return; + if (toCapture.length === 0) { + return; + } // Store each capturable piece (limit to 3 per conversation) let stored = 0; @@ -534,7 +566,9 @@ const memoryPlugin = { // Check for duplicates (high similarity threshold) const existing = await db.search(vector, 1, 0.95); - if (existing.length > 0) continue; + if (existing.length > 0) { + continue; + } await db.store({ text, diff --git a/extensions/msteams/src/attachments/download.ts b/extensions/msteams/src/attachments/download.ts index cadb00dca..ab9410bfb 100644 --- a/extensions/msteams/src/attachments/download.ts +++ b/extensions/msteams/src/attachments/download.ts @@ -26,10 +26,14 @@ function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate const name = typeof att.name === "string" ? att.name.trim() : ""; if (contentType === "application/vnd.microsoft.teams.file.download.info") { - if (!isRecord(att.content)) return null; + if (!isRecord(att.content)) { + return null; + } const downloadUrl = typeof att.content.downloadUrl === "string" ? att.content.downloadUrl.trim() : ""; - if (!downloadUrl) return null; + if (!downloadUrl) { + return null; + } const fileType = typeof att.content.fileType === "string" ? att.content.fileType.trim() : ""; const uniqueId = typeof att.content.uniqueId === "string" ? att.content.uniqueId.trim() : ""; @@ -49,7 +53,9 @@ function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate } const contentUrl = typeof att.contentUrl === "string" ? att.contentUrl.trim() : ""; - if (!contentUrl) return null; + if (!contentUrl) { + return null; + } return { url: contentUrl, @@ -82,9 +88,15 @@ async function fetchWithAuthFallback(params: { }): Promise { const fetchFn = params.fetchFn ?? fetch; const firstAttempt = await fetchFn(params.url); - if (firstAttempt.ok) return firstAttempt; - if (!params.tokenProvider) return firstAttempt; - if (firstAttempt.status !== 401 && firstAttempt.status !== 403) return firstAttempt; + if (firstAttempt.ok) { + return firstAttempt; + } + if (!params.tokenProvider) { + return firstAttempt; + } + if (firstAttempt.status !== 401 && firstAttempt.status !== 403) { + return firstAttempt; + } const scopes = scopeCandidatesForUrl(params.url); for (const scope of scopes) { @@ -93,7 +105,9 @@ async function fetchWithAuthFallback(params: { const res = await fetchFn(params.url, { headers: { Authorization: `Bearer ${token}` }, }); - if (res.ok) return res; + if (res.ok) { + return res; + } } catch { // Try the next scope. } @@ -116,7 +130,9 @@ export async function downloadMSTeamsAttachments(params: { preserveFilenames?: boolean; }): Promise { const list = Array.isArray(params.attachments) ? params.attachments : []; - if (list.length === 0) return []; + if (list.length === 0) { + return []; + } const allowHosts = resolveAllowedHosts(params.allowHosts); // Download ANY downloadable attachment (not just images) @@ -130,8 +146,12 @@ export async function downloadMSTeamsAttachments(params: { const seenUrls = new Set(); for (const inline of inlineCandidates) { if (inline.kind === "url") { - if (!isUrlAllowed(inline.url, allowHosts)) continue; - if (seenUrls.has(inline.url)) continue; + if (!isUrlAllowed(inline.url, allowHosts)) { + continue; + } + if (seenUrls.has(inline.url)) { + continue; + } seenUrls.add(inline.url); candidates.push({ url: inline.url, @@ -141,12 +161,18 @@ export async function downloadMSTeamsAttachments(params: { }); } } - if (candidates.length === 0 && inlineCandidates.length === 0) return []; + if (candidates.length === 0 && inlineCandidates.length === 0) { + return []; + } const out: MSTeamsInboundMedia[] = []; for (const inline of inlineCandidates) { - if (inline.kind !== "data") continue; - if (inline.data.byteLength > params.maxBytes) continue; + if (inline.kind !== "data") { + continue; + } + if (inline.data.byteLength > params.maxBytes) { + continue; + } try { // Data inline candidates (base64 data URLs) don't have original filenames const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer( @@ -165,16 +191,22 @@ export async function downloadMSTeamsAttachments(params: { } } for (const candidate of candidates) { - if (!isUrlAllowed(candidate.url, allowHosts)) continue; + if (!isUrlAllowed(candidate.url, allowHosts)) { + continue; + } try { const res = await fetchWithAuthFallback({ url: candidate.url, tokenProvider: params.tokenProvider, fetchFn: params.fetchFn, }); - if (!res.ok) continue; + if (!res.ok) { + continue; + } const buffer = Buffer.from(await res.arrayBuffer()); - if (buffer.byteLength > params.maxBytes) continue; + if (buffer.byteLength > params.maxBytes) { + continue; + } const mime = await getMSTeamsRuntime().media.detectMime({ buffer, headerMime: res.headers.get("content-type"), diff --git a/extensions/msteams/src/attachments/graph.ts b/extensions/msteams/src/attachments/graph.ts index ccf58fd27..e1b7d2c67 100644 --- a/extensions/msteams/src/attachments/graph.ts +++ b/extensions/msteams/src/attachments/graph.ts @@ -32,7 +32,9 @@ type GraphAttachment = { function readNestedString(value: unknown, keys: Array): string | undefined { let current: unknown = value; for (const key of keys) { - if (!isRecord(current)) return undefined; + if (!isRecord(current)) { + return undefined; + } current = current[key as keyof typeof current]; } return typeof current === "string" && current.trim() ? current.trim() : undefined; @@ -50,7 +52,9 @@ export function buildMSTeamsGraphMessageUrls(params: { const messageIdCandidates = new Set(); const pushCandidate = (value: string | null | undefined) => { const trimmed = typeof value === "string" ? value.trim() : ""; - if (trimmed) messageIdCandidates.add(trimmed); + if (trimmed) { + messageIdCandidates.add(trimmed); + } }; pushCandidate(params.messageId); @@ -68,17 +72,23 @@ export function buildMSTeamsGraphMessageUrls(params: { readNestedString(params.channelData, ["channel", "id"]) ?? readNestedString(params.channelData, ["channelId"]) ?? readNestedString(params.channelData, ["teamsChannelId"]); - if (!teamId || !channelId) return []; + if (!teamId || !channelId) { + return []; + } const urls: string[] = []; if (replyToId) { for (const candidate of messageIdCandidates) { - if (candidate === replyToId) continue; + if (candidate === replyToId) { + continue; + } urls.push( `${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(replyToId)}/replies/${encodeURIComponent(candidate)}`, ); } } - if (messageIdCandidates.size === 0 && replyToId) messageIdCandidates.add(replyToId); + if (messageIdCandidates.size === 0 && replyToId) { + messageIdCandidates.add(replyToId); + } for (const candidate of messageIdCandidates) { urls.push( `${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(candidate)}`, @@ -88,8 +98,12 @@ export function buildMSTeamsGraphMessageUrls(params: { } const chatId = params.conversationId?.trim() || readNestedString(params.channelData, ["chatId"]); - if (!chatId) return []; - if (messageIdCandidates.size === 0 && replyToId) messageIdCandidates.add(replyToId); + if (!chatId) { + return []; + } + if (messageIdCandidates.size === 0 && replyToId) { + messageIdCandidates.add(replyToId); + } const urls = Array.from(messageIdCandidates).map( (candidate) => `${GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(candidate)}`, @@ -107,7 +121,9 @@ async function fetchGraphCollection(params: { headers: { Authorization: `Bearer ${params.accessToken}` }, }); const status = res.status; - if (!res.ok) return { status, items: [] }; + if (!res.ok) { + return { status, items: [] }; + } try { const data = (await res.json()) as { value?: T[] }; return { status, items: Array.isArray(data.value) ? data.value : [] }; @@ -157,14 +173,18 @@ async function downloadGraphHostedContent(params: { const out: MSTeamsInboundMedia[] = []; for (const item of hosted.items) { const contentBytes = typeof item.contentBytes === "string" ? item.contentBytes : ""; - if (!contentBytes) continue; + if (!contentBytes) { + continue; + } let buffer: Buffer; try { buffer = Buffer.from(contentBytes, "base64"); } catch { continue; } - if (buffer.byteLength > params.maxBytes) continue; + if (buffer.byteLength > params.maxBytes) { + continue; + } const mime = await getMSTeamsRuntime().media.detectMime({ buffer, headerMime: item.contentType ?? undefined, @@ -199,7 +219,9 @@ export async function downloadMSTeamsGraphMedia(params: { /** When true, embeds original filename in stored path for later extraction. */ preserveFilenames?: boolean; }): Promise { - if (!params.messageUrl || !params.tokenProvider) return { media: [] }; + if (!params.messageUrl || !params.tokenProvider) { + return { media: [] }; + } const allowHosts = resolveAllowedHosts(params.allowHosts); const messageUrl = params.messageUrl; let accessToken: string; @@ -299,9 +321,13 @@ export async function downloadMSTeamsGraphMedia(params: { sharePointMedia.length > 0 ? normalizedAttachments.filter((att) => { const contentType = att.contentType?.toLowerCase(); - if (contentType !== "reference") return true; + if (contentType !== "reference") { + return true; + } const url = typeof att.contentUrl === "string" ? att.contentUrl : ""; - if (!url) return true; + if (!url) { + return true; + } return !downloadedReferenceUrls.has(url); }) : normalizedAttachments; diff --git a/extensions/msteams/src/attachments/html.ts b/extensions/msteams/src/attachments/html.ts index 821a3d23f..33c5d28a8 100644 --- a/extensions/msteams/src/attachments/html.ts +++ b/extensions/msteams/src/attachments/html.ts @@ -12,7 +12,9 @@ export function summarizeMSTeamsHtmlAttachments( attachments: MSTeamsAttachmentLike[] | undefined, ): MSTeamsHtmlAttachmentSummary | undefined { const list = Array.isArray(attachments) ? attachments : []; - if (list.length === 0) return undefined; + if (list.length === 0) { + return undefined; + } let htmlAttachments = 0; let imgTags = 0; let dataImages = 0; @@ -23,7 +25,9 @@ export function summarizeMSTeamsHtmlAttachments( for (const att of list) { const html = extractHtmlFromAttachment(att); - if (!html) continue; + if (!html) { + continue; + } htmlAttachments += 1; IMG_SRC_RE.lastIndex = 0; let match: RegExpExecArray | null = IMG_SRC_RE.exec(html); @@ -31,9 +35,13 @@ export function summarizeMSTeamsHtmlAttachments( imgTags += 1; const src = match[1]?.trim(); if (src) { - if (src.startsWith("data:")) dataImages += 1; - else if (src.startsWith("cid:")) cidImages += 1; - else srcHosts.add(safeHostForUrl(src)); + if (src.startsWith("data:")) { + dataImages += 1; + } else if (src.startsWith("cid:")) { + cidImages += 1; + } else { + srcHosts.add(safeHostForUrl(src)); + } } match = IMG_SRC_RE.exec(html); } @@ -43,12 +51,16 @@ export function summarizeMSTeamsHtmlAttachments( while (attachmentMatch) { attachmentTags += 1; const id = attachmentMatch[1]?.trim(); - if (id) attachmentIds.add(id); + if (id) { + attachmentIds.add(id); + } attachmentMatch = ATTACHMENT_TAG_RE.exec(html); } } - if (htmlAttachments === 0) return undefined; + if (htmlAttachments === 0) { + return undefined; + } return { htmlAttachments, imgTags, @@ -64,7 +76,9 @@ export function buildMSTeamsAttachmentPlaceholder( attachments: MSTeamsAttachmentLike[] | undefined, ): string { const list = Array.isArray(attachments) ? attachments : []; - if (list.length === 0) return ""; + if (list.length === 0) { + return ""; + } const imageCount = list.filter(isLikelyImageAttachment).length; const inlineCount = extractInlineImageCandidates(list).length; const totalImages = imageCount + inlineCount; diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index 8d290d87e..39a41e8d6 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -55,7 +55,9 @@ export function isRecord(value: unknown): value is Record { } export function normalizeContentType(value: unknown): string | undefined { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const trimmed = value.trim(); return trimmed ? trimmed : undefined; } @@ -78,17 +80,25 @@ export function inferPlaceholder(params: { export function isLikelyImageAttachment(att: MSTeamsAttachmentLike): boolean { const contentType = normalizeContentType(att.contentType) ?? ""; const name = typeof att.name === "string" ? att.name : ""; - if (contentType.startsWith("image/")) return true; - if (IMAGE_EXT_RE.test(name)) return true; + if (contentType.startsWith("image/")) { + return true; + } + if (IMAGE_EXT_RE.test(name)) { + return true; + } if ( contentType === "application/vnd.microsoft.teams.file.download.info" && isRecord(att.content) ) { const fileType = typeof att.content.fileType === "string" ? att.content.fileType : ""; - if (fileType && IMAGE_EXT_RE.test(`x.${fileType}`)) return true; + if (fileType && IMAGE_EXT_RE.test(`x.${fileType}`)) { + return true; + } const fileName = typeof att.content.fileName === "string" ? att.content.fileName : ""; - if (fileName && IMAGE_EXT_RE.test(fileName)) return true; + if (fileName && IMAGE_EXT_RE.test(fileName)) { + return true; + } } return false; @@ -124,9 +134,15 @@ function isHtmlAttachment(att: MSTeamsAttachmentLike): boolean { } export function extractHtmlFromAttachment(att: MSTeamsAttachmentLike): string | undefined { - if (!isHtmlAttachment(att)) return undefined; - if (typeof att.content === "string") return att.content; - if (!isRecord(att.content)) return undefined; + if (!isHtmlAttachment(att)) { + return undefined; + } + if (typeof att.content === "string") { + return att.content; + } + if (!isRecord(att.content)) { + return undefined; + } const text = typeof att.content.text === "string" ? att.content.text @@ -140,12 +156,18 @@ export function extractHtmlFromAttachment(att: MSTeamsAttachmentLike): string | function decodeDataImage(src: string): InlineImageCandidate | null { const match = /^data:(image\/[a-z0-9.+-]+)?(;base64)?,(.*)$/i.exec(src); - if (!match) return null; + if (!match) { + return null; + } const contentType = match[1]?.toLowerCase(); const isBase64 = Boolean(match[2]); - if (!isBase64) return null; + if (!isBase64) { + return null; + } const payload = match[3] ?? ""; - if (!payload) return null; + if (!payload) { + return null; + } try { const data = Buffer.from(payload, "base64"); return { kind: "data", data, contentType, placeholder: "" }; @@ -170,7 +192,9 @@ export function extractInlineImageCandidates( const out: InlineImageCandidate[] = []; for (const att of attachments) { const html = extractHtmlFromAttachment(att); - if (!html) continue; + if (!html) { + continue; + } IMG_SRC_RE.lastIndex = 0; let match: RegExpExecArray | null = IMG_SRC_RE.exec(html); while (match) { @@ -178,7 +202,9 @@ export function extractInlineImageCandidates( if (src && !src.startsWith("cid:")) { if (src.startsWith("data:")) { const decoded = decodeDataImage(src); - if (decoded) out.push(decoded); + if (decoded) { + out.push(decoded); + } } else { out.push({ kind: "url", @@ -204,8 +230,12 @@ export function safeHostForUrl(url: string): string { function normalizeAllowHost(value: string): string { const trimmed = value.trim().toLowerCase(); - if (!trimmed) return ""; - if (trimmed === "*") return "*"; + if (!trimmed) { + return ""; + } + if (trimmed === "*") { + return "*"; + } return trimmed.replace(/^\*\.?/, ""); } @@ -214,12 +244,16 @@ export function resolveAllowedHosts(input?: string[]): string[] { return DEFAULT_MEDIA_HOST_ALLOWLIST.slice(); } const normalized = input.map(normalizeAllowHost).filter(Boolean); - if (normalized.includes("*")) return ["*"]; + if (normalized.includes("*")) { + return ["*"]; + } return normalized; } function isHostAllowed(host: string, allowlist: string[]): boolean { - if (allowlist.includes("*")) return true; + if (allowlist.includes("*")) { + return true; + } const normalized = host.toLowerCase(); return allowlist.some((entry) => normalized === entry || normalized.endsWith(`.${entry}`)); } @@ -227,7 +261,9 @@ function isHostAllowed(host: string, allowlist: string[]): boolean { export function isUrlAllowed(url: string, allowlist: string[]): boolean { try { const parsed = new URL(url); - if (parsed.protocol !== "https:") return false; + if (parsed.protocol !== "https:") { + return false; + } return isHostAllowed(parsed.hostname, allowlist); } catch { return false; diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 4de0944bf..4442f4519 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -126,7 +126,9 @@ export const msteamsPlugin: ChannelPlugin = { collectWarnings: ({ cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = cfg.channels?.msteams?.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ `- MS Teams groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.msteams.groupPolicy="allowlist" + channels.msteams.groupAllowFrom to restrict senders.`, ]; @@ -150,8 +152,12 @@ export const msteamsPlugin: ChannelPlugin = { targetResolver: { looksLikeId: (raw) => { const trimmed = raw.trim(); - if (!trimmed) return false; - if (/^conversation:/i.test(trimmed)) return true; + if (!trimmed) { + return false; + } + if (/^conversation:/i.test(trimmed)) { + return true; + } if (/^user:/i.test(trimmed)) { // Only treat as ID if the value after user: looks like a UUID const id = trimmed.slice("user:".length).trim(); @@ -169,11 +175,15 @@ export const msteamsPlugin: ChannelPlugin = { const ids = new Set(); for (const entry of cfg.channels?.msteams?.allowFrom ?? []) { const trimmed = String(entry).trim(); - if (trimmed && trimmed !== "*") ids.add(trimmed); + if (trimmed && trimmed !== "*") { + ids.add(trimmed); + } } for (const userId of Object.keys(cfg.channels?.msteams?.dms ?? {})) { const trimmed = userId.trim(); - if (trimmed) ids.add(trimmed); + if (trimmed) { + ids.add(trimmed); + } } return Array.from(ids) .map((raw) => raw.trim()) @@ -181,8 +191,12 @@ export const msteamsPlugin: ChannelPlugin = { .map((raw) => normalizeMSTeamsMessagingTarget(raw) ?? raw) .map((raw) => { const lowered = raw.toLowerCase(); - if (lowered.startsWith("user:")) return raw; - if (lowered.startsWith("conversation:")) return raw; + if (lowered.startsWith("user:")) { + return raw; + } + if (lowered.startsWith("conversation:")) { + return raw; + } return `user:${raw}`; }) .filter((id) => (q ? id.toLowerCase().includes(q) : true)) @@ -195,7 +209,9 @@ export const msteamsPlugin: ChannelPlugin = { for (const team of Object.values(cfg.channels?.msteams?.teams ?? {})) { for (const channelId of Object.keys(team.channels ?? {})) { const trimmed = channelId.trim(); - if (trimmed && trimmed !== "*") ids.add(trimmed); + if (trimmed && trimmed !== "*") { + ids.add(trimmed); + } } } return Array.from(ids) @@ -249,7 +265,9 @@ export const msteamsPlugin: ChannelPlugin = { }); resolved.forEach((entry, idx) => { const target = results[pending[idx]?.index ?? -1]; - if (!target) return; + if (!target) { + return; + } target.resolved = entry.resolved; target.id = entry.id; target.name = entry.name; @@ -259,7 +277,9 @@ export const msteamsPlugin: ChannelPlugin = { runtime.error?.(`msteams resolve failed: ${String(err)}`); pending.forEach(({ index }) => { const entry = results[index]; - if (entry) entry.note = "lookup failed"; + if (entry) { + entry.note = "lookup failed"; + } }); } } @@ -298,7 +318,9 @@ export const msteamsPlugin: ChannelPlugin = { }); resolved.forEach((entry, idx) => { const target = results[pending[idx]?.index ?? -1]; - if (!target) return; + if (!target) { + return; + } if (!entry.resolved || !entry.teamId) { target.resolved = false; target.note = entry.note; @@ -316,13 +338,17 @@ export const msteamsPlugin: ChannelPlugin = { target.name = entry.teamName; target.note = "team id"; } - if (entry.note) target.note = entry.note; + if (entry.note) { + target.note = entry.note; + } }); } catch (err) { runtime.error?.(`msteams resolve failed: ${String(err)}`); pending.forEach(({ index }) => { const entry = results[index]; - if (entry) entry.note = "lookup failed"; + if (entry) { + entry.note = "lookup failed"; + } }); } } @@ -335,7 +361,9 @@ export const msteamsPlugin: ChannelPlugin = { const enabled = cfg.channels?.msteams?.enabled !== false && Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)); - if (!enabled) return []; + if (!enabled) { + return []; + } return ["poll"] satisfies ChannelMessageActionName[]; }, supportsCards: ({ cfg }) => { diff --git a/extensions/msteams/src/conversation-store-fs.test.ts b/extensions/msteams/src/conversation-store-fs.test.ts index 55a2c3bb0..59c30897d 100644 --- a/extensions/msteams/src/conversation-store-fs.test.ts +++ b/extensions/msteams/src/conversation-store-fs.test.ts @@ -13,7 +13,9 @@ const runtimeStub = { state: { resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => { const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim(); - if (override) return override; + if (override) { + return override; + } const resolvedHome = homedir ? homedir() : os.homedir(); return path.join(resolvedHome, ".openclaw"); }, @@ -66,7 +68,7 @@ describe("msteams conversation store (fs)", () => { await fs.promises.writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`); const list = await store.list(); - const ids = list.map((e) => e.conversationId).sort(); + const ids = list.map((e) => e.conversationId).toSorted(); expect(ids).toEqual(["19:active@thread.tacv2", "19:legacy@thread.tacv2"]); expect(await store.get("19:old@thread.tacv2")).toBeNull(); @@ -79,7 +81,7 @@ describe("msteams conversation store (fs)", () => { const rawAfter = await fs.promises.readFile(filePath, "utf-8"); const jsonAfter = JSON.parse(rawAfter) as typeof json; - expect(Object.keys(jsonAfter.conversations).sort()).toEqual([ + expect(Object.keys(jsonAfter.conversations).toSorted()).toEqual([ "19:active@thread.tacv2", "19:legacy@thread.tacv2", "19:new@thread.tacv2", diff --git a/extensions/msteams/src/conversation-store-fs.ts b/extensions/msteams/src/conversation-store-fs.ts index b2d3a97a7..8257114fc 100644 --- a/extensions/msteams/src/conversation-store-fs.ts +++ b/extensions/msteams/src/conversation-store-fs.ts @@ -16,9 +16,13 @@ const MAX_CONVERSATIONS = 1000; const CONVERSATION_TTL_MS = 365 * 24 * 60 * 60 * 1000; function parseTimestamp(value: string | undefined): number | null { - if (!value) return null; + if (!value) { + return null; + } const parsed = Date.parse(value); - if (!Number.isFinite(parsed)) return null; + if (!Number.isFinite(parsed)) { + return null; + } return parsed; } @@ -26,7 +30,9 @@ function pruneToLimit( conversations: Record, ) { const entries = Object.entries(conversations); - if (entries.length <= MAX_CONVERSATIONS) return conversations; + if (entries.length <= MAX_CONVERSATIONS) { + return conversations; + } entries.sort((a, b) => { const aTs = parseTimestamp(a[1].lastSeenAt) ?? 0; @@ -109,7 +115,9 @@ export function createMSTeamsConversationStoreFs(params?: { const findByUserId = async (id: string): Promise => { const target = id.trim(); - if (!target) return null; + if (!target) { + return null; + } for (const entry of await list()) { const { conversationId, reference } = entry; if (reference.user?.aadObjectId === target) { @@ -144,7 +152,9 @@ export function createMSTeamsConversationStoreFs(params?: { const normalizedId = normalizeConversationId(conversationId); return await withFileLock(filePath, empty, async () => { const store = await readStore(); - if (!(normalizedId in store.conversations)) return false; + if (!(normalizedId in store.conversations)) { + return false; + } delete store.conversations[normalizedId]; await writeJsonFile(filePath, store); return true; diff --git a/extensions/msteams/src/conversation-store-memory.ts b/extensions/msteams/src/conversation-store-memory.ts index 098f09bb6..c03ee6e7c 100644 --- a/extensions/msteams/src/conversation-store-memory.ts +++ b/extensions/msteams/src/conversation-store-memory.ts @@ -30,7 +30,9 @@ export function createMSTeamsConversationStoreMemory( }, findByUserId: async (id) => { const target = id.trim(); - if (!target) return null; + if (!target) { + return null; + } for (const [conversationId, reference] of map.entries()) { if (reference.user?.aadObjectId === target) { return { conversationId, reference }; diff --git a/extensions/msteams/src/directory-live.ts b/extensions/msteams/src/directory-live.ts index da9c60738..6608b9b70 100644 --- a/extensions/msteams/src/directory-live.ts +++ b/extensions/msteams/src/directory-live.ts @@ -24,7 +24,9 @@ type GraphChannel = { type GraphResponse = { value?: T[] }; function readAccessToken(value: unknown): string | null { - if (typeof value === "string") return value; + if (typeof value === "string") { + return value; + } if (value && typeof value === "object") { const token = (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token; @@ -49,7 +51,7 @@ async function fetchGraphJson(params: { const res = await fetch(`${GRAPH_ROOT}${params.path}`, { headers: { Authorization: `Bearer ${params.token}`, - ...(params.headers ?? {}), + ...params.headers, }, }); if (!res.ok) { @@ -63,12 +65,16 @@ async function resolveGraphToken(cfg: unknown): Promise { const creds = resolveMSTeamsCredentials( (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams, ); - if (!creds) throw new Error("MS Teams credentials missing"); + if (!creds) { + throw new Error("MS Teams credentials missing"); + } const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds); const tokenProvider = new sdk.MsalTokenProvider(authConfig); const token = await tokenProvider.getAccessToken("https://graph.microsoft.com"); const accessToken = readAccessToken(token); - if (!accessToken) throw new Error("MS Teams graph token unavailable"); + if (!accessToken) { + throw new Error("MS Teams graph token unavailable"); + } return accessToken; } @@ -92,7 +98,9 @@ export async function listMSTeamsDirectoryPeersLive(params: { limit?: number | null; }): Promise { const query = normalizeQuery(params.query); - if (!query) return []; + if (!query) { + return []; + } const token = await resolveGraphToken(params.cfg); const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20; @@ -116,7 +124,9 @@ export async function listMSTeamsDirectoryPeersLive(params: { return users .map((user) => { const id = user.id?.trim(); - if (!id) return null; + if (!id) { + return null; + } const name = user.displayName?.trim(); const handle = user.userPrincipalName?.trim() || user.mail?.trim(); return { @@ -136,7 +146,9 @@ export async function listMSTeamsDirectoryGroupsLive(params: { limit?: number | null; }): Promise { const rawQuery = normalizeQuery(params.query); - if (!rawQuery) return []; + if (!rawQuery) { + return []; + } const token = await resolveGraphToken(params.cfg); const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20; const [teamQuery, channelQuery] = rawQuery.includes("/") @@ -151,7 +163,9 @@ export async function listMSTeamsDirectoryGroupsLive(params: { for (const team of teams) { const teamId = team.id?.trim(); - if (!teamId) continue; + if (!teamId) { + continue; + } const teamName = team.displayName?.trim() || teamQuery; if (!channelQuery) { results.push({ @@ -161,14 +175,20 @@ export async function listMSTeamsDirectoryGroupsLive(params: { handle: teamName ? `#${teamName}` : undefined, raw: team, }); - if (results.length >= limit) return results; + if (results.length >= limit) { + return results; + } continue; } const channels = await listChannelsForTeam(token, teamId); for (const channel of channels) { const name = channel.displayName?.trim(); - if (!name) continue; - if (!name.toLowerCase().includes(channelQuery.toLowerCase())) continue; + if (!name) { + continue; + } + if (!name.toLowerCase().includes(channelQuery.toLowerCase())) { + continue; + } results.push({ kind: "group", id: `conversation:${channel.id}`, @@ -176,7 +196,9 @@ export async function listMSTeamsDirectoryGroupsLive(params: { handle: `#${name}`, raw: channel, }); - if (results.length >= limit) return results; + if (results.length >= limit) { + return results; + } } } diff --git a/extensions/msteams/src/errors.ts b/extensions/msteams/src/errors.ts index e7782a22d..6512f6ca3 100644 --- a/extensions/msteams/src/errors.ts +++ b/extensions/msteams/src/errors.ts @@ -1,12 +1,22 @@ export function formatUnknownError(err: unknown): string { - if (err instanceof Error) return err.message; - if (typeof err === "string") return err; - if (err === null) return "null"; - if (err === undefined) return "undefined"; + if (err instanceof Error) { + return err.message; + } + if (typeof err === "string") { + return err; + } + if (err === null) { + return "null"; + } + if (err === undefined) { + return "undefined"; + } if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { return String(err); } - if (typeof err === "symbol") return err.description ?? err.toString(); + if (typeof err === "symbol") { + return err.description ?? err.toString(); + } if (typeof err === "function") { return err.name ? `[function ${err.name}]` : "[function]"; } @@ -22,21 +32,31 @@ function isRecord(value: unknown): value is Record { } function extractStatusCode(err: unknown): number | null { - if (!isRecord(err)) return null; + if (!isRecord(err)) { + return null; + } const direct = err.statusCode ?? err.status; - if (typeof direct === "number" && Number.isFinite(direct)) return direct; + if (typeof direct === "number" && Number.isFinite(direct)) { + return direct; + } if (typeof direct === "string") { const parsed = Number.parseInt(direct, 10); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } const response = err.response; if (isRecord(response)) { const status = response.status; - if (typeof status === "number" && Number.isFinite(status)) return status; + if (typeof status === "number" && Number.isFinite(status)) { + return status; + } if (typeof status === "string") { const parsed = Number.parseInt(status, 10); - if (Number.isFinite(parsed)) return parsed; + if (Number.isFinite(parsed)) { + return parsed; + } } } @@ -44,7 +64,9 @@ function extractStatusCode(err: unknown): number | null { } function extractRetryAfterMs(err: unknown): number | null { - if (!isRecord(err)) return null; + if (!isRecord(err)) { + return null; + } const direct = err.retryAfterMs ?? err.retry_after_ms; if (typeof direct === "number" && Number.isFinite(direct) && direct >= 0) { @@ -57,20 +79,28 @@ function extractRetryAfterMs(err: unknown): number | null { } if (typeof retryAfter === "string") { const parsed = Number.parseFloat(retryAfter); - if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1000; + if (Number.isFinite(parsed) && parsed >= 0) { + return parsed * 1000; + } } const response = err.response; - if (!isRecord(response)) return null; + if (!isRecord(response)) { + return null; + } const headers = response.headers; - if (!headers) return null; + if (!headers) { + return null; + } if (isRecord(headers)) { const raw = headers["retry-after"] ?? headers["Retry-After"]; if (typeof raw === "string") { const parsed = Number.parseFloat(raw); - if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1000; + if (Number.isFinite(parsed) && parsed >= 0) { + return parsed * 1000; + } } } @@ -84,7 +114,9 @@ function extractRetryAfterMs(err: unknown): number | null { const raw = (headers as { get: (name: string) => string | null }).get("retry-after"); if (raw) { const parsed = Number.parseFloat(raw); - if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1000; + if (Number.isFinite(parsed) && parsed >= 0) { + return parsed * 1000; + } } } diff --git a/extensions/msteams/src/file-consent.ts b/extensions/msteams/src/file-consent.ts index 02183e7a2..268e82fff 100644 --- a/extensions/msteams/src/file-consent.ts +++ b/extensions/msteams/src/file-consent.ts @@ -78,7 +78,9 @@ export function parseFileConsentInvoke(activity: { name?: string; value?: unknown; }): FileConsentResponse | null { - if (activity.name !== "fileConsent/invoke") return null; + if (activity.name !== "fileConsent/invoke") { + return null; + } const value = activity.value as { type?: string; @@ -87,7 +89,9 @@ export function parseFileConsentInvoke(activity: { context?: Record; }; - if (value?.type !== "fileUpload") return null; + if (value?.type !== "fileUpload") { + return null; + } return { action: value.action === "accept" ? "accept" : "decline", diff --git a/extensions/msteams/src/inbound.ts b/extensions/msteams/src/inbound.ts index 84e1cd259..88e6c19a4 100644 --- a/extensions/msteams/src/inbound.ts +++ b/extensions/msteams/src/inbound.ts @@ -11,16 +11,24 @@ export function normalizeMSTeamsConversationId(raw: string): string { } export function extractMSTeamsConversationMessageId(raw: string): string | undefined { - if (!raw) return undefined; + if (!raw) { + return undefined; + } const match = /(?:^|;)messageid=([^;]+)/i.exec(raw); const value = match?.[1]?.trim() ?? ""; return value || undefined; } export function parseMSTeamsActivityTimestamp(value: unknown): Date | undefined { - if (!value) return undefined; - if (value instanceof Date) return value; - if (typeof value !== "string") return undefined; + if (!value) { + return undefined; + } + if (value instanceof Date) { + return value; + } + if (typeof value !== "string") { + return undefined; + } const date = new Date(value); return Number.isNaN(date.getTime()) ? undefined : date; } @@ -32,7 +40,9 @@ export function stripMSTeamsMentionTags(text: string): string { export function wasMSTeamsBotMentioned(activity: MentionableActivity): boolean { const botId = activity.recipient?.id; - if (!botId) return false; + if (!botId) { + return false; + } const entities = activity.entities ?? []; return entities.some((e) => e.type === "mention" && e.mentioned?.id === botId); } diff --git a/extensions/msteams/src/media-helpers.ts b/extensions/msteams/src/media-helpers.ts index e6ae4ca79..da1464258 100644 --- a/extensions/msteams/src/media-helpers.ts +++ b/extensions/msteams/src/media-helpers.ts @@ -19,7 +19,9 @@ export async function getMimeType(url: string): Promise { // Handle data URLs: data:image/png;base64,... if (url.startsWith("data:")) { const match = url.match(/^data:([^;,]+)/); - if (match?.[1]) return match[1]; + if (match?.[1]) { + return match[1]; + } } // Use shared MIME detection (extension-based for URLs) @@ -46,7 +48,9 @@ export async function extractFilename(url: string): Promise { const pathname = new URL(url).pathname; const basename = path.basename(pathname); const existingExt = getFileExtension(pathname); - if (basename && existingExt) return basename; + if (basename && existingExt) { + return basename; + } // No extension in URL, derive from MIME const mime = await getMimeType(url); const ext = extensionForMime(mime) ?? ".bin"; @@ -69,9 +73,15 @@ export function isLocalPath(url: string): boolean { * Extract the message ID from a Bot Framework response. */ export function extractMessageId(response: unknown): string | null { - if (!response || typeof response !== "object") return null; - if (!("id" in response)) return null; + if (!response || typeof response !== "object") { + return null; + } + if (!("id" in response)) { + return null; + } const { id } = response as { id?: unknown }; - if (typeof id !== "string" || !id) return null; + if (typeof id !== "string" || !id) { + return null; + } return id; } diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 5aef95c64..7aa47907a 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -10,8 +10,12 @@ import { import { setMSTeamsRuntime } from "./runtime.js"; const chunkMarkdownText = (text: string, limit: number) => { - if (!text) return []; - if (limit <= 0 || text.length <= limit) return [text]; + if (!text) { + return []; + } + if (limit <= 0 || text.length <= limit) { + return [text]; + } const chunks: string[] = []; for (let index = 0; index < text.length; index += limit) { chunks.push(text.slice(index, index + limit)); diff --git a/extensions/msteams/src/messenger.ts b/extensions/msteams/src/messenger.ts index dd51335db..44b1e8363 100644 --- a/extensions/msteams/src/messenger.ts +++ b/extensions/msteams/src/messenger.ts @@ -134,7 +134,9 @@ function pushTextMessages( chunkMode: ChunkMode; }, ) { - if (!text) return; + if (!text) { + return; + } if (opts.chunkText) { for (const chunk of getMSTeamsRuntime().channel.text.chunkMarkdownTextWithMode( text, @@ -142,25 +144,33 @@ function pushTextMessages( opts.chunkMode, )) { const trimmed = chunk.trim(); - if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) continue; + if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) { + continue; + } out.push({ text: trimmed }); } return; } const trimmed = text.trim(); - if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) return; + if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) { + return; + } out.push({ text: trimmed }); } function clampMs(value: number, maxMs: number): number { - if (!Number.isFinite(value) || value < 0) return 0; + if (!Number.isFinite(value) || value < 0) { + return 0; + } return Math.min(value, maxMs); } async function sleep(ms: number): Promise { const delay = Math.max(0, ms); - if (delay === 0) return; + if (delay === 0) { + return; + } await new Promise((resolve) => { setTimeout(resolve, delay); }); @@ -219,7 +229,9 @@ export function renderReplyPayloadsToMessages( tableMode, ); - if (!text && mediaList.length === 0) continue; + if (!text && mediaList.length === 0) { + continue; + } if (mediaList.length === 0) { pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode }); @@ -233,7 +245,9 @@ export function renderReplyPayloadsToMessages( out.push({ text: text || undefined, mediaUrl: firstMedia }); // Additional media URLs as separate messages for (let i = 1; i < mediaList.length; i++) { - if (mediaList[i]) out.push({ mediaUrl: mediaList[i] }); + if (mediaList[i]) { + out.push({ mediaUrl: mediaList[i] }); + } } } else { pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode }); @@ -244,7 +258,9 @@ export function renderReplyPayloadsToMessages( // mediaMode === "split" pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode }); for (const mediaUrl of mediaList) { - if (!mediaUrl) continue; + if (!mediaUrl) { + continue; + } out.push({ mediaUrl }); } } @@ -383,7 +399,9 @@ export async function sendMSTeamsMessages(params: { const messages = params.messages.filter( (m) => (m.text && m.text.trim().length > 0) || m.mediaUrl, ); - if (messages.length === 0) return []; + if (messages.length === 0) { + return []; + } const retryOptions = resolveRetryOptions(params.retry); @@ -391,7 +409,9 @@ export async function sendMSTeamsMessages(params: { sendOnce: () => Promise, meta: { messageIndex: number; messageCount: number }, ): Promise => { - if (!retryOptions.enabled) return await sendOnce(); + if (!retryOptions.enabled) { + return await sendOnce(); + } let attempt = 1; while (true) { @@ -400,7 +420,9 @@ export async function sendMSTeamsMessages(params: { } catch (err) { const classification = classifyMSTeamsSendError(err); const canRetry = attempt < retryOptions.maxAttempts && shouldRetry(classification); - if (!canRetry) throw err; + if (!canRetry) { + throw err; + } const delayMs = computeRetryDelayMs(attempt, classification, retryOptions); const nextAttempt = attempt + 1; diff --git a/extensions/msteams/src/monitor-handler/inbound-media.ts b/extensions/msteams/src/monitor-handler/inbound-media.ts index 03398b5a0..2fba00172 100644 --- a/extensions/msteams/src/monitor-handler/inbound-media.ts +++ b/extensions/msteams/src/monitor-handler/inbound-media.ts @@ -99,7 +99,9 @@ export async function resolveMSTeamsInboundMedia(params: { mediaList = graphMedia.media; break; } - if (graphMedia.tokenError) break; + if (graphMedia.tokenError) { + break; + } } if (mediaList.length === 0) { log.debug("graph media fetch empty", { attempts }); diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 93753d6fb..a7b8b8205 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -105,7 +105,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { from: from?.id, conversation: conversation?.id, }); - if (htmlSummary) log.debug("html attachment summary", htmlSummary); + if (htmlSummary) { + log.debug("html attachment summary", htmlSummary); + } if (!from?.id) { log.debug("skipping message without from.id"); @@ -520,7 +522,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { markDispatchIdle(); log.info("dispatch complete", { queuedFinal, counts }); - const didSendReply = counts.final + counts.tool + counts.block > 0; if (!queuedFinal) { if (isRoomish && historyKey) { clearHistoryEntriesIfEnabled({ @@ -563,17 +564,25 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { ); const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? ""; - if (!senderId || !conversationId) return null; + if (!senderId || !conversationId) { + return null; + } return `msteams:${appId}:${conversationId}:${senderId}`; }, shouldDebounce: (entry) => { - if (!entry.text.trim()) return false; - if (entry.attachments.length > 0) return false; + if (!entry.text.trim()) { + return false; + } + if (entry.attachments.length > 0) { + return false; + } return !core.channel.text.hasControlCommand(entry.text, cfg); }, onFlush: async (entries) => { const last = entries.at(-1); - if (!last) return; + if (!last) { + return; + } if (entries.length === 1) { await handleTeamsMessageNow(last); return; @@ -582,7 +591,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { .map((entry) => entry.text) .filter(Boolean) .join("\n"); - if (!combinedText.trim()) return; + if (!combinedText.trim()) { + return; + } const combinedRawText = entries .map((entry) => entry.rawText) .filter(Boolean) diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index e8c9bb5eb..ba9c55b16 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -70,7 +70,9 @@ export async function monitorMSTeamsProvider( .trim(); const resolveAllowlistUsers = async (label: string, entries: string[]) => { - if (entries.length === 0) return { additions: [], unresolved: [] }; + if (entries.length === 0) { + return { additions: [], unresolved: [] }; + } const resolved = await resolveMSTeamsUserAllowlist({ cfg, entries }); const additions: string[] = []; const unresolved: string[] = []; @@ -111,7 +113,9 @@ export async function monitorMSTeamsProvider( if (teamsConfig && Object.keys(teamsConfig).length > 0) { const entries: Array<{ input: string; teamKey: string; channelKey?: string }> = []; for (const [teamKey, teamCfg] of Object.entries(teamsConfig)) { - if (teamKey === "*") continue; + if (teamKey === "*") { + continue; + } const channels = teamCfg?.channels ?? {}; const channelKeys = Object.keys(channels).filter((key) => key !== "*"); if (channelKeys.length === 0) { @@ -134,11 +138,13 @@ export async function monitorMSTeamsProvider( }); const mapping: string[] = []; const unresolved: string[] = []; - const nextTeams = { ...(teamsConfig ?? {}) }; + const nextTeams = { ...teamsConfig }; resolved.forEach((entry, idx) => { const source = entries[idx]; - if (!source) return; + if (!source) { + return; + } const sourceTeam = teamsConfig?.[source.teamKey] ?? {}; if (!entry.resolved || !entry.teamId) { unresolved.push(entry.input); @@ -151,8 +157,8 @@ export async function monitorMSTeamsProvider( ); const existing = nextTeams[entry.teamId] ?? {}; const mergedChannels = { - ...(sourceTeam.channels ?? {}), - ...(existing.channels ?? {}), + ...sourceTeam.channels, + ...existing.channels, }; const mergedTeam = { ...sourceTeam, ...existing, channels: mergedChannels }; nextTeams[entry.teamId] = mergedTeam; @@ -165,7 +171,7 @@ export async function monitorMSTeamsProvider( ...mergedChannels, [entry.channelId]: { ...sourceChannel, - ...(mergedChannels?.[entry.channelId] ?? {}), + ...mergedChannels?.[entry.channelId], }, }, }; @@ -239,9 +245,8 @@ export async function monitorMSTeamsProvider( // Set up the messages endpoint - use configured path and /api/messages as fallback const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages"; const messageHandler = (req: Request, res: Response) => { - type HandlerContext = Parameters<(typeof handler)["run"]>[0]; void adapter - .process(req, res, (context: unknown) => handler.run(context as HandlerContext)) + .process(req, res, (context: unknown) => handler.run(context)) .catch((err: unknown) => { log.error("msteams webhook failed", { error: formatUnknownError(err) }); }); diff --git a/extensions/msteams/src/onboarding.ts b/extensions/msteams/src/onboarding.ts index dc1a7fe69..eb379bda3 100644 --- a/extensions/msteams/src/onboarding.ts +++ b/extensions/msteams/src/onboarding.ts @@ -166,10 +166,12 @@ function setMSTeamsTeamsAllowlist( const teams: Record }> = { ...baseTeams }; for (const entry of entries) { const teamKey = entry.teamKey; - if (!teamKey) continue; + if (!teamKey) { + continue; + } const existing = teams[teamKey] ?? {}; if (entry.channelKey) { - const channels = { ...(existing.channels ?? {}) }; + const channels = { ...existing.channels }; channels[entry.channelKey] = channels[entry.channelKey] ?? {}; teams[teamKey] = { ...existing, channels }; } else { @@ -334,7 +336,9 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = { ([teamKey, value]) => { const channels = value?.channels ?? {}; const channelKeys = Object.keys(channels); - if (channelKeys.length === 0) return [teamKey]; + if (channelKeys.length === 0) { + return [teamKey]; + } return channelKeys.map((channelKey) => `${teamKey}/${channelKey}`); }, ); diff --git a/extensions/msteams/src/pending-uploads.ts b/extensions/msteams/src/pending-uploads.ts index 12646a48b..d879008d1 100644 --- a/extensions/msteams/src/pending-uploads.ts +++ b/extensions/msteams/src/pending-uploads.ts @@ -48,9 +48,13 @@ export function storePendingUpload(upload: Omit PENDING_UPLOAD_TTL_MS) { diff --git a/extensions/msteams/src/policy.ts b/extensions/msteams/src/policy.ts index d504bca6b..eb1e74762 100644 --- a/extensions/msteams/src/policy.ts +++ b/extensions/msteams/src/policy.ts @@ -93,7 +93,9 @@ export function resolveMSTeamsGroupToolPolicy( params: ChannelGroupContext, ): GroupToolPolicyConfig | undefined { const cfg = params.cfg.channels?.msteams; - if (!cfg) return undefined; + if (!cfg) { + return undefined; + } const groupId = params.groupId?.trim(); const groupChannel = params.groupChannel?.trim(); const groupSpace = params.groupSpace?.trim(); @@ -114,8 +116,12 @@ export function resolveMSTeamsGroupToolPolicy( senderUsername: params.senderUsername, senderE164: params.senderE164, }); - if (senderPolicy) return senderPolicy; - if (resolved.channelConfig.tools) return resolved.channelConfig.tools; + if (senderPolicy) { + return senderPolicy; + } + if (resolved.channelConfig.tools) { + return resolved.channelConfig.tools; + } const teamSenderPolicy = resolveToolsBySender({ toolsBySender: resolved.teamConfig?.toolsBySender, senderId: params.senderId, @@ -123,7 +129,9 @@ export function resolveMSTeamsGroupToolPolicy( senderUsername: params.senderUsername, senderE164: params.senderE164, }); - if (teamSenderPolicy) return teamSenderPolicy; + if (teamSenderPolicy) { + return teamSenderPolicy; + } return resolved.teamConfig?.tools; } if (resolved.teamConfig) { @@ -134,11 +142,17 @@ export function resolveMSTeamsGroupToolPolicy( senderUsername: params.senderUsername, senderE164: params.senderE164, }); - if (teamSenderPolicy) return teamSenderPolicy; - if (resolved.teamConfig.tools) return resolved.teamConfig.tools; + if (teamSenderPolicy) { + return teamSenderPolicy; + } + if (resolved.teamConfig.tools) { + return resolved.teamConfig.tools; + } } - if (!groupId) return undefined; + if (!groupId) { + return undefined; + } const channelCandidates = buildChannelKeyCandidates( groupId, @@ -160,8 +174,12 @@ export function resolveMSTeamsGroupToolPolicy( senderUsername: params.senderUsername, senderE164: params.senderE164, }); - if (senderPolicy) return senderPolicy; - if (match.entry.tools) return match.entry.tools; + if (senderPolicy) { + return senderPolicy; + } + if (match.entry.tools) { + return match.entry.tools; + } const teamSenderPolicy = resolveToolsBySender({ toolsBySender: teamConfig?.toolsBySender, senderId: params.senderId, @@ -169,7 +187,9 @@ export function resolveMSTeamsGroupToolPolicy( senderUsername: params.senderUsername, senderE164: params.senderE164, }); - if (teamSenderPolicy) return teamSenderPolicy; + if (teamSenderPolicy) { + return teamSenderPolicy; + } return teamConfig?.tools; } } @@ -192,7 +212,9 @@ export function resolveMSTeamsAllowlistMatch(params: { const allowFrom = params.allowFrom .map((entry) => String(entry).trim().toLowerCase()) .filter(Boolean); - if (allowFrom.length === 0) return { allowed: false }; + if (allowFrom.length === 0) { + return { allowed: false }; + } if (allowFrom.includes("*")) { return { allowed: true, matchKey: "*", matchSource: "wildcard" }; } @@ -241,7 +263,11 @@ export function isMSTeamsGroupAllowed(params: { senderName?: string | null; }): boolean { const { groupPolicy } = params; - if (groupPolicy === "disabled") return false; - if (groupPolicy === "open") return true; + if (groupPolicy === "disabled") { + return false; + } + if (groupPolicy === "open") { + return true; + } return resolveMSTeamsAllowlistMatch(params).allowed; } diff --git a/extensions/msteams/src/polls-store-memory.ts b/extensions/msteams/src/polls-store-memory.ts index a4f0c183d..d3fc7b11a 100644 --- a/extensions/msteams/src/polls-store-memory.ts +++ b/extensions/msteams/src/polls-store-memory.ts @@ -18,7 +18,9 @@ export function createMSTeamsPollStoreMemory(initial: MSTeamsPoll[] = []): MSTea const recordVote = async (params: { pollId: string; voterId: string; selections: string[] }) => { const poll = polls.get(params.pollId); - if (!poll) return null; + if (!poll) { + return null; + } const normalized = normalizeMSTeamsPollSelections(poll, params.selections); poll.votes[params.voterId] = normalized; poll.updatedAt = new Date().toISOString(); diff --git a/extensions/msteams/src/polls.test.ts b/extensions/msteams/src/polls.test.ts index 48c2303ba..a3b84cd84 100644 --- a/extensions/msteams/src/polls.test.ts +++ b/extensions/msteams/src/polls.test.ts @@ -12,7 +12,9 @@ const runtimeStub = { state: { resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => { const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim(); - if (override) return override; + if (override) { + return override; + } const resolvedHome = homedir ? homedir() : os.homedir(); return path.join(resolvedHome, ".openclaw"); }, diff --git a/extensions/msteams/src/polls.ts b/extensions/msteams/src/polls.ts index 708a0c0be..2159b4fcc 100644 --- a/extensions/msteams/src/polls.ts +++ b/extensions/msteams/src/polls.ts @@ -67,7 +67,9 @@ function extractSelections(value: unknown): string[] { return value.map(normalizeChoiceValue).filter((entry): entry is string => Boolean(entry)); } const normalized = normalizeChoiceValue(value); - if (!normalized) return []; + if (!normalized) { + return []; + } if (normalized.includes(",")) { return normalized .split(",") @@ -80,7 +82,9 @@ function extractSelections(value: unknown): string[] { function readNestedValue(value: unknown, keys: Array): unknown { let current: unknown = value; for (const key of keys) { - if (!isRecord(current)) return undefined; + if (!isRecord(current)) { + return undefined; + } current = current[key as keyof typeof current]; } return current; @@ -95,7 +99,9 @@ export function extractMSTeamsPollVote( activity: { value?: unknown } | undefined, ): MSTeamsPollVote | null { const value = activity?.value; - if (!value || !isRecord(value)) return null; + if (!value || !isRecord(value)) { + return null; + } const pollId = readNestedString(value, ["openclawPollId"]) ?? readNestedString(value, ["pollId"]) ?? @@ -104,7 +110,9 @@ export function extractMSTeamsPollVote( readNestedString(value, ["data", "openclawPollId"]) ?? readNestedString(value, ["data", "pollId"]) ?? readNestedString(value, ["data", "openclaw", "pollId"]); - if (!pollId) return null; + if (!pollId) { + return null; + } const directSelections = extractSelections(value.choices); const nestedSelections = extractSelections(readNestedValue(value, ["choices"])); @@ -116,7 +124,9 @@ export function extractMSTeamsPollVote( ? nestedSelections : dataSelections; - if (selections.length === 0) return null; + if (selections.length === 0) { + return null; + } return { pollId, @@ -212,7 +222,9 @@ export type MSTeamsPollStoreFsOptions = { }; function parseTimestamp(value?: string): number | null { - if (!value) return null; + if (!value) { + return null; + } const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : null; } @@ -228,7 +240,9 @@ function pruneExpired(polls: Record) { function pruneToLimit(polls: Record) { const entries = Object.entries(polls); - if (entries.length <= MAX_POLLS) return polls; + if (entries.length <= MAX_POLLS) { + return polls; + } entries.sort((a, b) => { const aTs = parseTimestamp(a[1].updatedAt ?? a[1].createdAt) ?? 0; const bTs = parseTimestamp(b[1].updatedAt ?? b[1].createdAt) ?? 0; @@ -287,7 +301,9 @@ export function createMSTeamsPollStoreFs(params?: MSTeamsPollStoreFsOptions): MS await withFileLock(filePath, empty, async () => { const data = await readStore(); const poll = data.polls[params.pollId]; - if (!poll) return null; + if (!poll) { + return null; + } const normalized = normalizeMSTeamsPollSelections(poll, params.selections); poll.votes[params.voterId] = normalized; poll.updatedAt = new Date().toISOString(); diff --git a/extensions/msteams/src/probe.test.ts b/extensions/msteams/src/probe.test.ts index afae0d14b..59fae64e0 100644 --- a/extensions/msteams/src/probe.test.ts +++ b/extensions/msteams/src/probe.test.ts @@ -10,7 +10,9 @@ vi.mock("@microsoft/agents-hosting", () => ({ getAuthConfigWithDefaults: (cfg: unknown) => cfg, MsalTokenProvider: class { async getAccessToken() { - if (hostMockState.tokenError) throw hostMockState.tokenError; + if (hostMockState.tokenError) { + throw hostMockState.tokenError; + } return "token"; } }, diff --git a/extensions/msteams/src/probe.ts b/extensions/msteams/src/probe.ts index 5bfd1b54d..6bbcc0b3c 100644 --- a/extensions/msteams/src/probe.ts +++ b/extensions/msteams/src/probe.ts @@ -16,7 +16,9 @@ export type ProbeMSTeamsResult = { }; function readAccessToken(value: unknown): string | null { - if (typeof value === "string") return value; + if (typeof value === "string") { + return value; + } if (value && typeof value === "object") { const token = (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token; @@ -27,7 +29,9 @@ function readAccessToken(value: unknown): string | null { function decodeJwtPayload(token: string): Record | null { const parts = token.split("."); - if (parts.length < 2) return null; + if (parts.length < 2) { + return null; + } const payload = parts[1] ?? ""; const padded = payload.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), "="); const normalized = padded.replace(/-/g, "+").replace(/_/g, "/"); @@ -41,13 +45,17 @@ function decodeJwtPayload(token: string): Record | null { } function readStringArray(value: unknown): string[] | undefined { - if (!Array.isArray(value)) return undefined; + if (!Array.isArray(value)) { + return undefined; + } const out = value.map((entry) => String(entry).trim()).filter(Boolean); return out.length > 0 ? out : undefined; } function readScopes(value: unknown): string[] | undefined { - if (typeof value !== "string") return undefined; + if (typeof value !== "string") { + return undefined; + } const out = value .split(/\s+/) .map((entry) => entry.trim()) diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index 1d46ee169..01b657ae9 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -101,7 +101,9 @@ export function createMSTeamsReplyDispatcher(params: { sharePointSiteId: params.sharePointSiteId, mediaMaxBytes, }); - if (ids.length > 0) params.onSentMessageIds?.(ids); + if (ids.length > 0) { + params.onSentMessageIds?.(ids); + } }, onError: (err, info) => { const errMsg = formatUnknownError(err); diff --git a/extensions/msteams/src/resolve-allowlist.ts b/extensions/msteams/src/resolve-allowlist.ts index f2b5bf323..371b615f3 100644 --- a/extensions/msteams/src/resolve-allowlist.ts +++ b/extensions/msteams/src/resolve-allowlist.ts @@ -40,7 +40,9 @@ export type MSTeamsUserResolution = { }; function readAccessToken(value: unknown): string | null { - if (typeof value === "string") return value; + if (typeof value === "string") { + return value; + } if (value && typeof value === "object") { const token = (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token; @@ -55,7 +57,9 @@ function stripProviderPrefix(raw: string): string { export function normalizeMSTeamsMessagingTarget(raw: string): string | undefined { let trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } trimmed = stripProviderPrefix(trimmed).trim(); if (/^conversation:/i.test(trimmed)) { const id = trimmed.slice("conversation:".length).trim(); @@ -76,7 +80,9 @@ export function normalizeMSTeamsUserInput(raw: string): string { export function parseMSTeamsConversationId(raw: string): string | null { const trimmed = stripProviderPrefix(raw).trim(); - if (!/^conversation:/i.test(trimmed)) return null; + if (!/^conversation:/i.test(trimmed)) { + return null; + } const id = trimmed.slice("conversation:".length).trim(); return id; } @@ -95,7 +101,9 @@ function normalizeMSTeamsChannelKey(raw?: string | null): string | undefined { export function parseMSTeamsTeamChannelInput(raw: string): { team?: string; channel?: string } { const trimmed = stripProviderPrefix(raw).trim(); - if (!trimmed) return {}; + if (!trimmed) { + return {}; + } const parts = trimmed.split("/"); const team = normalizeMSTeamsTeamKey(parts[0] ?? ""); const channel = @@ -110,7 +118,9 @@ export function parseMSTeamsTeamEntry( raw: string, ): { teamKey: string; channelKey?: string } | null { const { team, channel } = parseMSTeamsTeamChannelInput(raw); - if (!team) return null; + if (!team) { + return null; + } return { teamKey: team, ...(channel ? { channelKey: channel } : {}), @@ -133,7 +143,7 @@ async function fetchGraphJson(params: { const res = await fetch(`${GRAPH_ROOT}${params.path}`, { headers: { Authorization: `Bearer ${params.token}`, - ...(params.headers ?? {}), + ...params.headers, }, }); if (!res.ok) { @@ -147,12 +157,16 @@ async function resolveGraphToken(cfg: unknown): Promise { const creds = resolveMSTeamsCredentials( (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams, ); - if (!creds) throw new Error("MS Teams credentials missing"); + if (!creds) { + throw new Error("MS Teams credentials missing"); + } const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds); const tokenProvider = new sdk.MsalTokenProvider(authConfig); const token = await tokenProvider.getAccessToken("https://graph.microsoft.com"); const accessToken = readAccessToken(token); - if (!accessToken) throw new Error("MS Teams graph token unavailable"); + if (!accessToken) { + throw new Error("MS Teams graph token unavailable"); + } return accessToken; } diff --git a/extensions/msteams/src/send-context.ts b/extensions/msteams/src/send-context.ts index 1d7e293e1..b718eb812 100644 --- a/extensions/msteams/src/send-context.ts +++ b/extensions/msteams/src/send-context.ts @@ -78,12 +78,16 @@ async function findConversationReference(recipient: { } | null> { if (recipient.type === "conversation") { const ref = await recipient.store.get(recipient.id); - if (ref) return { conversationId: recipient.id, ref }; + if (ref) { + return { conversationId: recipient.id, ref }; + } return null; } const found = await recipient.store.findByUserId(recipient.id); - if (!found) return null; + if (!found) { + return null; + } return { conversationId: found.conversationId, ref: found.reference }; } diff --git a/extensions/msteams/src/send.ts b/extensions/msteams/src/send.ts index 6473e9181..04836708c 100644 --- a/extensions/msteams/src/send.ts +++ b/extensions/msteams/src/send.ts @@ -172,6 +172,7 @@ export async function sendMessageMSTeams( const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : ""; throw new Error( `msteams consent card send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, + { cause: err }, ); } @@ -310,6 +311,7 @@ export async function sendMessageMSTeams( const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : ""; throw new Error( `msteams file send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, + { cause: err }, ); } } @@ -359,6 +361,7 @@ async function sendTextWithMedia( const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : ""; throw new Error( `msteams send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, + { cause: err }, ); } @@ -424,6 +427,7 @@ export async function sendPollMSTeams( const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : ""; throw new Error( `msteams poll send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, + { cause: err }, ); } @@ -483,6 +487,7 @@ export async function sendAdaptiveCardMSTeams( const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : ""; throw new Error( `msteams card send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, + { cause: err }, ); } diff --git a/extensions/msteams/src/sent-message-cache.ts b/extensions/msteams/src/sent-message-cache.ts index 00a810f9f..1085d096b 100644 --- a/extensions/msteams/src/sent-message-cache.ts +++ b/extensions/msteams/src/sent-message-cache.ts @@ -18,7 +18,9 @@ function cleanupExpired(entry: CacheEntry): void { } export function recordMSTeamsSentMessage(conversationId: string, messageId: string): void { - if (!conversationId || !messageId) return; + if (!conversationId || !messageId) { + return; + } let entry = sentMessages.get(conversationId); if (!entry) { entry = { messageIds: new Set(), timestamps: new Map() }; @@ -26,12 +28,16 @@ export function recordMSTeamsSentMessage(conversationId: string, messageId: stri } entry.messageIds.add(messageId); entry.timestamps.set(messageId, Date.now()); - if (entry.messageIds.size > 200) cleanupExpired(entry); + if (entry.messageIds.size > 200) { + cleanupExpired(entry); + } } export function wasMSTeamsMessageSent(conversationId: string, messageId: string): boolean { const entry = sentMessages.get(conversationId); - if (!entry) return false; + if (!entry) { + return false; + } cleanupExpired(entry); return entry.messageIds.has(messageId); } diff --git a/extensions/msteams/src/storage.ts b/extensions/msteams/src/storage.ts index 09fdcf121..94ccbf900 100644 --- a/extensions/msteams/src/storage.ts +++ b/extensions/msteams/src/storage.ts @@ -11,8 +11,12 @@ export type MSTeamsStorePathOptions = { }; export function resolveMSTeamsStorePath(params: MSTeamsStorePathOptions): string { - if (params.storePath) return params.storePath; - if (params.stateDir) return path.join(params.stateDir, params.filename); + if (params.storePath) { + return params.storePath; + } + if (params.stateDir) { + return path.join(params.stateDir, params.filename); + } const env = params.env ?? process.env; const stateDir = params.homedir diff --git a/extensions/msteams/src/store-fs.ts b/extensions/msteams/src/store-fs.ts index 7f85cc9e4..08d53f355 100644 --- a/extensions/msteams/src/store-fs.ts +++ b/extensions/msteams/src/store-fs.ts @@ -30,11 +30,15 @@ export async function readJsonFile( try { const raw = await fs.promises.readFile(filePath, "utf-8"); const parsed = safeParseJson(raw); - if (parsed == null) return { value: fallback, exists: true }; + if (parsed == null) { + return { value: fallback, exists: true }; + } return { value: parsed, exists: true }; } catch (err) { const code = (err as { code?: string }).code; - if (code === "ENOENT") return { value: fallback, exists: false }; + if (code === "ENOENT") { + return { value: fallback, exists: false }; + } return { value: fallback, exists: false }; } } diff --git a/extensions/nextcloud-talk/src/accounts.ts b/extensions/nextcloud-talk/src/accounts.ts index 160215934..975d8212d 100644 --- a/extensions/nextcloud-talk/src/accounts.ts +++ b/extensions/nextcloud-talk/src/accounts.ts @@ -7,7 +7,9 @@ import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js"; const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]); function isTruthyEnvValue(value?: string): boolean { - if (!value) return false; + if (!value) { + return false; + } return TRUTHY_ENV.has(value.trim().toLowerCase()); } @@ -29,10 +31,14 @@ export type ResolvedNextcloudTalkAccount = { function listConfiguredAccountIds(cfg: CoreConfig): string[] { const accounts = cfg.channels?.["nextcloud-talk"]?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } const ids = new Set(); for (const key of Object.keys(accounts)) { - if (!key) continue; + if (!key) { + continue; + } ids.add(normalizeAccountId(key)); } return [...ids]; @@ -41,13 +47,17 @@ function listConfiguredAccountIds(cfg: CoreConfig): string[] { export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] { const ids = listConfiguredAccountIds(cfg); debugAccounts("listNextcloudTalkAccountIds", ids); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultNextcloudTalkAccountId(cfg: CoreConfig): string { const ids = listNextcloudTalkAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -56,9 +66,13 @@ function resolveAccountConfig( accountId: string, ): NextcloudTalkAccountConfig | undefined { const accounts = cfg.channels?.["nextcloud-talk"]?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } const direct = accounts[accountId] as NextcloudTalkAccountConfig | undefined; - if (direct) return direct; + if (direct) { + return direct; + } const normalized = normalizeAccountId(accountId); const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized); return matchKey ? (accounts[matchKey] as NextcloudTalkAccountConfig | undefined) : undefined; @@ -88,7 +102,9 @@ function resolveNextcloudTalkSecret( if (merged.botSecretFile) { try { const fileSecret = readFileSync(merged.botSecretFile, "utf-8").trim(); - if (fileSecret) return { secret: fileSecret, source: "secretFile" }; + if (fileSecret) { + return { secret: fileSecret, source: "secretFile" }; + } } catch { // File not found or unreadable, fall through. } @@ -135,13 +151,21 @@ export function resolveNextcloudTalkAccount(params: { const normalized = normalizeAccountId(params.accountId); const primary = resolve(normalized); - if (hasExplicitAccountId) return primary; - if (primary.secretSource !== "none") return primary; + if (hasExplicitAccountId) { + return primary; + } + if (primary.secretSource !== "none") { + return primary; + } const fallbackId = resolveDefaultNextcloudTalkAccountId(params.cfg); - if (fallbackId === primary.accountId) return primary; + if (fallbackId === primary.accountId) { + return primary; + } const fallback = resolve(fallbackId); - if (fallback.secretSource === "none") return primary; + if (fallback.secretSource === "none") { + return primary; + } return fallback; } diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index ea60e6919..10f6636ca 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -131,7 +131,9 @@ export const nextcloudTalkPlugin: ChannelPlugin = collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } const roomAllowlistConfigured = account.config.rooms && Object.keys(account.config.rooms).length > 0; if (roomAllowlistConfigured) { @@ -148,7 +150,9 @@ export const nextcloudTalkPlugin: ChannelPlugin = resolveRequireMention: ({ cfg, accountId, groupId }) => { const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }); const rooms = account.config.rooms; - if (!rooms || !groupId) return true; + if (!rooms || !groupId) { + return true; + } const roomConfig = rooms[groupId]; if (roomConfig?.requireMention !== undefined) { @@ -175,7 +179,7 @@ export const nextcloudTalkPlugin: ChannelPlugin = resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "nextcloud-talk", accountId, name, @@ -196,7 +200,7 @@ export const nextcloudTalkPlugin: ChannelPlugin = applyAccountConfig: ({ cfg, accountId, input }) => { const setupInput = input as NextcloudSetupInput; const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "nextcloud-talk", accountId, name: setupInput.name, diff --git a/extensions/nextcloud-talk/src/format.ts b/extensions/nextcloud-talk/src/format.ts index 707d5c893..4ea7fc1de 100644 --- a/extensions/nextcloud-talk/src/format.ts +++ b/extensions/nextcloud-talk/src/format.ts @@ -67,7 +67,9 @@ export function stripNextcloudTalkFormatting(text: string): string { * Truncate text to a maximum length, preserving word boundaries. */ export function truncateNextcloudTalkText(text: string, maxLength: number, suffix = "..."): string { - if (text.length <= maxLength) return text; + if (text.length <= maxLength) { + return text; + } const truncated = text.slice(0, maxLength - suffix.length); const lastSpace = truncated.lastIndexOf(" "); if (lastSpace > maxLength * 0.7) { diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index d7087f8b5..7fcadddab 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -35,7 +35,9 @@ async function deliverNextcloudTalkReply(params: { ? [payload.mediaUrl] : []; - if (!text.trim() && mediaList.length === 0) return; + if (!text.trim() && mediaList.length === 0) { + return; + } const mediaBlock = mediaList.length ? mediaList.map((url) => `Attachment: ${url}`).join("\n") @@ -64,7 +66,9 @@ export async function handleNextcloudTalkInbound(params: { const core = getNextcloudTalkRuntime(); const rawBody = message.text?.trim() ?? ""; - if (!rawBody) return; + if (!rawBody) { + return; + } const roomKind = await resolveNextcloudTalkRoomKind({ account, diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index e13410ee3..6fa4cbe90 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -19,7 +19,9 @@ const DEFAULT_WEBHOOK_PATH = "/nextcloud-talk-webhook"; const HEALTH_PATH = "/healthz"; function formatError(err: unknown): string { - if (err instanceof Error) return err.message; + if (err instanceof Error) { + return err.message; + } return typeof err === "string" ? err : JSON.stringify(err); } diff --git a/extensions/nextcloud-talk/src/normalize.ts b/extensions/nextcloud-talk/src/normalize.ts index c8365d69d..6854d603f 100644 --- a/extensions/nextcloud-talk/src/normalize.ts +++ b/extensions/nextcloud-talk/src/normalize.ts @@ -1,6 +1,8 @@ export function normalizeNextcloudTalkMessagingTarget(raw: string): string | undefined { const trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } let normalized = trimmed; @@ -16,16 +18,22 @@ export function normalizeNextcloudTalkMessagingTarget(raw: string): string | und normalized = normalized.slice("room:".length).trim(); } - if (!normalized) return undefined; + if (!normalized) { + return undefined; + } return `nextcloud-talk:${normalized}`.toLowerCase(); } export function looksLikeNextcloudTalkTargetId(raw: string): boolean { const trimmed = raw.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } - if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed)) return true; + if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed)) { + return true; + } return /^[a-z0-9]{8,}$/i.test(trimmed); } diff --git a/extensions/nextcloud-talk/src/onboarding.ts b/extensions/nextcloud-talk/src/onboarding.ts index edbc9bca5..751949211 100644 --- a/extensions/nextcloud-talk/src/onboarding.ts +++ b/extensions/nextcloud-talk/src/onboarding.ts @@ -221,7 +221,9 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = { message: "Enter Nextcloud instance URL (e.g., https://cloud.example.com)", validate: (value) => { const v = String(value ?? "").trim(); - if (!v) return "Required"; + if (!v) { + return "Required"; + } if (!v.startsWith("http://") && !v.startsWith("https://")) { return "URL must start with http:// or https://"; } diff --git a/extensions/nextcloud-talk/src/policy.ts b/extensions/nextcloud-talk/src/policy.ts index 9369a8afc..a38d307fc 100644 --- a/extensions/nextcloud-talk/src/policy.ts +++ b/extensions/nextcloud-talk/src/policy.ts @@ -33,7 +33,9 @@ export function resolveNextcloudTalkAllowlistMatch(params: { senderName?: string | null; }): AllowlistMatch<"wildcard" | "id" | "name"> { const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom); - if (allowFrom.length === 0) return { allowed: false }; + if (allowFrom.length === 0) { + return { allowed: false }; + } if (allowFrom.includes("*")) { return { allowed: true, matchKey: "*", matchSource: "wildcard" }; } @@ -101,7 +103,9 @@ export function resolveNextcloudTalkGroupToolPolicy( channels?: { "nextcloud-talk"?: { rooms?: Record } }; }; const roomToken = params.groupId?.trim(); - if (!roomToken) return undefined; + if (!roomToken) { + return undefined; + } const roomName = params.groupChannel?.trim() || undefined; const match = resolveNextcloudTalkRoomMatch({ rooms: cfg.channels?.["nextcloud-talk"]?.rooms, diff --git a/extensions/nextcloud-talk/src/room-info.ts b/extensions/nextcloud-talk/src/room-info.ts index 3f5c2796d..191c423d1 100644 --- a/extensions/nextcloud-talk/src/room-info.ts +++ b/extensions/nextcloud-talk/src/room-info.ts @@ -20,8 +20,12 @@ function readApiPassword(params: { apiPassword?: string; apiPasswordFile?: string; }): string | undefined { - if (params.apiPassword?.trim()) return params.apiPassword.trim(); - if (!params.apiPasswordFile) return undefined; + if (params.apiPassword?.trim()) { + return params.apiPassword.trim(); + } + if (!params.apiPasswordFile) { + return undefined; + } try { const value = readFileSync(params.apiPasswordFile, "utf-8").trim(); return value || undefined; @@ -31,7 +35,9 @@ function readApiPassword(params: { } function coerceRoomType(value: unknown): number | undefined { - if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } if (typeof value === "string" && value.trim()) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : undefined; @@ -40,8 +46,12 @@ function coerceRoomType(value: unknown): number | undefined { } function resolveRoomKindFromType(type: number | undefined): "direct" | "group" | undefined { - if (!type) return undefined; - if (type === 1 || type === 5 || type === 6) return "direct"; + if (!type) { + return undefined; + } + if (type === 1 || type === 5 || type === 6) { + return "direct"; + } return "group"; } @@ -55,8 +65,12 @@ export async function resolveNextcloudTalkRoomKind(params: { const cached = roomCache.get(key); if (cached) { const age = Date.now() - cached.fetchedAt; - if (cached.kind && age < ROOM_CACHE_TTL_MS) return cached.kind; - if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) return undefined; + if (cached.kind && age < ROOM_CACHE_TTL_MS) { + return cached.kind; + } + if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) { + return undefined; + } } const apiUser = account.config.apiUser?.trim(); @@ -64,10 +78,14 @@ export async function resolveNextcloudTalkRoomKind(params: { apiPassword: account.config.apiPassword, apiPasswordFile: account.config.apiPasswordFile, }); - if (!apiUser || !apiPassword) return undefined; + if (!apiUser || !apiPassword) { + return undefined; + } const baseUrl = account.baseUrl?.trim(); - if (!baseUrl) return undefined; + if (!baseUrl) { + return undefined; + } const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`; const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64"); diff --git a/extensions/nextcloud-talk/src/send.ts b/extensions/nextcloud-talk/src/send.ts index 1dd8f5094..0bece021c 100644 --- a/extensions/nextcloud-talk/src/send.ts +++ b/extensions/nextcloud-talk/src/send.ts @@ -34,7 +34,9 @@ function resolveCredentials( function normalizeRoomToken(to: string): string { const trimmed = to.trim(); - if (!trimmed) throw new Error("Room token is required for Nextcloud Talk sends"); + if (!trimmed) { + throw new Error("Room token is required for Nextcloud Talk sends"); + } let normalized = trimmed; if (normalized.startsWith("nextcloud-talk:")) { @@ -47,7 +49,9 @@ function normalizeRoomToken(to: string): string { normalized = normalized.slice("room:".length).trim(); } - if (!normalized) throw new Error("Room token is required for Nextcloud Talk sends"); + if (!normalized) { + throw new Error("Room token is required for Nextcloud Talk sends"); + } return normalized; } diff --git a/extensions/nextcloud-talk/src/signature.ts b/extensions/nextcloud-talk/src/signature.ts index 93384720d..ad5351d36 100644 --- a/extensions/nextcloud-talk/src/signature.ts +++ b/extensions/nextcloud-talk/src/signature.ts @@ -17,13 +17,17 @@ export function verifyNextcloudTalkSignature(params: { secret: string; }): boolean { const { signature, random, body, secret } = params; - if (!signature || !random || !secret) return false; + if (!signature || !random || !secret) { + return false; + } const expected = createHmac("sha256", secret) .update(random + body) .digest("hex"); - if (signature.length !== expected.length) return false; + if (signature.length !== expected.length) { + return false; + } let result = 0; for (let i = 0; i < signature.length; i++) { result |= signature.charCodeAt(i) ^ expected.charCodeAt(i); @@ -46,7 +50,9 @@ export function extractNextcloudTalkHeaders( const random = getHeader(RANDOM_HEADER); const backend = getHeader(BACKEND_HEADER); - if (!signature || !random || !backend) return null; + if (!signature || !random || !backend) { + return null; + } return { signature, random, backend }; } diff --git a/extensions/nostr/index.ts b/extensions/nostr/index.ts index 13499d7e7..c762ce3c1 100644 --- a/extensions/nostr/index.ts +++ b/extensions/nostr/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; import { nostrPlugin } from "./src/channel.js"; @@ -20,13 +20,13 @@ const plugin = { const httpHandler = createNostrProfileHttpHandler({ getConfigProfile: (accountId: string) => { const runtime = getNostrRuntime(); - const cfg = runtime.config.loadConfig() as OpenClawConfig; + const cfg = runtime.config.loadConfig(); const account = resolveNostrAccount({ cfg, accountId }); return account.profile; }, updateConfigProfile: async (accountId: string, profile: NostrProfile) => { const runtime = getNostrRuntime(); - const cfg = runtime.config.loadConfig() as OpenClawConfig; + const cfg = runtime.config.loadConfig(); // Build the config patch for channels.nostr.profile const channels = (cfg.channels ?? {}) as Record; @@ -49,7 +49,7 @@ const plugin = { }, getAccountInfo: (accountId: string) => { const runtime = getNostrRuntime(); - const cfg = runtime.config.loadConfig() as OpenClawConfig; + const cfg = runtime.config.loadConfig(); const account = resolveNostrAccount({ cfg, accountId }); if (!account.configured || !account.publicKey) { return null; diff --git a/extensions/nostr/src/channel.test.ts b/extensions/nostr/src/channel.test.ts index 4008d6304..0f8cb72ae 100644 --- a/extensions/nostr/src/channel.test.ts +++ b/extensions/nostr/src/channel.test.ts @@ -61,14 +61,18 @@ describe("nostrPlugin", () => { it("recognizes npub as valid target", () => { const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId; - if (!looksLikeId) return; + if (!looksLikeId) { + return; + } expect(looksLikeId("npub1xyz123")).toBe(true); }); it("recognizes hex pubkey as valid target", () => { const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId; - if (!looksLikeId) return; + if (!looksLikeId) { + return; + } const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(looksLikeId(hexPubkey)).toBe(true); @@ -76,7 +80,9 @@ describe("nostrPlugin", () => { it("rejects invalid input", () => { const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId; - if (!looksLikeId) return; + if (!looksLikeId) { + return; + } expect(looksLikeId("not-a-pubkey")).toBe(false); expect(looksLikeId("")).toBe(false); @@ -84,7 +90,9 @@ describe("nostrPlugin", () => { it("normalizeTarget strips nostr: prefix", () => { const normalize = nostrPlugin.messaging?.normalizeTarget; - if (!normalize) return; + if (!normalize) { + return; + } const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey); @@ -108,7 +116,9 @@ describe("nostrPlugin", () => { it("normalizes nostr: prefix in allow entries", () => { const normalize = nostrPlugin.pairing?.normalizeAllowEntry; - if (!normalize) return; + if (!normalize) { + return; + } const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey); diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index 66a2d7c1d..3fa07064e 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -63,7 +63,9 @@ export const nostrPlugin: ChannelPlugin = { .map((entry) => String(entry).trim()) .filter(Boolean) .map((entry) => { - if (entry === "*") return "*"; + if (entry === "*") { + return "*"; + } try { return normalizePubkey(entry); } catch { @@ -162,7 +164,9 @@ export const nostrPlugin: ChannelPlugin = { collectStatusIssues: (accounts) => accounts.flatMap((account) => { const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; - if (!lastError) return []; + if (!lastError) { + return []; + } return [ { channel: "nostr", diff --git a/extensions/nostr/src/metrics.ts b/extensions/nostr/src/metrics.ts index a55189fdb..11030e5bc 100644 --- a/extensions/nostr/src/metrics.ts +++ b/extensions/nostr/src/metrics.ts @@ -300,34 +300,54 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics { // Relay metrics case "relay.connect": - if (relayUrl) getOrCreateRelay(relayUrl).connects += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).connects += value; + } break; case "relay.disconnect": - if (relayUrl) getOrCreateRelay(relayUrl).disconnects += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).disconnects += value; + } break; case "relay.reconnect": - if (relayUrl) getOrCreateRelay(relayUrl).reconnects += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).reconnects += value; + } break; case "relay.error": - if (relayUrl) getOrCreateRelay(relayUrl).errors += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).errors += value; + } break; case "relay.message.event": - if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.event += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).messagesReceived.event += value; + } break; case "relay.message.eose": - if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.eose += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).messagesReceived.eose += value; + } break; case "relay.message.closed": - if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.closed += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).messagesReceived.closed += value; + } break; case "relay.message.notice": - if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.notice += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).messagesReceived.notice += value; + } break; case "relay.message.ok": - if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.ok += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).messagesReceived.ok += value; + } break; case "relay.message.auth": - if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.auth += value; + if (relayUrl) { + getOrCreateRelay(relayUrl).messagesReceived.auth += value; + } break; case "relay.circuit_breaker.open": if (relayUrl) { diff --git a/extensions/nostr/src/nostr-bus.integration.test.ts b/extensions/nostr/src/nostr-bus.integration.test.ts index b286fb8aa..b145b3ef3 100644 --- a/extensions/nostr/src/nostr-bus.integration.test.ts +++ b/extensions/nostr/src/nostr-bus.integration.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createSeenTracker } from "./seen-tracker.js"; import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js"; diff --git a/extensions/nostr/src/nostr-bus.ts b/extensions/nostr/src/nostr-bus.ts index b4a3f6675..eb4abfccb 100644 --- a/extensions/nostr/src/nostr-bus.ts +++ b/extensions/nostr/src/nostr-bus.ts @@ -36,11 +36,6 @@ const STARTUP_LOOKBACK_SEC = 120; // tolerate relay lag / clock skew const MAX_PERSISTED_EVENT_IDS = 5000; const STATE_PERSIST_DEBOUNCE_MS = 5000; // Debounce state writes -// Reconnect configuration (exponential backoff with jitter) -const RECONNECT_BASE_MS = 1000; // 1 second base -const RECONNECT_MAX_MS = 60000; // 60 seconds max -const RECONNECT_JITTER = 0.3; // ±30% jitter - // Circuit breaker configuration const CIRCUIT_BREAKER_THRESHOLD = 5; // failures before opening const CIRCUIT_BREAKER_RESET_MS = 30000; // 30 seconds before half-open @@ -137,7 +132,9 @@ function createCircuitBreaker( return { canAttempt(): boolean { - if (state.state === "closed") return true; + if (state.state === "closed") { + return true; + } if (state.state === "open") { // Check if enough time has passed to try half-open @@ -243,10 +240,14 @@ function createRelayHealthTracker(): RelayHealthTracker { getScore(relay: string): number { const s = stats.get(relay); - if (!s) return 0.5; // Unknown relay gets neutral score + if (!s) { + return 0.5; + } // Unknown relay gets neutral score const total = s.successCount + s.failureCount; - if (total === 0) return 0.5; + if (total === 0) { + return 0.5; + } // Success rate (0-1) const successRate = s.successCount / total; @@ -266,25 +267,11 @@ function createRelayHealthTracker(): RelayHealthTracker { }, getSortedRelays(relays: string[]): string[] { - return [...relays].sort((a, b) => this.getScore(b) - this.getScore(a)); + return [...relays].toSorted((a, b) => this.getScore(b) - this.getScore(a)); }, }; } -// ============================================================================ -// Reconnect with Exponential Backoff + Jitter -// ============================================================================ - -function computeReconnectDelay(attempt: number): number { - // Exponential backoff: base * 2^attempt - const exponential = RECONNECT_BASE_MS * Math.pow(2, attempt); - const capped = Math.min(exponential, RECONNECT_MAX_MS); - - // Add jitter: ±JITTER% - const jitter = capped * RECONNECT_JITTER * (Math.random() * 2 - 1); - return Math.max(RECONNECT_BASE_MS, capped + jitter); -} - // ============================================================================ // Key Validation // ============================================================================ @@ -397,7 +384,9 @@ export async function startNostrBus(options: NostrBusOptions): Promise { writeNostrBusState({ accountId, @@ -461,7 +450,7 @@ export async function startNostrBus(options: NostrBusOptions): Promise void, ): Promise { - const ciphertext = await encrypt(sk, toPubkey, text); + const ciphertext = encrypt(sk, toPubkey, text); const reply = finalizeEvent( { kind: 4, @@ -640,6 +629,7 @@ async function sendEncryptedDm( const startTime = Date.now(); try { + // oxlint-disable-next-line typescript/await-thenable typesciript/no-floating-promises await pool.publish([relay], reply); const latency = Date.now() - startTime; @@ -672,7 +662,9 @@ async function sendEncryptedDm( * Check if a string looks like a valid Nostr pubkey (hex or npub) */ export function isValidPubkey(input: string): boolean { - if (typeof input !== "string") return false; + if (typeof input !== "string") { + return false; + } const trimmed = input.trim(); // npub format diff --git a/extensions/nostr/src/nostr-profile-http.test.ts b/extensions/nostr/src/nostr-profile-http.test.ts index 98bbbe4c7..a32896019 100644 --- a/extensions/nostr/src/nostr-profile-http.test.ts +++ b/extensions/nostr/src/nostr-profile-http.test.ts @@ -2,7 +2,7 @@ * Tests for Nostr Profile HTTP Handler */ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { IncomingMessage, ServerResponse } from "node:http"; import { Socket } from "node:net"; @@ -56,7 +56,6 @@ function createMockResponse(): ServerResponse & { _getData: () => string; _getStatusCode: () => number; } { - const socket = new Socket(); const res = new ServerResponse({} as IncomingMessage); let data = ""; @@ -68,7 +67,10 @@ function createMockResponse(): ServerResponse & { }; res.end = function (chunk?: unknown) { - if (chunk) data += String(chunk); + if (chunk) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + data += String(chunk); + } return this; }; diff --git a/extensions/nostr/src/nostr-profile-http.ts b/extensions/nostr/src/nostr-profile-http.ts index 3136e3db2..6a8efb0c8 100644 --- a/extensions/nostr/src/nostr-profile-http.ts +++ b/extensions/nostr/src/nostr-profile-http.ts @@ -113,33 +113,53 @@ function isPrivateIp(ip: string): boolean { // Handle IPv4 const ipv4Match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/); if (ipv4Match) { - const [, a, b, c] = ipv4Match.map(Number); + const [, a, b] = ipv4Match.map(Number); // 127.0.0.0/8 (loopback) - if (a === 127) return true; + if (a === 127) { + return true; + } // 10.0.0.0/8 (private) - if (a === 10) return true; + if (a === 10) { + return true; + } // 172.16.0.0/12 (private) - if (a === 172 && b >= 16 && b <= 31) return true; + if (a === 172 && b >= 16 && b <= 31) { + return true; + } // 192.168.0.0/16 (private) - if (a === 192 && b === 168) return true; + if (a === 192 && b === 168) { + return true; + } // 169.254.0.0/16 (link-local) - if (a === 169 && b === 254) return true; + if (a === 169 && b === 254) { + return true; + } // 0.0.0.0/8 - if (a === 0) return true; + if (a === 0) { + return true; + } return false; } // Handle IPv6 const ipLower = ip.toLowerCase().replace(/^\[|\]$/g, ""); // ::1 (loopback) - if (ipLower === "::1") return true; + if (ipLower === "::1") { + return true; + } // fe80::/10 (link-local) - if (ipLower.startsWith("fe80:")) return true; + if (ipLower.startsWith("fe80:")) { + return true; + } // fc00::/7 (unique local) - if (ipLower.startsWith("fc") || ipLower.startsWith("fd")) return true; + if (ipLower.startsWith("fc") || ipLower.startsWith("fd")) { + return true; + } // ::ffff:x.x.x.x (IPv4-mapped IPv6) - extract and check IPv4 const v4Mapped = ipLower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); - if (v4Mapped) return isPrivateIp(v4Mapped[1]); + if (v4Mapped) { + return isPrivateIp(v4Mapped[1]); + } return false; } diff --git a/extensions/nostr/src/nostr-profile-import.test.ts b/extensions/nostr/src/nostr-profile-import.test.ts index 74b9deacf..6488195e2 100644 --- a/extensions/nostr/src/nostr-profile-import.test.ts +++ b/extensions/nostr/src/nostr-profile-import.test.ts @@ -2,9 +2,9 @@ * Tests for Nostr Profile Import */ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect } from "vitest"; -import { mergeProfiles, type ProfileImportOptions } from "./nostr-profile-import.js"; +import { mergeProfiles } from "./nostr-profile-import.js"; import type { NostrProfile } from "./config-schema.js"; // Note: importProfileFromRelays requires real network calls or complex mocking diff --git a/extensions/nostr/src/nostr-profile-import.ts b/extensions/nostr/src/nostr-profile-import.ts index 87d29a8dc..b839f825e 100644 --- a/extensions/nostr/src/nostr-profile-import.ts +++ b/extensions/nostr/src/nostr-profile-import.ts @@ -243,8 +243,12 @@ export function mergeProfiles( local: NostrProfile | undefined, imported: NostrProfile | undefined, ): NostrProfile { - if (!imported) return local ?? {}; - if (!local) return imported; + if (!imported) { + return local ?? {}; + } + if (!local) { + return imported; + } return { name: local.name ?? imported.name, diff --git a/extensions/nostr/src/nostr-profile.fuzz.test.ts b/extensions/nostr/src/nostr-profile.fuzz.test.ts index 669ec27ed..e082830c4 100644 --- a/extensions/nostr/src/nostr-profile.fuzz.test.ts +++ b/extensions/nostr/src/nostr-profile.fuzz.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; -import { getPublicKey } from "nostr-tools"; import { createProfileEvent, profileToContent, @@ -422,7 +421,7 @@ describe("profile type confusion", () => { it("handles prototype pollution attempt", () => { const malicious = JSON.parse('{"__proto__": {"polluted": true}}') as unknown; - const result = validateProfile(malicious); + validateProfile(malicious); // Should not pollute Object.prototype expect(({} as Record).polluted).toBeUndefined(); }); diff --git a/extensions/nostr/src/nostr-profile.ts b/extensions/nostr/src/nostr-profile.ts index 45dd54314..6796c6f3f 100644 --- a/extensions/nostr/src/nostr-profile.ts +++ b/extensions/nostr/src/nostr-profile.ts @@ -49,14 +49,30 @@ export function profileToContent(profile: NostrProfile): ProfileContent { const content: ProfileContent = {}; - if (validated.name !== undefined) content.name = validated.name; - if (validated.displayName !== undefined) content.display_name = validated.displayName; - if (validated.about !== undefined) content.about = validated.about; - if (validated.picture !== undefined) content.picture = validated.picture; - if (validated.banner !== undefined) content.banner = validated.banner; - if (validated.website !== undefined) content.website = validated.website; - if (validated.nip05 !== undefined) content.nip05 = validated.nip05; - if (validated.lud16 !== undefined) content.lud16 = validated.lud16; + if (validated.name !== undefined) { + content.name = validated.name; + } + if (validated.displayName !== undefined) { + content.display_name = validated.displayName; + } + if (validated.about !== undefined) { + content.about = validated.about; + } + if (validated.picture !== undefined) { + content.picture = validated.picture; + } + if (validated.banner !== undefined) { + content.banner = validated.banner; + } + if (validated.website !== undefined) { + content.website = validated.website; + } + if (validated.nip05 !== undefined) { + content.nip05 = validated.nip05; + } + if (validated.lud16 !== undefined) { + content.lud16 = validated.lud16; + } return content; } @@ -68,14 +84,30 @@ export function profileToContent(profile: NostrProfile): ProfileContent { export function contentToProfile(content: ProfileContent): NostrProfile { const profile: NostrProfile = {}; - if (content.name !== undefined) profile.name = content.name; - if (content.display_name !== undefined) profile.displayName = content.display_name; - if (content.about !== undefined) profile.about = content.about; - if (content.picture !== undefined) profile.picture = content.picture; - if (content.banner !== undefined) profile.banner = content.banner; - if (content.website !== undefined) profile.website = content.website; - if (content.nip05 !== undefined) profile.nip05 = content.nip05; - if (content.lud16 !== undefined) profile.lud16 = content.lud16; + if (content.name !== undefined) { + profile.name = content.name; + } + if (content.display_name !== undefined) { + profile.displayName = content.display_name; + } + if (content.about !== undefined) { + profile.about = content.about; + } + if (content.picture !== undefined) { + profile.picture = content.picture; + } + if (content.banner !== undefined) { + profile.banner = content.banner; + } + if (content.website !== undefined) { + profile.website = content.website; + } + if (content.nip05 !== undefined) { + profile.nip05 = content.nip05; + } + if (content.lud16 !== undefined) { + profile.lud16 = content.lud16; + } return profile; } @@ -150,6 +182,7 @@ export async function publishProfileEvent( setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS); }); + // oxlint-disable-next-line typescript/no-floating-promises await Promise.race([pool.publish([relay], event), timeoutPromise]); successes.push(relay); @@ -220,7 +253,9 @@ export function validateProfile(profile: unknown): { */ export function sanitizeProfileForDisplay(profile: NostrProfile): NostrProfile { const escapeHtml = (str: string | undefined): string | undefined => { - if (str === undefined) return undefined; + if (str === undefined) { + return undefined; + } return str .replace(/&/g, "&") .replace(/(fn: (dir: string) => Promise) { state: { resolveStateDir: (env, homedir) => { const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim(); - if (override) return override; + if (override) { + return override; + } return path.join(homedir(), ".openclaw"); }, }, @@ -28,8 +30,11 @@ async function withTempStateDir(fn: (dir: string) => Promise) { try { return await fn(dir); } finally { - if (previous === undefined) delete process.env.OPENCLAW_STATE_DIR; - else process.env.OPENCLAW_STATE_DIR = previous; + if (previous === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previous; + } await fs.rm(dir, { recursive: true, force: true }); } } diff --git a/extensions/nostr/src/nostr-state-store.ts b/extensions/nostr/src/nostr-state-store.ts index 944f72e61..08a2235de 100644 --- a/extensions/nostr/src/nostr-state-store.ts +++ b/extensions/nostr/src/nostr-state-store.ts @@ -39,7 +39,9 @@ export type NostrProfileState = { function normalizeAccountId(accountId?: string): string { const trimmed = accountId?.trim(); - if (!trimmed) return "default"; + if (!trimmed) { + return "default"; + } return trimmed.replace(/[^a-z0-9._-]+/gi, "_"); } @@ -101,7 +103,9 @@ export async function readNostrBusState(params: { return safeParseState(raw); } catch (err) { const code = (err as { code?: string }).code; - if (code === "ENOENT") return null; + if (code === "ENOENT") { + return null; + } return null; } } @@ -139,14 +143,18 @@ export function computeSinceTimestamp( state: NostrBusState | null, nowSec: number = Math.floor(Date.now() / 1000), ): number { - if (!state) return nowSec; + if (!state) { + return nowSec; + } // Use the most recent timestamp we have const candidates = [state.lastProcessedAt, state.gatewayStartedAt].filter( (t): t is number => t !== null && t > 0, ); - if (candidates.length === 0) return nowSec; + if (candidates.length === 0) { + return nowSec; + } return Math.max(...candidates); } @@ -166,7 +174,7 @@ function safeParseProfileState(raw: string): NostrProfileState | null { typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null, lastPublishResults: parsed.lastPublishResults && typeof parsed.lastPublishResults === "object" - ? (parsed.lastPublishResults as Record) + ? parsed.lastPublishResults : null, }; } @@ -187,7 +195,9 @@ export async function readNostrProfileState(params: { return safeParseProfileState(raw); } catch (err) { const code = (err as { code?: string }).code; - if (code === "ENOENT") return null; + if (code === "ENOENT") { + return null; + } return null; } } diff --git a/extensions/nostr/src/seen-tracker.ts b/extensions/nostr/src/seen-tracker.ts index 29d19ed5d..7c9033c49 100644 --- a/extensions/nostr/src/seen-tracker.ts +++ b/extensions/nostr/src/seen-tracker.ts @@ -56,19 +56,27 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { // Move an entry to the front (most recently used) function moveToFront(id: string): void { const entry = entries.get(id); - if (!entry) return; + if (!entry) { + return; + } // Already at front - if (head === id) return; + if (head === id) { + return; + } // Remove from current position if (entry.prev) { const prevEntry = entries.get(entry.prev); - if (prevEntry) prevEntry.next = entry.next; + if (prevEntry) { + prevEntry.next = entry.next; + } } if (entry.next) { const nextEntry = entries.get(entry.next); - if (nextEntry) nextEntry.prev = entry.prev; + if (nextEntry) { + nextEntry.prev = entry.prev; + } } // Update tail if this was the tail @@ -81,29 +89,39 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { entry.next = head; if (head) { const headEntry = entries.get(head); - if (headEntry) headEntry.prev = id; + if (headEntry) { + headEntry.prev = id; + } } head = id; // If no tail, this is also the tail - if (!tail) tail = id; + if (!tail) { + tail = id; + } } // Remove an entry from the linked list function removeFromList(id: string): void { const entry = entries.get(id); - if (!entry) return; + if (!entry) { + return; + } if (entry.prev) { const prevEntry = entries.get(entry.prev); - if (prevEntry) prevEntry.next = entry.next; + if (prevEntry) { + prevEntry.next = entry.next; + } } else { head = entry.next; } if (entry.next) { const nextEntry = entries.get(entry.next); - if (nextEntry) nextEntry.prev = entry.prev; + if (nextEntry) { + nextEntry.prev = entry.prev; + } } else { tail = entry.prev; } @@ -111,7 +129,9 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { // Evict the least recently used entry function evictLRU(): void { - if (!tail) return; + if (!tail) { + return; + } const idToEvict = tail; removeFromList(idToEvict); entries.delete(idToEvict); @@ -139,7 +159,9 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { if (pruneIntervalMs > 0) { pruneTimer = setInterval(pruneExpired, pruneIntervalMs); // Don't keep process alive just for pruning - if (pruneTimer.unref) pruneTimer.unref(); + if (pruneTimer.unref) { + pruneTimer.unref(); + } } function add(id: string): void { @@ -167,12 +189,16 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { if (head) { const headEntry = entries.get(head); - if (headEntry) headEntry.prev = id; + if (headEntry) { + headEntry.prev = id; + } } entries.set(id, newEntry); head = id; - if (!tail) tail = id; + if (!tail) { + tail = id; + } } function has(id: string): boolean { @@ -198,7 +224,9 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { function peek(id: string): boolean { const entry = entries.get(id); - if (!entry) return false; + if (!entry) { + return false; + } // Check if expired if (Date.now() - entry.seenAt > ttlMs) { @@ -248,12 +276,16 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { if (head) { const headEntry = entries.get(head); - if (headEntry) headEntry.prev = id; + if (headEntry) { + headEntry.prev = id; + } } entries.set(id, newEntry); head = id; - if (!tail) tail = id; + if (!tail) { + tail = id; + } } } } diff --git a/extensions/nostr/src/types.ts b/extensions/nostr/src/types.ts index 586c7c52e..f094294c5 100644 --- a/extensions/nostr/src/types.ts +++ b/extensions/nostr/src/types.ts @@ -48,7 +48,9 @@ export function listNostrAccountIds(cfg: OpenClawConfig): string[] { */ export function resolveDefaultNostrAccountId(cfg: OpenClawConfig): string { const ids = listNostrAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index b3e394247..99856ceed 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -115,7 +115,9 @@ export const signalPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } return [ `- Signal groups: groupPolicy="open" allows any member to trigger the bot. Set channels.signal.groupPolicy="allowlist" + channels.signal.groupAllowFrom to restrict senders.`, ]; @@ -252,7 +254,9 @@ export const signalPlugin: ChannelPlugin = { collectStatusIssues: (accounts) => accounts.flatMap((account) => { const lastError = typeof account.lastError === "string" ? account.lastError.trim() : ""; - if (!lastError) return []; + if (!lastError) { + return []; + } return [ { channel: "signal", diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 51a445f35..d71aef81b 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -43,8 +43,12 @@ function getTokenForOperation( const userToken = account.config.userToken?.trim() || undefined; const botToken = account.botToken?.trim(); const allowUserWrites = account.config.userTokenReadOnly === false; - if (operation === "read") return userToken ?? botToken; - if (!allowUserWrites) return botToken; + if (operation === "read") { + return userToken ?? botToken; + } + if (!allowUserWrites) { + return botToken; + } return botToken ?? userToken; } @@ -234,7 +238,9 @@ export const slackPlugin: ChannelPlugin = { const accounts = listEnabledSlackAccounts(cfg).filter( (account) => account.botTokenSource !== "none", ); - if (accounts.length === 0) return []; + if (accounts.length === 0) { + return []; + } const isActionEnabled = (key: string, defaultValue = true) => { for (const account of accounts) { const gate = createActionGate( @@ -243,7 +249,9 @@ export const slackPlugin: ChannelPlugin = { boolean | undefined >, ); - if (gate(key, defaultValue)) return true; + if (gate(key, defaultValue)) { + return true; + } } return false; }; @@ -263,15 +271,23 @@ export const slackPlugin: ChannelPlugin = { actions.add("unpin"); actions.add("list-pins"); } - if (isActionEnabled("memberInfo")) actions.add("member-info"); - if (isActionEnabled("emojiList")) actions.add("emoji-list"); + if (isActionEnabled("memberInfo")) { + actions.add("member-info"); + } + if (isActionEnabled("emojiList")) { + actions.add("emoji-list"); + } return Array.from(actions); }, extractToolSend: ({ args }) => { const action = typeof args.action === "string" ? args.action.trim() : ""; - if (action !== "sendMessage") return null; + if (action !== "sendMessage") { + return null; + } const to = typeof args.to === "string" ? args.to : undefined; - if (!to) return null; + if (!to) { + return null; + } const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined; return { to, accountId }; }, @@ -544,7 +560,9 @@ export const slackPlugin: ChannelPlugin = { }), probeAccount: async ({ account, timeoutMs }) => { const token = account.botToken?.trim(); - if (!token) return { ok: false, error: "missing token" }; + if (!token) { + return { ok: false, error: "missing token" }; + } return await getSlackRuntime().channel.slack.probeSlack(token, timeoutMs); }, buildAccountSnapshot: ({ account, runtime, probe }) => { diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index cda5b76ab..0a923529c 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -40,18 +40,24 @@ const telegramMessageActions: ChannelMessageActionAdapter = { }; function parseReplyToMessageId(replyToId?: string | null) { - if (!replyToId) return undefined; + if (!replyToId) { + return undefined; + } const parsed = Number.parseInt(replyToId, 10); return Number.isFinite(parsed) ? parsed : undefined; } function parseThreadId(threadId?: string | number | null) { - if (threadId == null) return undefined; + if (threadId == null) { + return undefined; + } if (typeof threadId === "number") { return Number.isFinite(threadId) ? Math.trunc(threadId) : undefined; } const trimmed = threadId.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const parsed = Number.parseInt(trimmed, 10); return Number.isFinite(parsed) ? parsed : undefined; } @@ -67,7 +73,9 @@ export const telegramPlugin: ChannelPlugin = { normalizeAllowEntry: (entry) => entry.replace(/^(telegram|tg):/i, ""), notifyApproval: async ({ cfg, id }) => { const { token } = getTelegramRuntime().channel.telegram.resolveTelegramToken(cfg); - if (!token) throw new Error("telegram token not configured"); + if (!token) { + throw new Error("telegram token not configured"); + } await getTelegramRuntime().channel.telegram.sendMessageTelegram( id, PAIRING_APPROVED_MESSAGE, @@ -144,7 +152,9 @@ export const telegramPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } const groupAllowlistConfigured = account.config.groups && Object.keys(account.config.groups).length > 0; if (groupAllowlistConfigured) { @@ -389,7 +399,9 @@ export const telegramPlugin: ChannelPlugin = { account.config.proxy, ); const username = probe.ok ? probe.bot?.username?.trim() : null; - if (username) telegramBotLabel = ` (@${username})`; + if (username) { + telegramBotLabel = ` (@${username})`; + } } catch (err) { if (getTelegramRuntime().logging.shouldLogVerbose()) { ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`); diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index 2a1e7491c..a170e62cf 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -79,9 +79,9 @@ function applyTlonSetupConfig(params: { accounts: { ...(base as { accounts?: Record }).accounts, [accountId]: { - ...((base as { accounts?: Record> }).accounts?.[ + ...(base as { accounts?: Record> }).accounts?.[ accountId - ] ?? {}), + ], enabled: true, ...payload, }, @@ -108,7 +108,7 @@ const tlonOutbound: ChannelOutboundAdapter = { return { ok: true, to: parsed.nest }; }, sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => { - const account = resolveTlonAccount(cfg as OpenClawConfig, accountId ?? undefined); + const account = resolveTlonAccount(cfg, accountId ?? undefined); if (!account.configured || !account.ship || !account.url || !account.code) { throw new Error("Tlon account not configured"); } @@ -188,9 +188,8 @@ export const tlonPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.tlon"] }, configSchema: tlonChannelConfigSchema, config: { - listAccountIds: (cfg) => listTlonAccountIds(cfg as OpenClawConfig), - resolveAccount: (cfg, accountId) => - resolveTlonAccount(cfg as OpenClawConfig, accountId ?? undefined), + listAccountIds: (cfg) => listTlonAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveTlonAccount(cfg, accountId ?? undefined), defaultAccountId: () => "default", setAccountEnabled: ({ cfg, accountId, enabled }) => { const useDefault = !accountId || accountId === "default"; @@ -200,7 +199,7 @@ export const tlonPlugin: ChannelPlugin = { channels: { ...cfg.channels, tlon: { - ...(cfg.channels?.tlon ?? {}), + ...(cfg.channels?.tlon as Record), enabled, }, }, @@ -211,11 +210,11 @@ export const tlonPlugin: ChannelPlugin = { channels: { ...cfg.channels, tlon: { - ...(cfg.channels?.tlon ?? {}), + ...(cfg.channels?.tlon as Record), accounts: { - ...(cfg.channels?.tlon?.accounts ?? {}), + ...cfg.channels?.tlon?.accounts, [accountId]: { - ...(cfg.channels?.tlon?.accounts?.[accountId] ?? {}), + ...cfg.channels?.tlon?.accounts?.[accountId], enabled, }, }, @@ -226,6 +225,8 @@ export const tlonPlugin: ChannelPlugin = { deleteAccount: ({ cfg, accountId }) => { const useDefault = !accountId || accountId === "default"; if (useDefault) { + // @ts-expect-error + // oxlint-disable-next-line no-unused-vars const { ship, code, url, name, ...rest } = cfg.channels?.tlon ?? {}; return { ...cfg, @@ -235,13 +236,15 @@ export const tlonPlugin: ChannelPlugin = { }, } as OpenClawConfig; } + // @ts-expect-error + // oxlint-disable-next-line no-unused-vars const { [accountId]: removed, ...remainingAccounts } = cfg.channels?.tlon?.accounts ?? {}; return { ...cfg, channels: { ...cfg.channels, tlon: { - ...(cfg.channels?.tlon ?? {}), + ...(cfg.channels?.tlon as Record), accounts: remainingAccounts, }, }, @@ -261,25 +264,31 @@ export const tlonPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "tlon", accountId, name, }), validateInput: ({ cfg, accountId, input }) => { const setupInput = input as TlonSetupInput; - const resolved = resolveTlonAccount(cfg as OpenClawConfig, accountId ?? undefined); + const resolved = resolveTlonAccount(cfg, accountId ?? undefined); const ship = setupInput.ship?.trim() || resolved.ship; const url = setupInput.url?.trim() || resolved.url; const code = setupInput.code?.trim() || resolved.code; - if (!ship) return "Tlon requires --ship."; - if (!url) return "Tlon requires --url."; - if (!code) return "Tlon requires --code."; + if (!ship) { + return "Tlon requires --ship."; + } + if (!url) { + return "Tlon requires --url."; + } + if (!code) { + return "Tlon requires --code."; + } return null; }, applyAccountConfig: ({ cfg, accountId, input }) => applyTlonSetupConfig({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId, input: input as TlonSetupInput, }), @@ -287,8 +296,12 @@ export const tlonPlugin: ChannelPlugin = { messaging: { normalizeTarget: (target) => { const parsed = parseTlonTarget(target); - if (!parsed) return target.trim(); - if (parsed.kind === "dm") return parsed.ship; + if (!parsed) { + return target.trim(); + } + if (parsed.kind === "dm") { + return parsed.ship; + } return parsed.nest; }, targetResolver: { diff --git a/extensions/tlon/src/monitor/history.ts b/extensions/tlon/src/monitor/history.ts index 7333d39b6..f9fc39962 100644 --- a/extensions/tlon/src/monitor/history.ts +++ b/extensions/tlon/src/monitor/history.ts @@ -17,7 +17,9 @@ export function cacheMessage(channelNest: string, message: TlonHistoryEntry) { messageCache.set(channelNest, []); } const cache = messageCache.get(channelNest); - if (!cache) return; + if (!cache) { + return; + } cache.unshift(message); if (cache.length > MAX_CACHED_MESSAGES) { cache.pop(); @@ -35,7 +37,9 @@ export async function fetchChannelHistory( runtime?.log?.(`[tlon] Fetching history: ${scryPath}`); const data: any = await api.scry(scryPath); - if (!data) return []; + if (!data) { + return []; + } let posts: any[] = []; if (Array.isArray(data)) { diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index 400d58549..6b4af5a7c 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -49,8 +49,10 @@ function resolveChannelAuthorization( export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { const core = getTlonRuntime(); - const cfg = core.config.loadConfig() as OpenClawConfig; - if (cfg.channels?.tlon?.enabled === false) return; + const cfg = core.config.loadConfig(); + if (cfg.channels?.tlon?.enabled === false) { + return; + } const logger = core.logging.getChildLogger({ module: "tlon-auto-reply" }); const formatRuntimeMessage = (...args: Parameters) => format(...args); @@ -67,7 +69,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { try { const memo = update?.response?.add?.memo; - if (!memo) return; + if (!memo) { + return; + } const messageId = update.id as string | undefined; - if (!processedTracker.mark(messageId)) return; + if (!processedTracker.mark(messageId)) { + return; + } const senderShip = normalizeShip(memo.author ?? ""); - if (!senderShip || senderShip === botShipName) return; + if (!senderShip || senderShip === botShipName) { + return; + } const messageText = extractMessageText(memo.content); - if (!messageText) return; + if (!messageText) { + return; + } if (!isDmAllowed(senderShip, account.dmAllowlist)) { runtime.log?.(`[tlon] Blocked DM from ${senderShip}: not in allowlist`); @@ -152,11 +164,15 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise async (update: any) => { try { const parsed = parseChannelNest(channelNest); - if (!parsed) return; + if (!parsed) { + return; + } const essay = update?.response?.post?.["r-post"]?.set?.essay; const memo = update?.response?.post?.["r-post"]?.reply?.["r-reply"]?.set?.memo; - if (!essay && !memo) return; + if (!essay && !memo) { + return; + } const content = memo || essay; const isThreadReply = Boolean(memo); @@ -164,13 +180,19 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { let replyText = payload.text; - if (!replyText) return; + if (!replyText) { + return; + } const showSignature = account.showModelSignature ?? cfg.channels?.tlon?.showModelSignature ?? false; @@ -360,9 +386,11 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { @@ -387,7 +415,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise(); async function subscribeToChannel(channelNest: string) { - if (subscribedChannels.has(channelNest)) return; + if (subscribedChannels.has(channelNest)) { + return; + } const parsed = parseChannelNest(channelNest); if (!parsed) { runtime.error?.(`[tlon] Invalid channel format: ${channelNest}`); @@ -417,7 +447,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { const trimmed = id?.trim(); - if (!trimmed) return true; - if (seen.has(trimmed)) return false; + if (!trimmed) { + return true; + } + if (seen.has(trimmed)) { + return false; + } seen.add(trimmed); order.push(trimmed); if (order.length > limit) { const overflow = order.length - limit; for (let i = 0; i < overflow; i += 1) { const oldest = order.shift(); - if (oldest) seen.delete(oldest); + if (oldest) { + seen.delete(oldest); + } } } return true; @@ -26,7 +32,9 @@ export function createProcessedMessageTracker(limit = 2000): ProcessedMessageTra const has = (id?: string | null) => { const trimmed = id?.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } return seen.has(trimmed); }; diff --git a/extensions/tlon/src/monitor/utils.ts b/extensions/tlon/src/monitor/utils.ts index 808bd4757..6c5e2c3c4 100644 --- a/extensions/tlon/src/monitor/utils.ts +++ b/extensions/tlon/src/monitor/utils.ts @@ -1,7 +1,9 @@ import { normalizeShip } from "../targets.js"; export function formatModelName(modelString?: string | null): string { - if (!modelString) return "AI"; + if (!modelString) { + return "AI"; + } const modelName = modelString.includes("/") ? modelString.split("/")[1] : modelString; const modelMappings: Record = { "claude-opus-4-5": "Claude Opus 4.5", @@ -14,7 +16,9 @@ export function formatModelName(modelString?: string | null): string { "gemini-pro": "Gemini Pro", }; - if (modelMappings[modelName]) return modelMappings[modelName]; + if (modelMappings[modelName]) { + return modelMappings[modelName]; + } return modelName .replace(/-/g, " ") .split(" ") @@ -23,7 +27,9 @@ export function formatModelName(modelString?: string | null): string { } export function isBotMentioned(messageText: string, botShipName: string): boolean { - if (!messageText || !botShipName) return false; + if (!messageText || !botShipName) { + return false; + } const normalizedBotShip = normalizeShip(botShipName); const escapedShip = normalizedBotShip.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const mentionPattern = new RegExp(`(^|\\s)${escapedShip}(?=\\s|$)`, "i"); @@ -31,24 +37,36 @@ export function isBotMentioned(messageText: string, botShipName: string): boolea } export function isDmAllowed(senderShip: string, allowlist: string[] | undefined): boolean { - if (!allowlist || allowlist.length === 0) return true; + if (!allowlist || allowlist.length === 0) { + return true; + } const normalizedSender = normalizeShip(senderShip); return allowlist.map((ship) => normalizeShip(ship)).some((ship) => ship === normalizedSender); } export function extractMessageText(content: unknown): string { - if (!content || !Array.isArray(content)) return ""; + if (!content || !Array.isArray(content)) { + return ""; + } return content .map((block: any) => { if (block.inline && Array.isArray(block.inline)) { return block.inline .map((item: any) => { - if (typeof item === "string") return item; + if (typeof item === "string") { + return item; + } if (item && typeof item === "object") { - if (item.ship) return item.ship; - if (item.break !== undefined) return "\n"; - if (item.link && item.link.href) return item.link.href; + if (item.ship) { + return item.ship; + } + if (item.break !== undefined) { + return "\n"; + } + if (item.link && item.link.href) { + return item.link.href; + } } return ""; }) diff --git a/extensions/tlon/src/onboarding.ts b/extensions/tlon/src/onboarding.ts index c039d84a8..22bfb4508 100644 --- a/extensions/tlon/src/onboarding.ts +++ b/extensions/tlon/src/onboarding.ts @@ -66,9 +66,9 @@ function applyAccountConfig(params: { accounts: { ...(base as { accounts?: Record }).accounts, [accountId]: { - ...((base as { accounts?: Record> }).accounts?.[ + ...(base as { accounts?: Record> }).accounts?.[ accountId - ] ?? {}), + ], enabled: true, ...(input.name ? { name: input.name } : {}), ...(input.ship ? { ship: input.ship } : {}), diff --git a/extensions/tlon/src/targets.ts b/extensions/tlon/src/targets.ts index 7f1d9f28c..bacc6d576 100644 --- a/extensions/tlon/src/targets.ts +++ b/extensions/tlon/src/targets.ts @@ -7,13 +7,17 @@ const NEST_RE = /^chat\/([^/]+)\/([^/]+)$/i; export function normalizeShip(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } return trimmed.startsWith("~") ? trimmed : `~${trimmed}`; } export function parseChannelNest(raw: string): { hostShip: string; channelName: string } | null { const match = NEST_RE.exec(raw.trim()); - if (!match) return null; + if (!match) { + return null; + } const hostShip = normalizeShip(match[1]); const channelName = match[2]; return { hostShip, channelName }; @@ -21,7 +25,9 @@ export function parseChannelNest(raw: string): { hostShip: string; channelName: export function parseTlonTarget(raw?: string | null): TlonTarget | null { const trimmed = raw?.trim(); - if (!trimmed) return null; + if (!trimmed) { + return null; + } const withoutPrefix = trimmed.replace(/^tlon:/i, ""); const dmPrefix = withoutPrefix.match(/^dm[/:](.+)$/i); @@ -34,7 +40,9 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null { const groupTarget = groupPrefix[2].trim(); if (groupTarget.startsWith("chat/")) { const parsed = parseChannelNest(groupTarget); - if (!parsed) return null; + if (!parsed) { + return null; + } return { kind: "group", nest: `chat/${parsed.hostShip}/${parsed.channelName}`, @@ -58,7 +66,9 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null { if (withoutPrefix.startsWith("chat/")) { const parsed = parseChannelNest(withoutPrefix); - if (!parsed) return null; + if (!parsed) { + return null; + } return { kind: "group", nest: `chat/${parsed.hostShip}/${parsed.channelName}`, diff --git a/extensions/tlon/src/types.ts b/extensions/tlon/src/types.ts index 78bac6c50..408315468 100644 --- a/extensions/tlon/src/types.ts +++ b/extensions/tlon/src/types.ts @@ -50,9 +50,7 @@ export function resolveTlonAccount( } const useDefault = !accountId || accountId === "default"; - const account = useDefault - ? base - : (base.accounts?.[accountId] as Record | undefined); + const account = useDefault ? base : base.accounts?.[accountId]; const ship = (account?.ship ?? base.ship ?? null) as string | null; const url = (account?.url ?? base.url ?? null) as string | null; @@ -86,7 +84,9 @@ export function listTlonAccountIds(cfg: OpenClawConfig): string[] { const base = cfg.channels?.tlon as | { ship?: string; accounts?: Record> } | undefined; - if (!base) return []; + if (!base) { + return []; + } const accounts = base.accounts ?? {}; return [...(base.ship ? ["default"] : []), ...Object.keys(accounts)]; } diff --git a/extensions/tlon/src/urbit/http-api.ts b/extensions/tlon/src/urbit/http-api.ts index 61ff72371..13edb97b8 100644 --- a/extensions/tlon/src/urbit/http-api.ts +++ b/extensions/tlon/src/urbit/http-api.ts @@ -3,7 +3,9 @@ import { Urbit } from "@urbit/http-api"; let patched = false; export function ensureUrbitConnectPatched() { - if (patched) return; + if (patched) { + return; + } patched = true; Urbit.prototype.connect = async function patchedConnect() { const resp = await fetch(`${this.url}/~/login`, { diff --git a/extensions/tlon/src/urbit/send.ts b/extensions/tlon/src/urbit/send.ts index bd944eaa8..5d5b8e9d8 100644 --- a/extensions/tlon/src/urbit/send.ts +++ b/extensions/tlon/src/urbit/send.ts @@ -121,7 +121,11 @@ export async function sendGroupMessage({ export function buildMediaText(text: string | undefined, mediaUrl: string | undefined): string { const cleanText = text?.trim() ?? ""; const cleanUrl = mediaUrl?.trim() ?? ""; - if (cleanText && cleanUrl) return `${cleanText}\n${cleanUrl}`; - if (cleanUrl) return cleanUrl; + if (cleanText && cleanUrl) { + return `${cleanText}\n${cleanUrl}`; + } + if (cleanUrl) { + return cleanUrl; + } return cleanText; } diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 19878e679..ec131dd1b 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -180,20 +180,26 @@ export class UrbitSSEClient { if (!this.aborted) { this.logger.error?.(`Stream error: ${String(error)}`); for (const { err } of this.eventHandlers.values()) { - if (err) err(error); + if (err) { + err(error); + } } } }); } async processStream(body: ReadableStream | Readable | null) { - if (!body) return; + if (!body) { + return; + } const stream = body instanceof ReadableStream ? Readable.fromWeb(body) : body; let buffer = ""; try { for await (const chunk of stream) { - if (this.aborted) break; + if (this.aborted) { + break; + } buffer += chunk.toString(); let eventEnd; while ((eventEnd = buffer.indexOf("\n\n")) !== -1) { @@ -221,7 +227,9 @@ export class UrbitSSEClient { } } - if (!data) return; + if (!data) { + return; + } try { const parsed = JSON.parse(data) as { id?: number; json?: unknown; response?: string }; @@ -229,7 +237,9 @@ export class UrbitSSEClient { if (parsed.response === "quit") { if (parsed.id) { const handlers = this.eventHandlers.get(parsed.id); - if (handlers?.quit) handlers.quit(); + if (handlers?.quit) { + handlers.quit(); + } } return; } @@ -241,7 +251,9 @@ export class UrbitSSEClient { } } else if (parsed.json) { for (const { event } of this.eventHandlers.values()) { - if (event) event(parsed.json); + if (event) { + event(parsed.json); + } } } } catch (error) { diff --git a/extensions/twitch/src/access-control.ts b/extensions/twitch/src/access-control.ts index 0ce86d78b..51328b12e 100644 --- a/extensions/twitch/src/access-control.ts +++ b/extensions/twitch/src/access-control.ts @@ -114,16 +114,24 @@ function checkSenderRoles(params: { message: TwitchChatMessage; allowedRoles: st for (const role of allowedRoles) { switch (role) { case "moderator": - if (isMod) return true; + if (isMod) { + return true; + } break; case "owner": - if (isOwner) return true; + if (isOwner) { + return true; + } break; case "vip": - if (isVip) return true; + if (isVip) { + return true; + } break; case "subscriber": - if (isSub) return true; + if (isSub) { + return true; + } break; } } diff --git a/extensions/twitch/src/monitor.ts b/extensions/twitch/src/monitor.ts index e94a8a03a..c78b6f9e6 100644 --- a/extensions/twitch/src/monitor.ts +++ b/extensions/twitch/src/monitor.ts @@ -137,7 +137,7 @@ async function deliverTwitchReply(params: { runtime: TwitchRuntimeEnv; statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; }): Promise { - const { payload, channel, account, accountId, config, tableMode, runtime, statusSink } = params; + const { payload, channel, account, accountId, config, runtime, statusSink } = params; try { const clientManager = getOrCreateClientManager(accountId, { @@ -187,7 +187,9 @@ export async function monitorTwitchProvider( const coreLogger = core.logging.getChildLogger({ module: "twitch" }); const logVerboseMessage = (message: string) => { - if (!core.logging.shouldLogVerbose()) return; + if (!core.logging.shouldLogVerbose()) { + return; + } coreLogger.debug?.(message); }; const logger = { @@ -212,7 +214,9 @@ export async function monitorTwitchProvider( } const unregisterHandler = clientManager.onMessage(account, (message) => { - if (stopped) return; + if (stopped) { + return; + } // Access control check const botUsername = account.username.toLowerCase(); diff --git a/extensions/twitch/src/onboarding.ts b/extensions/twitch/src/onboarding.ts index fff390f1d..2768afd44 100644 --- a/extensions/twitch/src/onboarding.ts +++ b/extensions/twitch/src/onboarding.ts @@ -105,7 +105,9 @@ async function promptToken( initialValue: envToken ?? "", validate: (value) => { const raw = String(value ?? "").trim(); - if (!raw) return "Required"; + if (!raw) { + return "Required"; + } if (!raw.startsWith("oauth:")) { return "Token should start with 'oauth:'"; } @@ -265,14 +267,18 @@ const dmPolicy: ChannelOnboardingDmPolicy = { getCurrent: (cfg) => { const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); // Map allowedRoles to policy equivalent - if (account?.allowedRoles?.includes("all")) return "open"; - if (account?.allowFrom && account.allowFrom.length > 0) return "allowlist"; + if (account?.allowedRoles?.includes("all")) { + return "open"; + } + if (account?.allowFrom && account.allowFrom.length > 0) { + return "allowlist"; + } return "disabled"; }, setPolicy: (cfg, policy) => { const allowedRoles: TwitchRole[] = policy === "open" ? ["all"] : policy === "allowlist" ? [] : ["moderator"]; - return setTwitchAccessControl(cfg as OpenClawConfig, allowedRoles, true); + return setTwitchAccessControl(cfg, allowedRoles, true); }, promptAllowFrom: async ({ cfg, prompter }) => { const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); @@ -289,7 +295,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = { .map((s) => s.trim()) .filter(Boolean); - return setTwitchAccount(cfg as OpenClawConfig, { + return setTwitchAccount(cfg, { ...(account ?? undefined), allowFrom, }); diff --git a/extensions/twitch/src/outbound.ts b/extensions/twitch/src/outbound.ts index ad67d8d32..76700f42f 100644 --- a/extensions/twitch/src/outbound.ts +++ b/extensions/twitch/src/outbound.ts @@ -65,7 +65,7 @@ export const twitchOutbound: ChannelOutboundAdapter = { } // Fallback to first allowFrom entry // biome-ignore lint/style/noNonNullAssertion: length > 0 check ensures element exists - return { ok: true, to: allowList[0]! }; + return { ok: true, to: allowList[0] }; } // For explicit mode, accept any valid channel name @@ -75,7 +75,7 @@ export const twitchOutbound: ChannelOutboundAdapter = { // No target provided, use allowFrom fallback if (allowList.length > 0) { // biome-ignore lint/style/noNonNullAssertion: length > 0 check ensures element exists - return { ok: true, to: allowList[0]! }; + return { ok: true, to: allowList[0] }; } // No target and no allowFrom - error diff --git a/extensions/twitch/src/plugin.ts b/extensions/twitch/src/plugin.ts index fe02047de..800994c62 100644 --- a/extensions/twitch/src/plugin.ts +++ b/extensions/twitch/src/plugin.ts @@ -231,7 +231,7 @@ export const twitchPlugin: ChannelPlugin = { gateway: { /** Start an account connection */ startAccount: async (ctx): Promise => { - const account = ctx.account as TwitchAccountConfig; + const account = ctx.account; const accountId = ctx.accountId; ctx.setStatus?.({ @@ -256,7 +256,7 @@ export const twitchPlugin: ChannelPlugin = { /** Stop an account connection */ stopAccount: async (ctx): Promise => { - const account = ctx.account as TwitchAccountConfig; + const account = ctx.account; const accountId = ctx.accountId; // Disconnect and remove client manager from registry diff --git a/extensions/twitch/src/probe.test.ts b/extensions/twitch/src/probe.test.ts index 21d43ee18..5972cdaf0 100644 --- a/extensions/twitch/src/probe.test.ts +++ b/extensions/twitch/src/probe.test.ts @@ -8,7 +8,6 @@ const mockUnbind = vi.fn(); // Event handler storage let connectHandler: (() => void) | null = null; let disconnectHandler: ((manually: boolean, reason?: Error) => void) | null = null; -let authFailHandler: (() => void) | null = null; // Event listener mocks that store handlers and return unbind function const mockOnConnect = vi.fn((handler: () => void) => { @@ -21,8 +20,7 @@ const mockOnDisconnect = vi.fn((handler: (manually: boolean, reason?: Error) => return { unbind: mockUnbind }; }); -const mockOnAuthenticationFailure = vi.fn((handler: () => void) => { - authFailHandler = handler; +const mockOnAuthenticationFailure = vi.fn((_handler: () => void) => { return { unbind: mockUnbind }; }); @@ -65,7 +63,6 @@ describe("probeTwitch", () => { // Reset handlers connectHandler = null; disconnectHandler = null; - authFailHandler = null; }); it("returns error when username is missing", async () => { diff --git a/extensions/twitch/src/probe.ts b/extensions/twitch/src/probe.ts index 90e34826b..6e84d4933 100644 --- a/extensions/twitch/src/probe.ts +++ b/extensions/twitch/src/probe.ts @@ -55,7 +55,9 @@ export async function probeTwitch( let authFailListener: ReturnType | undefined; const cleanup = () => { - if (settled) return; + if (settled) { + return; + } settled = true; connectListener?.unbind(); disconnectListener?.unbind(); diff --git a/extensions/twitch/src/status.ts b/extensions/twitch/src/status.ts index b2a488e66..827fec79b 100644 --- a/extensions/twitch/src/status.ts +++ b/extensions/twitch/src/status.ts @@ -35,7 +35,9 @@ export function collectTwitchStatusIssues( for (const entry of accounts) { const accountId = entry.accountId; - if (!accountId) continue; + if (!accountId) { + continue; + } let account: ReturnType | null = null; let cfg: Parameters[0] | undefined; diff --git a/extensions/twitch/src/token.ts b/extensions/twitch/src/token.ts index 733160dc5..4c3eae6a2 100644 --- a/extensions/twitch/src/token.ts +++ b/extensions/twitch/src/token.ts @@ -23,9 +23,13 @@ export type TwitchTokenResolution = { * Normalize a Twitch OAuth token - ensure it has the oauth: prefix */ function normalizeTwitchToken(raw?: string | null): string | undefined { - if (!raw) return undefined; + if (!raw) { + return undefined; + } const trimmed = raw.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } // Twitch tokens should have oauth: prefix return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`; } @@ -55,7 +59,7 @@ export function resolveTwitchToken( const accountCfg = accountId === DEFAULT_ACCOUNT_ID ? (twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID] as Record | undefined) - : (twitchCfg?.accounts?.[accountId as string] as Record | undefined); + : (twitchCfg?.accounts?.[accountId] as Record | undefined); // For default account, also check base-level config let token: string | undefined; diff --git a/extensions/twitch/src/twitch-client.test.ts b/extensions/twitch/src/twitch-client.test.ts index b6e270acd..76adfa7b9 100644 --- a/extensions/twitch/src/twitch-client.test.ts +++ b/extensions/twitch/src/twitch-client.test.ts @@ -416,7 +416,9 @@ describe("TwitchClientManager", () => { // Get the onMessage callback const onMessageCallback = messageHandlers[0]; - if (!onMessageCallback) throw new Error("onMessageCallback not found"); + if (!onMessageCallback) { + throw new Error("onMessageCallback not found"); + } // Simulate Twitch message onMessageCallback("#testchannel", "testuser", "Hello bot!", { @@ -521,7 +523,9 @@ describe("TwitchClientManager", () => { // Simulate message for first account const onMessage1 = messageHandlers[0]; - if (!onMessage1) throw new Error("onMessage1 not found"); + if (!onMessage1) { + throw new Error("onMessage1 not found"); + } onMessage1("#testchannel", "user1", "msg1", { userInfo: { userName: "user1", @@ -537,7 +541,9 @@ describe("TwitchClientManager", () => { // Simulate message for second account const onMessage2 = messageHandlers[1]; - if (!onMessage2) throw new Error("onMessage2 not found"); + if (!onMessage2) { + throw new Error("onMessage2 not found"); + } onMessage2("#testchannel2", "user2", "msg2", { userInfo: { userName: "user2", diff --git a/extensions/twitch/src/utils/markdown.ts b/extensions/twitch/src/utils/markdown.ts index c50741b9a..2f3c591c7 100644 --- a/extensions/twitch/src/utils/markdown.ts +++ b/extensions/twitch/src/utils/markdown.ts @@ -61,9 +61,15 @@ export function stripMarkdownForTwitch(markdown: string): string { export function chunkTextForTwitch(text: string, limit: number): string[] { // First, strip markdown const cleaned = stripMarkdownForTwitch(text); - if (!cleaned) return []; - if (limit <= 0) return [cleaned]; - if (cleaned.length <= limit) return [cleaned]; + if (!cleaned) { + return []; + } + if (limit <= 0) { + return [cleaned]; + } + if (cleaned.length <= limit) { + return [cleaned]; + } const chunks: string[] = []; let remaining = cleaned; diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index f2510e1d0..1e5c9c804 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -169,7 +169,9 @@ const voiceCallPlugin = { if (!validation.valid) { throw new Error(validation.errors.join("; ")); } - if (runtime) return runtime; + if (runtime) { + return runtime; + } if (!runtimePromise) { runtimePromise = createVoiceCallRuntime({ config, @@ -341,12 +343,16 @@ const voiceCallPlugin = { switch (params.action) { case "initiate_call": { const message = String(params.message || "").trim(); - if (!message) throw new Error("message required"); + if (!message) { + throw new Error("message required"); + } const to = typeof params.to === "string" && params.to.trim() ? params.to.trim() : rt.config.toNumber; - if (!to) throw new Error("to required"); + if (!to) { + throw new Error("to required"); + } const result = await rt.manager.initiateCall(to, undefined, { message, mode: @@ -385,7 +391,9 @@ const voiceCallPlugin = { } case "end_call": { const callId = String(params.callId || "").trim(); - if (!callId) throw new Error("callId required"); + if (!callId) { + throw new Error("callId required"); + } const result = await rt.manager.endCall(callId); if (!result.success) { throw new Error(result.error || "end failed"); @@ -394,7 +402,9 @@ const voiceCallPlugin = { } case "get_status": { const callId = String(params.callId || "").trim(); - if (!callId) throw new Error("callId required"); + if (!callId) { + throw new Error("callId required"); + } const call = rt.manager.getCall(callId) || rt.manager.getCallByProviderCallId(callId); return json(call ? { found: true, call } : { found: false }); @@ -405,7 +415,9 @@ const voiceCallPlugin = { const mode = params?.mode ?? "call"; if (mode === "status") { const sid = typeof params.sid === "string" ? params.sid.trim() : ""; - if (!sid) throw new Error("sid required for status"); + if (!sid) { + throw new Error("sid required for status"); + } const call = rt.manager.getCall(sid) || rt.manager.getCallByProviderCallId(sid); return json(call ? { found: true, call } : { found: false }); } @@ -414,7 +426,9 @@ const voiceCallPlugin = { typeof params.to === "string" && params.to.trim() ? params.to.trim() : rt.config.toNumber; - if (!to) throw new Error("to required for call"); + if (!to) { + throw new Error("to required for call"); + } const result = await rt.manager.initiateCall(to, undefined, { message: typeof params.message === "string" && params.message.trim() @@ -447,7 +461,9 @@ const voiceCallPlugin = { api.registerService({ id: "voicecall", start: async () => { - if (!config.enabled) return; + if (!config.enabled) { + return; + } try { await ensureRuntime(); } catch (err) { @@ -459,7 +475,9 @@ const voiceCallPlugin = { } }, stop: async () => { - if (!runtimePromise) return; + if (!runtimePromise) { + return; + } try { const rt = await runtimePromise; await rt.stop(); diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index eb90398ae..bb42dc3fc 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -21,7 +21,9 @@ type Logger = { function resolveMode(input: string): "off" | "serve" | "funnel" { const raw = input.trim().toLowerCase(); - if (raw === "serve" || raw === "off") return raw; + if (raw === "serve" || raw === "off") { + return raw; + } return "funnel"; } diff --git a/extensions/voice-call/src/core-bridge.ts b/extensions/voice-call/src/core-bridge.ts index ca2cf75e8..b8f57a5d2 100644 --- a/extensions/voice-call/src/core-bridge.ts +++ b/extensions/voice-call/src/core-bridge.ts @@ -72,19 +72,25 @@ function findPackageRoot(startDir: string, name: string): string | null { if (fs.existsSync(pkgPath)) { const raw = fs.readFileSync(pkgPath, "utf8"); const pkg = JSON.parse(raw) as { name?: string }; - if (pkg.name === name) return dir; + if (pkg.name === name) { + return dir; + } } } catch { // ignore parse errors and keep walking } const parent = path.dirname(dir); - if (parent === dir) return null; + if (parent === dir) { + return null; + } dir = parent; } } function resolveOpenClawRoot(): string { - if (coreRootCache) return coreRootCache; + if (coreRootCache) { + return coreRootCache; + } const override = process.env.OPENCLAW_ROOT?.trim(); if (override) { coreRootCache = override; @@ -128,7 +134,9 @@ async function importCoreModule(relativePath: string): Promise { } export async function loadCoreAgentDeps(): Promise { - if (coreDepsPromise) return coreDepsPromise; + if (coreDepsPromise) { + return coreDepsPromise; + } coreDepsPromise = (async () => { const [ diff --git a/extensions/voice-call/src/manager.ts b/extensions/voice-call/src/manager.ts index fe0e010e3..8ffbf855f 100644 --- a/extensions/voice-call/src/manager.ts +++ b/extensions/voice-call/src/manager.ts @@ -21,7 +21,9 @@ import { escapeXml, mapVoiceToPolly } from "./voice-mapping.js"; function resolveDefaultStoreBase(config: VoiceCallConfig, storePath?: string): string { const rawOverride = storePath?.trim() || config.store?.trim(); - if (rawOverride) return resolveUserPath(rawOverride); + if (rawOverride) { + return resolveUserPath(rawOverride); + } const preferred = path.join(os.homedir(), ".openclaw", "voice-calls"); const candidates = [preferred].map((dir) => resolveUserPath(dir)); const existing = @@ -322,21 +324,27 @@ export class CallManager { private clearTranscriptWaiter(callId: CallId): void { const waiter = this.transcriptWaiters.get(callId); - if (!waiter) return; + if (!waiter) { + return; + } clearTimeout(waiter.timeout); this.transcriptWaiters.delete(callId); } private rejectTranscriptWaiter(callId: CallId, reason: string): void { const waiter = this.transcriptWaiters.get(callId); - if (!waiter) return; + if (!waiter) { + return; + } this.clearTranscriptWaiter(callId); waiter.reject(new Error(reason)); } private resolveTranscriptWaiter(callId: CallId, transcript: string): void { const waiter = this.transcriptWaiters.get(callId); - if (!waiter) return; + if (!waiter) { + return; + } this.clearTranscriptWaiter(callId); waiter.resolve(transcript); } @@ -520,7 +528,9 @@ export class CallManager { private findCall(callIdOrProviderCallId: string): CallRecord | undefined { // Try direct lookup by internal callId const directCall = this.activeCalls.get(callIdOrProviderCallId); - if (directCall) return directCall; + if (directCall) { + return directCall; + } // Try lookup by providerCallId return this.getCallByProviderCallId(callIdOrProviderCallId); @@ -648,13 +658,19 @@ export class CallManager { const initialMessage = typeof call.metadata?.initialMessage === "string" ? call.metadata.initialMessage.trim() : ""; - if (!initialMessage) return; + if (!initialMessage) { + return; + } - if (!this.provider || !call.providerCallId) return; + if (!this.provider || !call.providerCallId) { + return; + } // Twilio has provider-specific state for speaking ( fallback) and can // fail for inbound calls; keep existing Twilio behavior unchanged. - if (this.provider.name === "twilio") return; + if (this.provider.name === "twilio") { + return; + } void this.speakInitialMessage(call.providerCallId); } @@ -740,7 +756,9 @@ export class CallManager { */ private transitionState(call: CallRecord, newState: CallState): void { // No-op for same state or already terminal - if (call.state === newState || TerminalStates.has(call.state)) return; + if (call.state === newState || TerminalStates.has(call.state)) { + return; + } // Terminal states can always be reached from non-terminal if (TerminalStates.has(newState)) { @@ -797,7 +815,9 @@ export class CallManager { */ private loadActiveCalls(): void { const logPath = path.join(this.storePath, "calls.jsonl"); - if (!fs.existsSync(logPath)) return; + if (!fs.existsSync(logPath)) { + return; + } // Read file synchronously and parse lines const content = fs.readFileSync(logPath, "utf-8"); @@ -807,7 +827,9 @@ export class CallManager { const callMap = new Map(); for (const line of lines) { - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } try { const call = CallRecordSchema.parse(JSON.parse(line)); callMap.set(call.callId, call); diff --git a/extensions/voice-call/src/manager/events.ts b/extensions/voice-call/src/manager/events.ts index 5dd6e1951..76c6f1702 100644 --- a/extensions/voice-call/src/manager/events.ts +++ b/extensions/voice-call/src/manager/events.ts @@ -1,7 +1,6 @@ import crypto from "node:crypto"; -import type { CallId, CallRecord, CallState, NormalizedEvent } from "../types.js"; -import { TerminalStates } from "../types.js"; +import type { CallRecord, CallState, NormalizedEvent } from "../types.js"; import type { CallManagerContext } from "./context.js"; import { findCall } from "./lookup.js"; import { addTranscriptEntry, transitionState } from "./state.js"; @@ -81,7 +80,9 @@ function createInboundCall(params: { } export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): void { - if (ctx.processedEventIds.has(event.id)) return; + if (ctx.processedEventIds.has(event.id)) { + return; + } ctx.processedEventIds.add(event.id); let call = findCall({ @@ -107,7 +108,9 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v event.callId = call.callId; } - if (!call) return; + if (!call) { + return; + } if (event.providerCallId && !call.providerCallId) { call.providerCallId = event.providerCallId; @@ -160,7 +163,9 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v clearMaxDurationTimer(ctx, call.callId); rejectTranscriptWaiter(ctx, call.callId, `Call ended: ${event.reason}`); ctx.activeCalls.delete(call.callId); - if (call.providerCallId) ctx.providerCallIdMap.delete(call.providerCallId); + if (call.providerCallId) { + ctx.providerCallIdMap.delete(call.providerCallId); + } break; case "call.error": @@ -171,7 +176,9 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v clearMaxDurationTimer(ctx, call.callId); rejectTranscriptWaiter(ctx, call.callId, `Call error: ${event.error}`); ctx.activeCalls.delete(call.callId); - if (call.providerCallId) ctx.providerCallIdMap.delete(call.providerCallId); + if (call.providerCallId) { + ctx.providerCallIdMap.delete(call.providerCallId); + } } break; } diff --git a/extensions/voice-call/src/manager/lookup.ts b/extensions/voice-call/src/manager/lookup.ts index 99ac246b1..1d3856ce5 100644 --- a/extensions/voice-call/src/manager/lookup.ts +++ b/extensions/voice-call/src/manager/lookup.ts @@ -24,7 +24,9 @@ export function findCall(params: { callIdOrProviderCallId: string; }): CallRecord | undefined { const directCall = params.activeCalls.get(params.callIdOrProviderCallId); - if (directCall) return directCall; + if (directCall) { + return directCall; + } return getCallByProviderCallId({ activeCalls: params.activeCalls, providerCallIdMap: params.providerCallIdMap, diff --git a/extensions/voice-call/src/manager/outbound.ts b/extensions/voice-call/src/manager/outbound.ts index 82113751f..e681ad0ac 100644 --- a/extensions/voice-call/src/manager/outbound.ts +++ b/extensions/voice-call/src/manager/outbound.ts @@ -119,9 +119,15 @@ export async function speak( text: string, ): Promise<{ success: boolean; error?: string }> { const call = ctx.activeCalls.get(callId); - if (!call) return { success: false, error: "Call not found" }; - if (!ctx.provider || !call.providerCallId) return { success: false, error: "Call not connected" }; - if (TerminalStates.has(call.state)) return { success: false, error: "Call has ended" }; + if (!call) { + return { success: false, error: "Call not found" }; + } + if (!ctx.provider || !call.providerCallId) { + return { success: false, error: "Call not connected" }; + } + if (TerminalStates.has(call.state)) { + return { success: false, error: "Call has ended" }; + } try { transitionState(call, "speaking"); @@ -197,9 +203,15 @@ export async function continueCall( prompt: string, ): Promise<{ success: boolean; transcript?: string; error?: string }> { const call = ctx.activeCalls.get(callId); - if (!call) return { success: false, error: "Call not found" }; - if (!ctx.provider || !call.providerCallId) return { success: false, error: "Call not connected" }; - if (TerminalStates.has(call.state)) return { success: false, error: "Call has ended" }; + if (!call) { + return { success: false, error: "Call not found" }; + } + if (!ctx.provider || !call.providerCallId) { + return { success: false, error: "Call not connected" }; + } + if (TerminalStates.has(call.state)) { + return { success: false, error: "Call has ended" }; + } try { await speak(ctx, callId, prompt); @@ -227,9 +239,15 @@ export async function endCall( callId: CallId, ): Promise<{ success: boolean; error?: string }> { const call = ctx.activeCalls.get(callId); - if (!call) return { success: false, error: "Call not found" }; - if (!ctx.provider || !call.providerCallId) return { success: false, error: "Call not connected" }; - if (TerminalStates.has(call.state)) return { success: true }; + if (!call) { + return { success: false, error: "Call not found" }; + } + if (!ctx.provider || !call.providerCallId) { + return { success: false, error: "Call not connected" }; + } + if (TerminalStates.has(call.state)) { + return { success: true }; + } try { await ctx.provider.hangupCall({ @@ -247,7 +265,9 @@ export async function endCall( rejectTranscriptWaiter(ctx, callId, "Call ended: hangup-bot"); ctx.activeCalls.delete(callId); - if (call.providerCallId) ctx.providerCallIdMap.delete(call.providerCallId); + if (call.providerCallId) { + ctx.providerCallIdMap.delete(call.providerCallId); + } return { success: true }; } catch (err) { diff --git a/extensions/voice-call/src/manager/state.ts b/extensions/voice-call/src/manager/state.ts index 9cebf80ea..f95c0468c 100644 --- a/extensions/voice-call/src/manager/state.ts +++ b/extensions/voice-call/src/manager/state.ts @@ -13,7 +13,9 @@ const StateOrder: readonly CallState[] = [ export function transitionState(call: CallRecord, newState: CallState): void { // No-op for same state or already terminal. - if (call.state === newState || TerminalStates.has(call.state)) return; + if (call.state === newState || TerminalStates.has(call.state)) { + return; + } // Terminal states can always be reached from non-terminal. if (TerminalStates.has(newState)) { diff --git a/extensions/voice-call/src/manager/store.ts b/extensions/voice-call/src/manager/store.ts index 9200b684d..b6da22f70 100644 --- a/extensions/voice-call/src/manager/store.ts +++ b/extensions/voice-call/src/manager/store.ts @@ -32,7 +32,9 @@ export function loadActiveCallsFromStore(storePath: string): { const callMap = new Map(); for (const line of lines) { - if (!line.trim()) continue; + if (!line.trim()) { + continue; + } try { const call = CallRecordSchema.parse(JSON.parse(line)); callMap.set(call.callId, call); @@ -46,7 +48,9 @@ export function loadActiveCallsFromStore(storePath: string): { const processedEventIds = new Set(); for (const [callId, call] of callMap) { - if (TerminalStates.has(call.state)) continue; + if (TerminalStates.has(call.state)) { + continue; + } activeCalls.set(callId, call); if (call.providerCallId) { providerCallIdMap.set(call.providerCallId, callId); diff --git a/extensions/voice-call/src/manager/timers.ts b/extensions/voice-call/src/manager/timers.ts index 97d1e9848..8fcb89f0c 100644 --- a/extensions/voice-call/src/manager/timers.ts +++ b/extensions/voice-call/src/manager/timers.ts @@ -40,7 +40,9 @@ export function startMaxDurationTimer(params: { export function clearTranscriptWaiter(ctx: CallManagerContext, callId: CallId): void { const waiter = ctx.transcriptWaiters.get(callId); - if (!waiter) return; + if (!waiter) { + return; + } clearTimeout(waiter.timeout); ctx.transcriptWaiters.delete(callId); } @@ -51,7 +53,9 @@ export function rejectTranscriptWaiter( reason: string, ): void { const waiter = ctx.transcriptWaiters.get(callId); - if (!waiter) return; + if (!waiter) { + return; + } clearTranscriptWaiter(ctx, callId); waiter.reject(new Error(reason)); } @@ -62,7 +66,9 @@ export function resolveTranscriptWaiter( transcript: string, ): void { const waiter = ctx.transcriptWaiters.get(callId); - if (!waiter) return; + if (!waiter) { + return; + } clearTranscriptWaiter(ctx, callId); waiter.resolve(transcript); } diff --git a/extensions/voice-call/src/media-stream.ts b/extensions/voice-call/src/media-stream.ts index 63d9ec589..6a63fa5a6 100644 --- a/extensions/voice-call/src/media-stream.ts +++ b/extensions/voice-call/src/media-stream.ts @@ -295,7 +295,9 @@ export class MediaStreamHandler { private getTtsQueue(streamSid: string): TtsQueueEntry[] { const existing = this.ttsQueues.get(streamSid); - if (existing) return existing; + if (existing) { + return existing; + } const queue: TtsQueueEntry[] = []; this.ttsQueues.set(streamSid, queue); return queue; @@ -339,7 +341,9 @@ export class MediaStreamHandler { private clearTtsState(streamSid: string): void { const queue = this.ttsQueues.get(streamSid); - if (queue) queue.length = 0; + if (queue) { + queue.length = 0; + } this.ttsActiveControllers.get(streamSid)?.abort(); this.ttsActiveControllers.delete(streamSid); this.ttsPlaying.delete(streamSid); diff --git a/extensions/voice-call/src/providers/mock.ts b/extensions/voice-call/src/providers/mock.ts index ae8912abf..260bf3516 100644 --- a/extensions/voice-call/src/providers/mock.ts +++ b/extensions/voice-call/src/providers/mock.ts @@ -37,11 +37,15 @@ export class MockProvider implements VoiceCallProvider { if (Array.isArray(payload.events)) { for (const evt of payload.events) { const normalized = this.normalizeEvent(evt); - if (normalized) events.push(normalized); + if (normalized) { + events.push(normalized); + } } } else if (payload.event) { const normalized = this.normalizeEvent(payload.event); - if (normalized) events.push(normalized); + if (normalized) { + events.push(normalized); + } } return { events, statusCode: 200 }; @@ -51,7 +55,9 @@ export class MockProvider implements VoiceCallProvider { } private normalizeEvent(evt: Partial): NormalizedEvent | null { - if (!evt.type || !evt.callId) return null; + if (!evt.type || !evt.callId) { + return null; + } const base = { id: evt.id || crypto.randomUUID(), diff --git a/extensions/voice-call/src/providers/plivo.ts b/extensions/voice-call/src/providers/plivo.ts index 7488958cb..9131dc3a6 100644 --- a/extensions/voice-call/src/providers/plivo.ts +++ b/extensions/voice-call/src/providers/plivo.ts @@ -123,7 +123,9 @@ export class PlivoProvider implements VoiceCallProvider { if (flow === "xml-speak") { const callId = this.getCallIdFromQuery(ctx); const pending = callId ? this.pendingSpeakByCallId.get(callId) : undefined; - if (callId) this.pendingSpeakByCallId.delete(callId); + if (callId) { + this.pendingSpeakByCallId.delete(callId); + } const xml = pending ? PlivoProvider.xmlSpeak(pending.text, pending.locale) @@ -139,7 +141,9 @@ export class PlivoProvider implements VoiceCallProvider { if (flow === "xml-listen") { const callId = this.getCallIdFromQuery(ctx); const pending = callId ? this.pendingListenByCallId.get(callId) : undefined; - if (callId) this.pendingListenByCallId.delete(callId); + if (callId) { + this.pendingListenByCallId.delete(callId); + } const actionUrl = this.buildActionUrl(ctx, { flow: "getinput", @@ -393,7 +397,9 @@ export class PlivoProvider implements VoiceCallProvider { private static normalizeNumber(numberOrSip: string): string { const trimmed = numberOrSip.trim(); - if (trimmed.toLowerCase().startsWith("sip:")) return trimmed; + if (trimmed.toLowerCase().startsWith("sip:")) { + return trimmed; + } return trimmed.replace(/[^\d+]/g, ""); } @@ -440,12 +446,16 @@ export class PlivoProvider implements VoiceCallProvider { opts: { flow: string; callId?: string }, ): string | null { const base = PlivoProvider.baseWebhookUrlFromCtx(ctx); - if (!base) return null; + if (!base) { + return null; + } const u = new URL(base); u.searchParams.set("provider", "plivo"); u.searchParams.set("flow", opts.flow); - if (opts.callId) u.searchParams.set("callId", opts.callId); + if (opts.callId) { + u.searchParams.set("callId", opts.callId); + } return u.toString(); } @@ -478,7 +488,9 @@ export class PlivoProvider implements VoiceCallProvider { for (const key of candidates) { const value = params.get(key); - if (value && value.trim()) return value.trim(); + if (value && value.trim()) { + return value.trim(); + } } return null; } diff --git a/extensions/voice-call/src/providers/stt-openai-realtime.ts b/extensions/voice-call/src/providers/stt-openai-realtime.ts index d4e87895e..2ae83cc0f 100644 --- a/extensions/voice-call/src/providers/stt-openai-realtime.ts +++ b/extensions/voice-call/src/providers/stt-openai-realtime.ts @@ -155,7 +155,9 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession { this.ws.on("error", (error) => { console.error("[RealtimeSTT] WebSocket error:", error); - if (!this.connected) reject(error); + if (!this.connected) { + reject(error); + } }); this.ws.on("close", (code, reason) => { @@ -258,7 +260,9 @@ class OpenAIRealtimeSTTSession implements RealtimeSTTSession { } sendAudio(muLawData: Buffer): void { - if (!this.connected) return; + if (!this.connected) { + return; + } this.sendEvent({ type: "input_audio_buffer.append", audio: muLawData.toString("base64"), diff --git a/extensions/voice-call/src/providers/tts-openai.ts b/extensions/voice-call/src/providers/tts-openai.ts index 9f4cf052c..c483d6819 100644 --- a/extensions/voice-call/src/providers/tts-openai.ts +++ b/extensions/voice-call/src/providers/tts-openai.ts @@ -205,10 +205,14 @@ function linearToMulaw(sample: number): number { // Get sign bit const sign = sample < 0 ? 0x80 : 0; - if (sample < 0) sample = -sample; + if (sample < 0) { + sample = -sample; + } // Clip to prevent overflow - if (sample > CLIP) sample = CLIP; + if (sample > CLIP) { + sample = CLIP; + } // Add bias and find segment sample += BIAS; diff --git a/extensions/voice-call/src/providers/twilio.ts b/extensions/voice-call/src/providers/twilio.ts index 54c8fdeb6..be0b18f7b 100644 --- a/extensions/voice-call/src/providers/twilio.ts +++ b/extensions/voice-call/src/providers/twilio.ts @@ -85,10 +85,14 @@ export class TwilioProvider implements VoiceCallProvider { */ private deleteStoredTwimlForProviderCall(providerCallId: string): void { const webhookUrl = this.callWebhookUrls.get(providerCallId); - if (!webhookUrl) return; + if (!webhookUrl) { + return; + } const callIdMatch = webhookUrl.match(/callId=([^&]+)/); - if (!callIdMatch) return; + if (!callIdMatch) { + return; + } this.deleteStoredTwiml(callIdMatch[1]); } @@ -212,8 +216,12 @@ export class TwilioProvider implements VoiceCallProvider { * Parse Twilio direction to normalized format. */ private static parseDirection(direction: string | null): "inbound" | "outbound" | undefined { - if (direction === "inbound") return "inbound"; - if (direction === "outbound-api" || direction === "outbound-dial") return "outbound"; + if (direction === "inbound") { + return "inbound"; + } + if (direction === "outbound-api" || direction === "outbound-dial") { + return "outbound"; + } return undefined; } @@ -291,7 +299,9 @@ export class TwilioProvider implements VoiceCallProvider { * When a call is answered, connects to media stream for bidirectional audio. */ private generateTwimlResponse(ctx?: WebhookContext): string { - if (!ctx) return TwilioProvider.EMPTY_TWIML; + if (!ctx) { + return TwilioProvider.EMPTY_TWIML; + } const params = new URLSearchParams(ctx.rawBody); const type = typeof ctx.query?.type === "string" ? ctx.query.type.trim() : undefined; @@ -512,12 +522,16 @@ export class TwilioProvider implements VoiceCallProvider { // Generate audio with core TTS (returns mu-law at 8kHz) const muLawAudio = await ttsProvider.synthesizeForTelephony(text); for (const chunk of chunkAudio(muLawAudio, CHUNK_SIZE)) { - if (signal.aborted) break; + if (signal.aborted) { + break; + } handler.sendAudio(streamSid, chunk); // Pace the audio to match real-time playback await new Promise((resolve) => setTimeout(resolve, CHUNK_DELAY_MS)); - if (signal.aborted) break; + if (signal.aborted) { + break; + } } if (!signal.aborted) { diff --git a/extensions/voice-call/src/runtime.ts b/extensions/voice-call/src/runtime.ts index a0917be33..639caa5e1 100644 --- a/extensions/voice-call/src/runtime.ts +++ b/extensions/voice-call/src/runtime.ts @@ -34,7 +34,9 @@ type Logger = { }; function isLoopbackBind(bind: string | undefined): boolean { - if (!bind) return false; + if (!bind) { + return false; + } return bind === "127.0.0.1" || bind === "::1" || bind === "localhost"; } diff --git a/extensions/voice-call/src/telephony-audio.ts b/extensions/voice-call/src/telephony-audio.ts index de5cc5944..6b1ef1ec3 100644 --- a/extensions/voice-call/src/telephony-audio.ts +++ b/extensions/voice-call/src/telephony-audio.ts @@ -8,9 +8,13 @@ function clamp16(value: number): number { * Resample 16-bit PCM (little-endian mono) to 8kHz using linear interpolation. */ export function resamplePcmTo8k(input: Buffer, inputSampleRate: number): Buffer { - if (inputSampleRate === TELEPHONY_SAMPLE_RATE) return input; + if (inputSampleRate === TELEPHONY_SAMPLE_RATE) { + return input; + } const inputSamples = Math.floor(input.length / 2); - if (inputSamples === 0) return Buffer.alloc(0); + if (inputSamples === 0) { + return Buffer.alloc(0); + } const ratio = inputSampleRate / TELEPHONY_SAMPLE_RATE; const outputSamples = Math.floor(inputSamples / ratio); @@ -68,8 +72,12 @@ function linearToMulaw(sample: number): number { const CLIP = 32635; const sign = sample < 0 ? 0x80 : 0; - if (sample < 0) sample = -sample; - if (sample > CLIP) sample = CLIP; + if (sample < 0) { + sample = -sample; + } + if (sample > CLIP) { + sample = CLIP; + } sample += BIAS; let exponent = 7; diff --git a/extensions/voice-call/src/telephony-tts.ts b/extensions/voice-call/src/telephony-tts.ts index 33fd5f264..be16fae1d 100644 --- a/extensions/voice-call/src/telephony-tts.ts +++ b/extensions/voice-call/src/telephony-tts.ts @@ -45,16 +45,20 @@ export function createTelephonyTtsProvider(params: { } function applyTtsOverride(coreConfig: CoreConfig, override?: VoiceCallTtsConfig): CoreConfig { - if (!override) return coreConfig; + if (!override) { + return coreConfig; + } const base = coreConfig.messages?.tts; const merged = mergeTtsConfig(base, override); - if (!merged) return coreConfig; + if (!merged) { + return coreConfig; + } return { ...coreConfig, messages: { - ...(coreConfig.messages ?? {}), + ...coreConfig.messages, tts: merged, }, }; @@ -64,9 +68,15 @@ function mergeTtsConfig( base?: VoiceCallTtsConfig, override?: VoiceCallTtsConfig, ): VoiceCallTtsConfig | undefined { - if (!base && !override) return undefined; - if (!override) return base; - if (!base) return override; + if (!base && !override) { + return undefined; + } + if (!override) { + return base; + } + if (!base) { + return override; + } return deepMerge(base, override); } @@ -76,7 +86,9 @@ function deepMerge(base: T, override: T): T { } const result: Record = { ...base }; for (const [key, value] of Object.entries(override)) { - if (value === undefined) continue; + if (value === undefined) { + continue; + } const existing = (base as Record)[key]; if (isPlainObject(existing) && isPlainObject(value)) { result[key] = deepMerge(existing, value); diff --git a/extensions/voice-call/src/utils.ts b/extensions/voice-call/src/utils.ts index d7881989b..524b62e03 100644 --- a/extensions/voice-call/src/utils.ts +++ b/extensions/voice-call/src/utils.ts @@ -3,7 +3,9 @@ import path from "node:path"; export function resolveUserPath(input: string): string { const trimmed = input.trim(); - if (!trimmed) return trimmed; + if (!trimmed) { + return trimmed; + } if (trimmed.startsWith("~")) { const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir()); return path.resolve(expanded); diff --git a/extensions/voice-call/src/voice-mapping.ts b/extensions/voice-call/src/voice-mapping.ts index c83efea30..340a3bf2e 100644 --- a/extensions/voice-call/src/voice-mapping.ts +++ b/extensions/voice-call/src/voice-mapping.ts @@ -39,7 +39,9 @@ export const DEFAULT_POLLY_VOICE = "Polly.Joanna"; * @returns Polly voice name suitable for Twilio TwiML */ export function mapVoiceToPolly(voice: string | undefined): string { - if (!voice) return DEFAULT_POLLY_VOICE; + if (!voice) { + return DEFAULT_POLLY_VOICE; + } // Already a Polly/Google voice - pass through if (voice.startsWith("Polly.") || voice.startsWith("Google.")) { diff --git a/extensions/voice-call/src/webhook-security.test.ts b/extensions/voice-call/src/webhook-security.test.ts index 43dc4faca..f893bee08 100644 --- a/extensions/voice-call/src/webhook-security.test.ts +++ b/extensions/voice-call/src/webhook-security.test.ts @@ -29,7 +29,9 @@ function plivoV3Signature(params: { const u = new URL(params.urlWithQuery); const baseNoQuery = `${u.protocol}//${u.host}${u.pathname}`; const queryPairs: Array<[string, string]> = []; - for (const [k, v] of u.searchParams.entries()) queryPairs.push([k, v]); + for (const [k, v] of u.searchParams.entries()) { + queryPairs.push([k, v]); + } const queryMap = new Map(); for (const [k, v] of queryPairs) { @@ -37,8 +39,8 @@ function plivoV3Signature(params: { } const sortedQuery = Array.from(queryMap.keys()) - .sort() - .flatMap((k) => [...(queryMap.get(k) ?? [])].sort().map((v) => `${k}=${v}`)) + .toSorted() + .flatMap((k) => [...(queryMap.get(k) ?? [])].toSorted().map((v) => `${k}=${v}`)) .join("&"); const postParams = new URLSearchParams(params.postBody); @@ -48,8 +50,8 @@ function plivoV3Signature(params: { } const sortedPost = Array.from(postMap.keys()) - .sort() - .flatMap((k) => [...(postMap.get(k) ?? [])].sort().map((v) => `${k}${v}`)) + .toSorted() + .flatMap((k) => [...(postMap.get(k) ?? [])].toSorted().map((v) => `${k}${v}`)) .join(""); const hasPost = sortedPost.length > 0; @@ -71,7 +73,7 @@ function plivoV3Signature(params: { function twilioSignature(params: { authToken: string; url: string; postBody: string }): string { let dataToSign = params.url; - const sortedParams = Array.from(new URLSearchParams(params.postBody).entries()).sort((a, b) => + const sortedParams = Array.from(new URLSearchParams(params.postBody).entries()).toSorted((a, b) => a[0].localeCompare(b[0]), ); diff --git a/extensions/voice-call/src/webhook-security.ts b/extensions/voice-call/src/webhook-security.ts index fb07d3acc..ebefea964 100644 --- a/extensions/voice-call/src/webhook-security.ts +++ b/extensions/voice-call/src/webhook-security.ts @@ -24,7 +24,7 @@ export function validateTwilioSignature( let dataToSign = url; // Sort params alphabetically and append key+value - const sortedParams = Array.from(params.entries()).sort((a, b) => + const sortedParams = Array.from(params.entries()).toSorted((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0, ); @@ -129,9 +129,15 @@ function getHeader( } function isLoopbackAddress(address?: string): boolean { - if (!address) return false; - if (address === "127.0.0.1" || address === "::1") return true; - if (address.startsWith("::ffff:127.")) return true; + if (!address) { + return false; + } + if (address === "127.0.0.1" || address === "::1") { + return true; + } + if (address.startsWith("::ffff:127.")) { + return true; + } return false; } @@ -272,7 +278,9 @@ type PlivoParamMap = Record; function toParamMapFromSearchParams(sp: URLSearchParams): PlivoParamMap { const map: PlivoParamMap = {}; for (const [key, value] of sp.entries()) { - if (!map[key]) map[key] = []; + if (!map[key]) { + map[key] = []; + } map[key].push(value); } return map; @@ -280,8 +288,8 @@ function toParamMapFromSearchParams(sp: URLSearchParams): PlivoParamMap { function sortedQueryString(params: PlivoParamMap): string { const parts: string[] = []; - for (const key of Object.keys(params).sort()) { - const values = [...params[key]].sort(); + for (const key of Object.keys(params).toSorted()) { + const values = [...params[key]].toSorted(); for (const value of values) { parts.push(`${key}=${value}`); } @@ -291,8 +299,8 @@ function sortedQueryString(params: PlivoParamMap): string { function sortedParamsString(params: PlivoParamMap): string { const parts: string[] = []; - for (const key of Object.keys(params).sort()) { - const values = [...params[key]].sort(); + for (const key of Object.keys(params).toSorted()) { + const values = [...params[key]].toSorted(); for (const value of values) { parts.push(`${key}${value}`); } @@ -355,7 +363,9 @@ function validatePlivoV3Signature(params: { .map((s) => normalizeSignatureBase64(s)); for (const sig of provided) { - if (timingSafeEqualString(expected, sig)) return true; + if (timingSafeEqualString(expected, sig)) { + return true; + } } return false; } diff --git a/extensions/voice-call/src/webhook.ts b/extensions/voice-call/src/webhook.ts index 587d56c70..77fcd9c16 100644 --- a/extensions/voice-call/src/webhook.ts +++ b/extensions/voice-call/src/webhook.ts @@ -367,7 +367,9 @@ function runTailscaleCommand( export async function getTailscaleSelfInfo(): Promise { const { code, stdout } = await runTailscaleCommand(["status", "--json"]); - if (code !== 0) return null; + if (code !== 0) { + return null; + } try { const status = JSON.parse(stdout); diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 599e1bd7d..056371d90 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -99,7 +99,7 @@ export const whatsappPlugin: ChannelPlugin = { }, }; }, - isEnabled: (account, cfg) => account.enabled !== false && cfg.web?.enabled !== false, + isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false, disabledReason: () => "disabled", isConfigured: async (account) => await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir), @@ -141,7 +141,9 @@ export const whatsappPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; - if (groupPolicy !== "open") return []; + if (groupPolicy !== "open") { + return []; + } const groupAllowlistConfigured = Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0; if (groupAllowlistConfigured) { @@ -206,7 +208,9 @@ export const whatsappPlugin: ChannelPlugin = { mentions: { stripPatterns: ({ ctx }) => { const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/, ""); - if (!selfE164) return []; + if (!selfE164) { + return []; + } const escaped = escapeRegExp(selfE164); return [escaped, `@${escaped}`]; }, @@ -227,7 +231,9 @@ export const whatsappPlugin: ChannelPlugin = { const account = resolveWhatsAppAccount({ cfg, accountId }); const { e164, jid } = getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir); const id = e164 ?? jid; - if (!id) return null; + if (!id) { + return null; + } return { kind: "user", id, @@ -240,11 +246,17 @@ export const whatsappPlugin: ChannelPlugin = { }, actions: { listActions: ({ cfg }) => { - if (!cfg.channels?.whatsapp) return []; + if (!cfg.channels?.whatsapp) { + return []; + } const gate = createActionGate(cfg.channels.whatsapp.actions); const actions = new Set(); - if (gate("reactions")) actions.add("react"); - if (gate("polls")) actions.add("poll"); + if (gate("reactions")) { + actions.add("react"); + } + if (gate("polls")) { + actions.add("poll"); + } return Array.from(actions); }, supportsAction: ({ action }) => action === "react", diff --git a/extensions/zalo/src/accounts.ts b/extensions/zalo/src/accounts.ts index 4bded4be2..5a49091a1 100644 --- a/extensions/zalo/src/accounts.ts +++ b/extensions/zalo/src/accounts.ts @@ -6,21 +6,29 @@ import { resolveZaloToken } from "./token.js"; function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } export function listZaloAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string { const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined; - if (zaloConfig?.defaultAccount?.trim()) return zaloConfig.defaultAccount.trim(); + if (zaloConfig?.defaultAccount?.trim()) { + return zaloConfig.defaultAccount.trim(); + } const ids = listZaloAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -29,7 +37,9 @@ function resolveAccountConfig( accountId: string, ): ZaloAccountConfig | undefined { const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as ZaloAccountConfig | undefined; } diff --git a/extensions/zalo/src/actions.ts b/extensions/zalo/src/actions.ts index b15eea366..1a9d5d29d 100644 --- a/extensions/zalo/src/actions.ts +++ b/extensions/zalo/src/actions.ts @@ -18,17 +18,23 @@ function listEnabledAccounts(cfg: OpenClawConfig) { export const zaloMessageActions: ChannelMessageActionAdapter = { listActions: ({ cfg }) => { - const accounts = listEnabledAccounts(cfg as OpenClawConfig); - if (accounts.length === 0) return []; + const accounts = listEnabledAccounts(cfg); + if (accounts.length === 0) { + return []; + } const actions = new Set(["send"]); return Array.from(actions); }, supportsButtons: () => false, extractToolSend: ({ args }) => { const action = typeof args.action === "string" ? args.action.trim() : ""; - if (action !== "sendMessage") return null; + if (action !== "sendMessage") { + return null; + } const to = typeof args.to === "string" ? args.to : undefined; - if (!to) return null; + if (!to) { + return null; + } const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined; return { to, accountId }; }, @@ -44,7 +50,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = { const result = await sendMessageZalo(to ?? "", content ?? "", { accountId: accountId ?? undefined, mediaUrl: mediaUrl ?? undefined, - cfg: cfg as OpenClawConfig, + cfg: cfg, }); if (!result.ok) { diff --git a/extensions/zalo/src/api.ts b/extensions/zalo/src/api.ts index 63c04351a..ad11d5044 100644 --- a/extensions/zalo/src/api.ts +++ b/extensions/zalo/src/api.ts @@ -122,7 +122,9 @@ export async function callZaloApi( return data; } finally { - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId) { + clearTimeout(timeoutId); + } } } diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 3a5d956c4..4ea0e0aaf 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -44,7 +44,9 @@ const meta = { function normalizeZaloMessagingTarget(raw: string): string | undefined { const trimmed = raw?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } return trimmed.replace(/^(zalo|zl):/i, ""); } @@ -58,8 +60,8 @@ export const zaloDock: ChannelDock = { outbound: { textChunkLimit: 2000 }, config: { resolveAllowFrom: ({ cfg, accountId }) => - (resolveZaloAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? []).map( - (entry) => String(entry), + (resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => + String(entry), ), formatAllowFrom: ({ allowFrom }) => allowFrom @@ -92,13 +94,12 @@ export const zaloPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.zalo"] }, configSchema: buildChannelConfigSchema(ZaloConfigSchema), config: { - listAccountIds: (cfg) => listZaloAccountIds(cfg as OpenClawConfig), - resolveAccount: (cfg, accountId) => - resolveZaloAccount({ cfg: cfg as OpenClawConfig, accountId }), - defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg as OpenClawConfig), + listAccountIds: (cfg) => listZaloAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg, accountId }), + defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "zalo", accountId, enabled, @@ -106,7 +107,7 @@ export const zaloPlugin: ChannelPlugin = { }), deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "zalo", accountId, clearBaseFields: ["botToken", "tokenFile", "name"], @@ -120,8 +121,8 @@ export const zaloPlugin: ChannelPlugin = { tokenSource: account.tokenSource, }), resolveAllowFrom: ({ cfg, accountId }) => - (resolveZaloAccount({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? []).map( - (entry) => String(entry), + (resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => + String(entry), ), formatAllowFrom: ({ allowFrom }) => allowFrom @@ -133,9 +134,7 @@ export const zaloPlugin: ChannelPlugin = { security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean( - (cfg as OpenClawConfig).channels?.zalo?.accounts?.[resolvedAccountId], - ); + const useAccountPath = Boolean(cfg.channels?.zalo?.accounts?.[resolvedAccountId]); const basePath = useAccountPath ? `channels.zalo.accounts.${resolvedAccountId}.` : "channels.zalo."; @@ -161,7 +160,9 @@ export const zaloPlugin: ChannelPlugin = { targetResolver: { looksLikeId: (raw) => { const trimmed = raw.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } return /^\d{3,}$/.test(trimmed); }, hint: "", @@ -170,7 +171,7 @@ export const zaloPlugin: ChannelPlugin = { directory: { self: async () => null, listPeers: async ({ cfg, accountId, query, limit }) => { - const account = resolveZaloAccount({ cfg: cfg as OpenClawConfig, accountId }); + const account = resolveZaloAccount({ cfg: cfg, accountId }); const q = query?.trim().toLowerCase() || ""; const peers = Array.from( new Set( @@ -191,7 +192,7 @@ export const zaloPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "zalo", accountId, name, @@ -207,7 +208,7 @@ export const zaloPlugin: ChannelPlugin = { }, applyAccountConfig: ({ cfg, accountId, input }) => { const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "zalo", accountId, name: input.name, @@ -246,9 +247,9 @@ export const zaloPlugin: ChannelPlugin = { ...next.channels?.zalo, enabled: true, accounts: { - ...(next.channels?.zalo?.accounts ?? {}), + ...next.channels?.zalo?.accounts, [accountId]: { - ...(next.channels?.zalo?.accounts?.[accountId] ?? {}), + ...next.channels?.zalo?.accounts?.[accountId], enabled: true, ...(input.tokenFile ? { tokenFile: input.tokenFile } @@ -266,16 +267,22 @@ export const zaloPlugin: ChannelPlugin = { idLabel: "zaloUserId", normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""), notifyApproval: async ({ cfg, id }) => { - const account = resolveZaloAccount({ cfg: cfg as OpenClawConfig }); - if (!account.token) throw new Error("Zalo token not configured"); + const account = resolveZaloAccount({ cfg: cfg }); + if (!account.token) { + throw new Error("Zalo token not configured"); + } await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token }); }, }, outbound: { deliveryMode: "direct", chunker: (text, limit) => { - if (!text) return []; - if (limit <= 0 || text.length <= limit) return [text]; + if (!text) { + return []; + } + if (limit <= 0 || text.length <= limit) { + return [text]; + } const chunks: string[] = []; let remaining = text; while (remaining.length > limit) { @@ -283,15 +290,21 @@ export const zaloPlugin: ChannelPlugin = { const lastNewline = window.lastIndexOf("\n"); const lastSpace = window.lastIndexOf(" "); let breakIdx = lastNewline > 0 ? lastNewline : lastSpace; - if (breakIdx <= 0) breakIdx = limit; + if (breakIdx <= 0) { + breakIdx = limit; + } const rawChunk = remaining.slice(0, breakIdx); const chunk = rawChunk.trimEnd(); - if (chunk.length > 0) chunks.push(chunk); + if (chunk.length > 0) { + chunks.push(chunk); + } const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); remaining = remaining.slice(nextStart).trimStart(); } - if (remaining.length) chunks.push(remaining); + if (remaining.length) { + chunks.push(remaining); + } return chunks; }, chunkerMode: "text", @@ -299,7 +312,7 @@ export const zaloPlugin: ChannelPlugin = { sendText: async ({ to, text, accountId, cfg }) => { const result = await sendMessageZalo(to, text, { accountId: accountId ?? undefined, - cfg: cfg as OpenClawConfig, + cfg: cfg, }); return { channel: "zalo", @@ -312,7 +325,7 @@ export const zaloPlugin: ChannelPlugin = { const result = await sendMessageZalo(to, text, { accountId: accountId ?? undefined, mediaUrl, - cfg: cfg as OpenClawConfig, + cfg: cfg, }); return { channel: "zalo", @@ -372,7 +385,9 @@ export const zaloPlugin: ChannelPlugin = { try { const probe = await probeZalo(token, 2500, fetcher); const name = probe.ok ? probe.bot?.name?.trim() : null; - if (name) zaloBotLabel = ` (${name})`; + if (name) { + zaloBotLabel = ` (${name})`; + } ctx.setStatus({ accountId: account.accountId, bot: probe.bot, @@ -385,7 +400,7 @@ export const zaloPlugin: ChannelPlugin = { return monitorZaloProvider({ token, account, - config: ctx.cfg as OpenClawConfig, + config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, useWebhook: Boolean(account.config.webhookUrl), diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index 803b74f30..36c85dadc 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -52,7 +52,9 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str } function isSenderAllowed(senderId: string, allowFrom: string[]): boolean { - if (allowFrom.includes("*")) return true; + if (allowFrom.includes("*")) { + return true; + } const normalizedSenderId = senderId.toLowerCase(); return allowFrom.some((entry) => { const normalized = entry.toLowerCase().replace(/^(zalo|zl):/i, ""); @@ -108,7 +110,9 @@ const webhookTargets = new Map(); function normalizeWebhookPath(raw: string): string { const trimmed = raw.trim(); - if (!trimmed) return "/"; + if (!trimmed) { + return "/"; + } const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; if (withSlash.length > 1 && withSlash.endsWith("/")) { return withSlash.slice(0, -1); @@ -118,7 +122,9 @@ function normalizeWebhookPath(raw: string): string { function resolveWebhookPath(webhookPath?: string, webhookUrl?: string): string | null { const trimmedPath = webhookPath?.trim(); - if (trimmedPath) return normalizeWebhookPath(trimmedPath); + if (trimmedPath) { + return normalizeWebhookPath(trimmedPath); + } if (webhookUrl?.trim()) { try { const parsed = new URL(webhookUrl); @@ -153,7 +159,9 @@ export async function handleZaloWebhookRequest( const url = new URL(req.url ?? "/", "http://localhost"); const path = normalizeWebhookPath(url.pathname); const targets = webhookTargets.get(path); - if (!targets || targets.length === 0) return false; + if (!targets || targets.length === 0) { + return false; + } if (req.method !== "POST") { res.statusCode = 405; @@ -238,7 +246,9 @@ function startPollingLoop(params: { const pollTimeout = 30; const poll = async () => { - if (isStopped() || abortSignal.aborted) return; + if (isStopped() || abortSignal.aborted) { + return; + } try { const response = await getUpdates(token, { timeout: pollTimeout }, fetcher); @@ -285,7 +295,9 @@ async function processUpdate( fetcher?: ZaloFetch, ): Promise { const { event_name, message } = update; - if (!message) return; + if (!message) { + return; + } switch (event_name) { case "message.text.received": @@ -326,7 +338,9 @@ async function handleTextMessage( fetcher?: ZaloFetch, ): Promise { const { text } = message; - if (!text?.trim()) return; + if (!text?.trim()) { + return; + } await processMessageWithPipeline({ message, diff --git a/extensions/zalo/src/monitor.webhook.test.ts b/extensions/zalo/src/monitor.webhook.test.ts index 0f0eb355e..fcfbd0cee 100644 --- a/extensions/zalo/src/monitor.webhook.test.ts +++ b/extensions/zalo/src/monitor.webhook.test.ts @@ -16,7 +16,9 @@ async function withServer( server.listen(0, "127.0.0.1", () => resolve()); }); const address = server.address() as AddressInfo | null; - if (!address) throw new Error("missing server address"); + if (!address) { + throw new Error("missing server address"); + } try { await fn(`http://127.0.0.1:${address.port}`); } finally { diff --git a/extensions/zalo/src/onboarding.ts b/extensions/zalo/src/onboarding.ts index fb7db357a..7590fb27d 100644 --- a/extensions/zalo/src/onboarding.ts +++ b/extensions/zalo/src/onboarding.ts @@ -61,10 +61,7 @@ function setZaloUpdateMode( }, } as OpenClawConfig; } - const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record< - string, - Record - >; + const accounts = { ...cfg.channels?.zalo?.accounts } as Record>; const existing = accounts[accountId] ?? {}; const { webhookUrl: _url, webhookSecret: _secret, webhookPath: _path, ...rest } = existing; accounts[accountId] = rest; @@ -95,12 +92,9 @@ function setZaloUpdateMode( } as OpenClawConfig; } - const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record< - string, - Record - >; + const accounts = { ...cfg.channels?.zalo?.accounts } as Record>; accounts[accountId] = { - ...(accounts[accountId] ?? {}), + ...accounts[accountId], webhookUrl, webhookSecret, webhookPath, @@ -144,8 +138,12 @@ async function promptZaloAllowFrom(params: { initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, validate: (value) => { const raw = String(value ?? "").trim(); - if (!raw) return "Required"; - if (!/^\d+$/.test(raw)) return "Use a numeric Zalo user id"; + if (!raw) { + return "Required"; + } + if (!/^\d+$/.test(raw)) { + return "Use a numeric Zalo user id"; + } return undefined; }, }); @@ -179,9 +177,9 @@ async function promptZaloAllowFrom(params: { ...cfg.channels?.zalo, enabled: true, accounts: { - ...(cfg.channels?.zalo?.accounts ?? {}), + ...cfg.channels?.zalo?.accounts, [accountId]: { - ...(cfg.channels?.zalo?.accounts?.[accountId] ?? {}), + ...cfg.channels?.zalo?.accounts?.[accountId], enabled: cfg.channels?.zalo?.accounts?.[accountId]?.enabled ?? true, dmPolicy: "allowlist", allowFrom: unique, @@ -198,14 +196,14 @@ const dmPolicy: ChannelOnboardingDmPolicy = { policyKey: "channels.zalo.dmPolicy", allowFromKey: "channels.zalo.allowFrom", getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing", - setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as OpenClawConfig, policy), + setPolicy: (cfg, policy) => setZaloDmPolicy(cfg, policy), promptAllowFrom: async ({ cfg, prompter, accountId }) => { const id = accountId && normalizeAccountId(accountId) ? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID) - : resolveDefaultZaloAccountId(cfg as OpenClawConfig); + : resolveDefaultZaloAccountId(cfg); return promptZaloAllowFrom({ - cfg: cfg as OpenClawConfig, + cfg: cfg, prompter, accountId: id, }); @@ -216,8 +214,8 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { channel, dmPolicy, getStatus: async ({ cfg }) => { - const configured = listZaloAccountIds(cfg as OpenClawConfig).some((accountId) => - Boolean(resolveZaloAccount({ cfg: cfg as OpenClawConfig, accountId }).token), + const configured = listZaloAccountIds(cfg).some((accountId) => + Boolean(resolveZaloAccount({ cfg: cfg, accountId }).token), ); return { channel, @@ -235,11 +233,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { forceAllowFrom, }) => { const zaloOverride = accountOverrides.zalo?.trim(); - const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg as OpenClawConfig); + const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg); let zaloAccountId = zaloOverride ? normalizeAccountId(zaloOverride) : defaultZaloAccountId; if (shouldPromptAccountIds && !zaloOverride) { zaloAccountId = await promptAccountId({ - cfg: cfg as OpenClawConfig, + cfg: cfg, prompter, label: "Zalo", currentId: zaloAccountId, @@ -248,7 +246,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { }); } - let next = cfg as OpenClawConfig; + let next = cfg; const resolvedAccount = resolveZaloAccount({ cfg: next, accountId: zaloAccountId }); const accountConfigured = Boolean(resolvedAccount.token); const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID; @@ -329,9 +327,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { ...next.channels?.zalo, enabled: true, accounts: { - ...(next.channels?.zalo?.accounts ?? {}), + ...next.channels?.zalo?.accounts, [zaloAccountId]: { - ...(next.channels?.zalo?.accounts?.[zaloAccountId] ?? {}), + ...next.channels?.zalo?.accounts?.[zaloAccountId], enabled: true, botToken: token, }, @@ -366,7 +364,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = { message: "Webhook secret (8-256 chars)", validate: (value) => { const raw = String(value ?? ""); - if (raw.length < 8 || raw.length > 256) return "8-256 chars"; + if (raw.length < 8 || raw.length > 256) { + return "8-256 chars"; + } return undefined; }, }), diff --git a/extensions/zalo/src/proxy.ts b/extensions/zalo/src/proxy.ts index 65d6e04af..0a6a39b96 100644 --- a/extensions/zalo/src/proxy.ts +++ b/extensions/zalo/src/proxy.ts @@ -7,12 +7,16 @@ const proxyCache = new Map(); export function resolveZaloProxyFetch(proxyUrl?: string | null): ZaloFetch | undefined { const trimmed = proxyUrl?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } const cached = proxyCache.get(trimmed); - if (cached) return cached; + if (cached) { + return cached; + } const agent = new ProxyAgent(trimmed); const fetcher: ZaloFetch = (input, init) => - undiciFetch(input, { ...(init ?? {}), dispatcher: agent as Dispatcher }); + undiciFetch(input, { ...init, dispatcher: agent as Dispatcher }); proxyCache.set(trimmed, fetcher); return fetcher; } diff --git a/extensions/zalo/src/status-issues.ts b/extensions/zalo/src/status-issues.ts index 6a6635bc9..ba217570e 100644 --- a/extensions/zalo/src/status-issues.ts +++ b/extensions/zalo/src/status-issues.ts @@ -14,7 +14,9 @@ const asString = (value: unknown): string | undefined => typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined; function readZaloAccountStatus(value: ChannelAccountSnapshot): ZaloAccountStatus | null { - if (!isRecord(value)) return null; + if (!isRecord(value)) { + return null; + } return { accountId: value.accountId, enabled: value.enabled, @@ -27,11 +29,15 @@ export function collectZaloStatusIssues(accounts: ChannelAccountSnapshot[]): Cha const issues: ChannelStatusIssue[] = []; for (const entry of accounts) { const account = readZaloAccountStatus(entry); - if (!account) continue; + if (!account) { + continue; + } const accountId = asString(account.accountId) ?? "default"; const enabled = account.enabled !== false; const configured = account.configured === true; - if (!enabled || !configured) continue; + if (!enabled || !configured) { + continue; + } if (account.dmPolicy === "open") { issues.push({ diff --git a/extensions/zalo/src/token.ts b/extensions/zalo/src/token.ts index 1f0e1aaa7..595535011 100644 --- a/extensions/zalo/src/token.ts +++ b/extensions/zalo/src/token.ts @@ -23,12 +23,16 @@ export function resolveZaloToken( if (accountConfig) { const token = accountConfig.botToken?.trim(); - if (token) return { token, source: "config" }; + if (token) { + return { token, source: "config" }; + } const tokenFile = accountConfig.tokenFile?.trim(); if (tokenFile) { try { const fileToken = readFileSync(tokenFile, "utf8").trim(); - if (fileToken) return { token: fileToken, source: "configFile" }; + if (fileToken) { + return { token: fileToken, source: "configFile" }; + } } catch { // ignore read failures } @@ -37,18 +41,24 @@ export function resolveZaloToken( if (isDefaultAccount) { const token = baseConfig?.botToken?.trim(); - if (token) return { token, source: "config" }; + if (token) { + return { token, source: "config" }; + } const tokenFile = baseConfig?.tokenFile?.trim(); if (tokenFile) { try { const fileToken = readFileSync(tokenFile, "utf8").trim(); - if (fileToken) return { token: fileToken, source: "configFile" }; + if (fileToken) { + return { token: fileToken, source: "configFile" }; + } } catch { // ignore read failures } } const envToken = process.env.ZALO_BOT_TOKEN?.trim(); - if (envToken) return { token: envToken, source: "env" }; + if (envToken) { + return { token: envToken, source: "env" }; + } } return { token: "", source: "none" }; diff --git a/extensions/zalouser/src/accounts.ts b/extensions/zalouser/src/accounts.ts index 903fd8b4a..e218a7e33 100644 --- a/extensions/zalouser/src/accounts.ts +++ b/extensions/zalouser/src/accounts.ts @@ -6,21 +6,29 @@ import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } f function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts; - if (!accounts || typeof accounts !== "object") return []; + if (!accounts || typeof accounts !== "object") { + return []; + } return Object.keys(accounts).filter(Boolean); } export function listZalouserAccountIds(cfg: OpenClawConfig): string[] { const ids = listConfiguredAccountIds(cfg); - if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; - return ids.sort((a, b) => a.localeCompare(b)); + if (ids.length === 0) { + return [DEFAULT_ACCOUNT_ID]; + } + return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultZalouserAccountId(cfg: OpenClawConfig): string { const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined; - if (zalouserConfig?.defaultAccount?.trim()) return zalouserConfig.defaultAccount.trim(); + if (zalouserConfig?.defaultAccount?.trim()) { + return zalouserConfig.defaultAccount.trim(); + } const ids = listZalouserAccountIds(cfg); - if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; + if (ids.includes(DEFAULT_ACCOUNT_ID)) { + return DEFAULT_ACCOUNT_ID; + } return ids[0] ?? DEFAULT_ACCOUNT_ID; } @@ -29,7 +37,9 @@ function resolveAccountConfig( accountId: string, ): ZalouserAccountConfig | undefined { const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts; - if (!accounts || typeof accounts !== "object") return undefined; + if (!accounts || typeof accounts !== "object") { + return undefined; + } return accounts[accountId] as ZalouserAccountConfig | undefined; } @@ -41,9 +51,15 @@ function mergeZalouserAccountConfig(cfg: OpenClawConfig, accountId: string): Zal } function resolveZcaProfile(config: ZalouserAccountConfig, accountId: string): string { - if (config.profile?.trim()) return config.profile.trim(); - if (process.env.ZCA_PROFILE?.trim()) return process.env.ZCA_PROFILE.trim(); - if (accountId !== DEFAULT_ACCOUNT_ID) return accountId; + if (config.profile?.trim()) { + return config.profile.trim(); + } + if (process.env.ZCA_PROFILE?.trim()) { + return process.env.ZCA_PROFILE.trim(); + } + if (accountId !== DEFAULT_ACCOUNT_ID) { + return accountId; + } return "default"; } @@ -111,7 +127,9 @@ export async function getZcaUserInfo( profile: string, ): Promise<{ userId?: string; displayName?: string } | null> { const result = await runZca(["me", "info", "-j"], { profile, timeout: 10000 }); - if (!result.ok) return null; + if (!result.ok) { + return null; + } return parseJsonOutput<{ userId?: string; displayName?: string }>(result.stdout); } diff --git a/extensions/zalouser/src/channel.test.ts b/extensions/zalouser/src/channel.test.ts index 123cf358d..a5ba68f95 100644 --- a/extensions/zalouser/src/channel.test.ts +++ b/extensions/zalouser/src/channel.test.ts @@ -6,7 +6,9 @@ describe("zalouser outbound chunker", () => { it("chunks without empty strings and respects limit", () => { const chunker = zalouserPlugin.outbound?.chunker; expect(chunker).toBeTypeOf("function"); - if (!chunker) return; + if (!chunker) { + return; + } const limit = 10; const chunks = chunker("hello world\nthis is a test", limit); diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index 83db8019f..c901a19ca 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -85,7 +85,7 @@ function resolveZalouserGroupToolPolicy( params: ChannelGroupContext, ): GroupToolPolicyConfig | undefined { const account = resolveZalouserAccountSync({ - cfg: params.cfg as OpenClawConfig, + cfg: params.cfg, accountId: params.accountId ?? undefined, }); const groups = account.config.groups ?? {}; @@ -96,7 +96,9 @@ function resolveZalouserGroupToolPolicy( ); for (const key of candidates) { const entry = groups[key]; - if (entry?.tools) return entry.tools; + if (entry?.tools) { + return entry.tools; + } } return undefined; } @@ -111,9 +113,9 @@ export const zalouserDock: ChannelDock = { outbound: { textChunkLimit: 2000 }, config: { resolveAllowFrom: ({ cfg, accountId }) => - ( - resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? [] - ).map((entry) => String(entry)), + (resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => + String(entry), + ), formatAllowFrom: ({ allowFrom }) => allowFrom .map((entry) => String(entry).trim()) @@ -146,13 +148,12 @@ export const zalouserPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.zalouser"] }, configSchema: buildChannelConfigSchema(ZalouserConfigSchema), config: { - listAccountIds: (cfg) => listZalouserAccountIds(cfg as OpenClawConfig), - resolveAccount: (cfg, accountId) => - resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }), - defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg as OpenClawConfig), + listAccountIds: (cfg) => listZalouserAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveZalouserAccountSync({ cfg: cfg, accountId }), + defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "zalouser", accountId, enabled, @@ -160,7 +161,7 @@ export const zalouserPlugin: ChannelPlugin = { }), deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, sectionKey: "zalouser", accountId, clearBaseFields: [ @@ -188,9 +189,9 @@ export const zalouserPlugin: ChannelPlugin = { configured: undefined, }), resolveAllowFrom: ({ cfg, accountId }) => - ( - resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }).config.allowFrom ?? [] - ).map((entry) => String(entry)), + (resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => + String(entry), + ), formatAllowFrom: ({ allowFrom }) => allowFrom .map((entry) => String(entry).trim()) @@ -201,9 +202,7 @@ export const zalouserPlugin: ChannelPlugin = { security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean( - (cfg as OpenClawConfig).channels?.zalouser?.accounts?.[resolvedAccountId], - ); + const useAccountPath = Boolean(cfg.channels?.zalouser?.accounts?.[resolvedAccountId]); const basePath = useAccountPath ? `channels.zalouser.accounts.${resolvedAccountId}.` : "channels.zalouser."; @@ -228,7 +227,7 @@ export const zalouserPlugin: ChannelPlugin = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "zalouser", accountId, name, @@ -236,7 +235,7 @@ export const zalouserPlugin: ChannelPlugin = { validateInput: () => null, applyAccountConfig: ({ cfg, accountId, input }) => { const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg as OpenClawConfig, + cfg: cfg, channelKey: "zalouser", accountId, name: input.name, @@ -268,9 +267,9 @@ export const zalouserPlugin: ChannelPlugin = { ...next.channels?.zalouser, enabled: true, accounts: { - ...(next.channels?.zalouser?.accounts ?? {}), + ...next.channels?.zalouser?.accounts, [accountId]: { - ...(next.channels?.zalouser?.accounts?.[accountId] ?? {}), + ...next.channels?.zalouser?.accounts?.[accountId], enabled: true, }, }, @@ -282,13 +281,17 @@ export const zalouserPlugin: ChannelPlugin = { messaging: { normalizeTarget: (raw) => { const trimmed = raw?.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } return trimmed.replace(/^(zalouser|zlu):/i, ""); }, targetResolver: { looksLikeId: (raw) => { const trimmed = raw.trim(); - if (!trimmed) return false; + if (!trimmed) { + return false; + } return /^\d{3,}$/.test(trimmed); }, hint: "", @@ -297,8 +300,10 @@ export const zalouserPlugin: ChannelPlugin = { directory: { self: async ({ cfg, accountId, runtime }) => { const ok = await checkZcaInstalled(); - if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + if (!ok) { + throw new Error("Missing dependency: `zca` not found in PATH"); + } + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const result = await runZca(["me", "info", "-j"], { profile: account.profile, timeout: 10000, @@ -308,7 +313,9 @@ export const zalouserPlugin: ChannelPlugin = { return null; } const parsed = parseJsonOutput(result.stdout); - if (!parsed?.userId) return null; + if (!parsed?.userId) { + return null; + } return mapUser({ id: String(parsed.userId), name: parsed.displayName ?? null, @@ -318,8 +325,10 @@ export const zalouserPlugin: ChannelPlugin = { }, listPeers: async ({ cfg, accountId, query, limit }) => { const ok = await checkZcaInstalled(); - if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + if (!ok) { + throw new Error("Missing dependency: `zca` not found in PATH"); + } + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const args = query?.trim() ? ["friend", "find", query.trim()] : ["friend", "list", "-j"]; const result = await runZca(args, { profile: account.profile, timeout: 15000 }); if (!result.ok) { @@ -340,8 +349,10 @@ export const zalouserPlugin: ChannelPlugin = { }, listGroups: async ({ cfg, accountId, query, limit }) => { const ok = await checkZcaInstalled(); - if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + if (!ok) { + throw new Error("Missing dependency: `zca` not found in PATH"); + } + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const result = await runZca(["group", "list", "-j"], { profile: account.profile, timeout: 15000, @@ -367,8 +378,10 @@ export const zalouserPlugin: ChannelPlugin = { }, listGroupMembers: async ({ cfg, accountId, groupId, limit }) => { const ok = await checkZcaInstalled(); - if (!ok) throw new Error("Missing dependency: `zca` not found in PATH"); - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + if (!ok) { + throw new Error("Missing dependency: `zca` not found in PATH"); + } + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const result = await runZca(["group", "members", groupId, "-j"], { profile: account.profile, timeout: 20000, @@ -383,7 +396,9 @@ export const zalouserPlugin: ChannelPlugin = { ? parsed .map((m) => { const id = m.userId ?? (m as { id?: string | number }).id; - if (!id) return null; + if (!id) { + return null; + } return mapUser({ id: String(id), name: (m as { displayName?: string }).displayName ?? null, @@ -412,7 +427,7 @@ export const zalouserPlugin: ChannelPlugin = { } try { const account = resolveZalouserAccountSync({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID, }); const args = @@ -422,7 +437,9 @@ export const zalouserPlugin: ChannelPlugin = { : ["friend", "list", "-j"] : ["group", "list", "-j"]; const result = await runZca(args, { profile: account.profile, timeout: 15000 }); - if (!result.ok) throw new Error(result.stderr || "zca lookup failed"); + if (!result.ok) { + throw new Error(result.stderr || "zca lookup failed"); + } if (kind === "user") { const parsed = parseJsonOutput(result.stdout) ?? []; const matches = Array.isArray(parsed) @@ -469,9 +486,11 @@ export const zalouserPlugin: ChannelPlugin = { idLabel: "zalouserUserId", normalizeAllowEntry: (entry) => entry.replace(/^(zalouser|zlu):/i, ""), notifyApproval: async ({ cfg, id }) => { - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig }); + const account = resolveZalouserAccountSync({ cfg: cfg }); const authenticated = await checkZcaAuthenticated(account.profile); - if (!authenticated) throw new Error("Zalouser not authenticated"); + if (!authenticated) { + throw new Error("Zalouser not authenticated"); + } await sendMessageZalouser(id, "Your pairing request has been approved.", { profile: account.profile, }); @@ -480,7 +499,7 @@ export const zalouserPlugin: ChannelPlugin = { auth: { login: async ({ cfg, accountId, runtime }) => { const account = resolveZalouserAccountSync({ - cfg: cfg as OpenClawConfig, + cfg: cfg, accountId: accountId ?? DEFAULT_ACCOUNT_ID, }); const ok = await checkZcaInstalled(); @@ -501,8 +520,12 @@ export const zalouserPlugin: ChannelPlugin = { outbound: { deliveryMode: "direct", chunker: (text, limit) => { - if (!text) return []; - if (limit <= 0 || text.length <= limit) return [text]; + if (!text) { + return []; + } + if (limit <= 0 || text.length <= limit) { + return [text]; + } const chunks: string[] = []; let remaining = text; while (remaining.length > limit) { @@ -510,21 +533,27 @@ export const zalouserPlugin: ChannelPlugin = { const lastNewline = window.lastIndexOf("\n"); const lastSpace = window.lastIndexOf(" "); let breakIdx = lastNewline > 0 ? lastNewline : lastSpace; - if (breakIdx <= 0) breakIdx = limit; + if (breakIdx <= 0) { + breakIdx = limit; + } const rawChunk = remaining.slice(0, breakIdx); const chunk = rawChunk.trimEnd(); - if (chunk.length > 0) chunks.push(chunk); + if (chunk.length > 0) { + chunks.push(chunk); + } const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); remaining = remaining.slice(nextStart).trimStart(); } - if (remaining.length) chunks.push(remaining); + if (remaining.length) { + chunks.push(remaining); + } return chunks; }, chunkerMode: "text", textChunkLimit: 2000, sendText: async ({ to, text, accountId, cfg }) => { - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const result = await sendMessageZalouser(to, text, { profile: account.profile }); return { channel: "zalouser", @@ -534,7 +563,7 @@ export const zalouserPlugin: ChannelPlugin = { }; }, sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => { - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const result = await sendMessageZalouser(to, text, { profile: account.profile, mediaUrl, @@ -591,7 +620,9 @@ export const zalouserPlugin: ChannelPlugin = { let userLabel = ""; try { const userInfo = await getZcaUserInfo(account.profile); - if (userInfo?.displayName) userLabel = ` (${userInfo.displayName})`; + if (userInfo?.displayName) { + userLabel = ` (${userInfo.displayName})`; + } ctx.setStatus({ accountId: account.accountId, user: userInfo, @@ -603,7 +634,7 @@ export const zalouserPlugin: ChannelPlugin = { const { monitorZalouserProvider } = await import("./monitor.js"); return monitorZalouserProvider({ account, - config: ctx.cfg as OpenClawConfig, + config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }), diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 783bc9cc9..bbea88ecd 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -29,7 +29,9 @@ function buildNameIndex(items: T[], nameFn: (item: T) => string | undefined): const index = new Map(); for (const item of items) { const name = nameFn(item)?.trim().toLowerCase(); - if (!name) continue; + if (!name) { + continue; + } const list = index.get(name) ?? []; list.push(item); index.set(name, list); @@ -46,7 +48,9 @@ function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: str } function isSenderAllowed(senderId: string, allowFrom: string[]): boolean { - if (allowFrom.includes("*")) return true; + if (allowFrom.includes("*")) { + return true; + } const normalizedSenderId = senderId.toLowerCase(); return allowFrom.some((entry) => { const normalized = entry.toLowerCase().replace(/^(zalouser|zlu):/i, ""); @@ -56,7 +60,9 @@ function isSenderAllowed(senderId: string, allowFrom: string[]): boolean { function normalizeGroupSlug(raw?: string | null): string { const trimmed = raw?.trim().toLowerCase() ?? ""; - if (!trimmed) return ""; + if (!trimmed) { + return ""; + } return trimmed .replace(/^#/, "") .replace(/[^a-z0-9]+/g, "-") @@ -70,7 +76,9 @@ function isGroupAllowed(params: { }): boolean { const groups = params.groups ?? {}; const keys = Object.keys(groups); - if (keys.length === 0) return false; + if (keys.length === 0) { + return false; + } const candidates = [ params.groupId, `group:${params.groupId}`, @@ -79,11 +87,15 @@ function isGroupAllowed(params: { ].filter(Boolean); for (const candidate of candidates) { const entry = groups[candidate]; - if (!entry) continue; + if (!entry) { + continue; + } return entry.allow !== false && entry.enabled !== false; } const wildcard = groups["*"]; - if (wildcard) return wildcard.allow !== false && wildcard.enabled !== false; + if (wildcard) { + return wildcard.allow !== false && wildcard.enabled !== false; + } return false; } @@ -104,7 +116,9 @@ function startZcaListener( buffer = lines.pop() ?? ""; for (const line of lines) { const trimmed = line.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } try { const parsed = JSON.parse(trimmed) as ZcaMessage; onMessage(parsed); @@ -118,7 +132,9 @@ function startZcaListener( proc.stderr?.on("data", (data: Buffer) => { const text = data.toString().trim(); - if (text) runtime.error(`[zalouser] zca stderr: ${text}`); + if (text) { + runtime.error(`[zalouser] zca stderr: ${text}`); + } }); void promise.then((result) => { @@ -147,7 +163,9 @@ async function processMessage( statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, ): Promise { const { threadId, content, timestamp, metadata } = message; - if (!content?.trim()) return; + if (!content?.trim()) { + return; + } const isGroup = metadata?.isGroup ?? false; const senderId = metadata?.fromId ?? threadId; @@ -476,7 +494,9 @@ export async function monitorZalouserProvider( for (const entry of groupKeys) { const cleaned = normalizeZalouserEntry(entry); if (/^\d+$/.test(cleaned)) { - if (!nextGroups[cleaned]) nextGroups[cleaned] = groupsConfig[entry]; + if (!nextGroups[cleaned]) { + nextGroups[cleaned] = groupsConfig[entry]; + } mapping.push(`${entry}→${cleaned}`); continue; } @@ -484,7 +504,9 @@ export async function monitorZalouserProvider( const match = matches[0]; const id = match?.groupId ? String(match.groupId) : undefined; if (id) { - if (!nextGroups[id]) nextGroups[id] = groupsConfig[entry]; + if (!nextGroups[id]) { + nextGroups[id] = groupsConfig[entry]; + } mapping.push(`${entry}→${id}`); } else { unresolved.push(entry); diff --git a/extensions/zalouser/src/onboarding.ts b/extensions/zalouser/src/onboarding.ts index 4a4551170..2d0d1fe83 100644 --- a/extensions/zalouser/src/onboarding.ts +++ b/extensions/zalouser/src/onboarding.ts @@ -73,19 +73,29 @@ async function promptZalouserAllowFrom(params: { const resolveUserId = async (input: string): Promise => { const trimmed = input.trim(); - if (!trimmed) return null; - if (/^\d+$/.test(trimmed)) return trimmed; + if (!trimmed) { + return null; + } + if (/^\d+$/.test(trimmed)) { + return trimmed; + } const ok = await checkZcaInstalled(); - if (!ok) return null; + if (!ok) { + return null; + } const result = await runZca(["friend", "find", trimmed], { profile: resolved.profile, timeout: 15000, }); - if (!result.ok) return null; + if (!result.ok) { + return null; + } const parsed = parseJsonOutput(result.stdout); const rows = Array.isArray(parsed) ? parsed : []; const match = rows[0]; - if (!match?.userId) return null; + if (!match?.userId) { + return null; + } if (rows.length > 1) { await prompter.note( `Multiple matches for "${trimmed}", using ${match.displayName ?? match.userId}.`, @@ -140,9 +150,9 @@ async function promptZalouserAllowFrom(params: { ...cfg.channels?.zalouser, enabled: true, accounts: { - ...(cfg.channels?.zalouser?.accounts ?? {}), + ...cfg.channels?.zalouser?.accounts, [accountId]: { - ...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}), + ...cfg.channels?.zalouser?.accounts?.[accountId], enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true, dmPolicy: "allowlist", allowFrom: unique, @@ -180,9 +190,9 @@ function setZalouserGroupPolicy( ...cfg.channels?.zalouser, enabled: true, accounts: { - ...(cfg.channels?.zalouser?.accounts ?? {}), + ...cfg.channels?.zalouser?.accounts, [accountId]: { - ...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}), + ...cfg.channels?.zalouser?.accounts?.[accountId], enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true, groupPolicy, }, @@ -219,9 +229,9 @@ function setZalouserGroupAllowlist( ...cfg.channels?.zalouser, enabled: true, accounts: { - ...(cfg.channels?.zalouser?.accounts ?? {}), + ...cfg.channels?.zalouser?.accounts, [accountId]: { - ...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}), + ...cfg.channels?.zalouser?.accounts?.[accountId], enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true, groups, }, @@ -241,14 +251,18 @@ async function resolveZalouserGroups(params: { profile: account.profile, timeout: 15000, }); - if (!result.ok) throw new Error(result.stderr || "Failed to list groups"); + if (!result.ok) { + throw new Error(result.stderr || "Failed to list groups"); + } const groups = (parseJsonOutput(result.stdout) ?? []).filter((group) => Boolean(group.groupId), ); const byName = new Map(); for (const group of groups) { const name = group.name?.trim().toLowerCase(); - if (!name) continue; + if (!name) { + continue; + } const list = byName.get(name) ?? []; list.push(group); byName.set(name, list); @@ -256,8 +270,12 @@ async function resolveZalouserGroups(params: { return params.entries.map((input) => { const trimmed = input.trim(); - if (!trimmed) return { input, resolved: false }; - if (/^\d+$/.test(trimmed)) return { input, resolved: true, id: trimmed }; + if (!trimmed) { + return { input, resolved: false }; + } + if (/^\d+$/.test(trimmed)) { + return { input, resolved: true, id: trimmed }; + } const matches = byName.get(trimmed.toLowerCase()) ?? []; const match = matches[0]; return match?.groupId @@ -271,16 +289,15 @@ const dmPolicy: ChannelOnboardingDmPolicy = { channel, policyKey: "channels.zalouser.dmPolicy", allowFromKey: "channels.zalouser.allowFrom", - getCurrent: (cfg) => - ((cfg as OpenClawConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing", - setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as OpenClawConfig, policy), + getCurrent: (cfg) => (cfg.channels?.zalouser?.dmPolicy ?? "pairing") as "pairing", + setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg, policy), promptAllowFrom: async ({ cfg, prompter, accountId }) => { const id = accountId && normalizeAccountId(accountId) ? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID) - : resolveDefaultZalouserAccountId(cfg as OpenClawConfig); + : resolveDefaultZalouserAccountId(cfg); return promptZalouserAllowFrom({ - cfg: cfg as OpenClawConfig, + cfg: cfg, prompter, accountId: id, }); @@ -291,10 +308,10 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = { channel, dmPolicy, getStatus: async ({ cfg }) => { - const ids = listZalouserAccountIds(cfg as OpenClawConfig); + const ids = listZalouserAccountIds(cfg); let configured = false; for (const accountId of ids) { - const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId }); + const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); const isAuth = await checkZcaAuthenticated(account.profile); if (isAuth) { configured = true; @@ -332,12 +349,12 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = { } const zalouserOverride = accountOverrides.zalouser?.trim(); - const defaultAccountId = resolveDefaultZalouserAccountId(cfg as OpenClawConfig); + const defaultAccountId = resolveDefaultZalouserAccountId(cfg); let accountId = zalouserOverride ? normalizeAccountId(zalouserOverride) : defaultAccountId; if (shouldPromptAccountIds && !zalouserOverride) { accountId = await promptAccountId({ - cfg: cfg as OpenClawConfig, + cfg: cfg, prompter, label: "Zalo Personal", currentId: accountId, @@ -346,7 +363,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = { }); } - let next = cfg as OpenClawConfig; + let next = cfg; const account = resolveZalouserAccountSync({ cfg: next, accountId }); const alreadyAuthenticated = await checkZcaAuthenticated(account.profile); @@ -411,9 +428,9 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = { ...next.channels?.zalouser, enabled: true, accounts: { - ...(next.channels?.zalouser?.accounts ?? {}), + ...next.channels?.zalouser?.accounts, [accountId]: { - ...(next.channels?.zalouser?.accounts?.[accountId] ?? {}), + ...next.channels?.zalouser?.accounts?.[accountId], enabled: true, profile: account.profile, }, diff --git a/extensions/zalouser/src/send.ts b/extensions/zalouser/src/send.ts index 3b07a6060..0674b88e2 100644 --- a/extensions/zalouser/src/send.ts +++ b/extensions/zalouser/src/send.ts @@ -34,7 +34,9 @@ export async function sendMessageZalouser( // Send text message const args = ["msg", "send", threadId.trim(), text.slice(0, 2000)]; - if (options.isGroup) args.push("-g"); + if (options.isGroup) { + args.push("-g"); + } try { const result = await runZca(args, { profile }); @@ -79,7 +81,9 @@ async function sendMediaZalouser( if (options.caption) { args.push("-m", options.caption.slice(0, 2000)); } - if (options.isGroup) args.push("-g"); + if (options.isGroup) { + args.push("-g"); + } try { const result = await runZca(args, { profile }); @@ -104,7 +108,9 @@ export async function sendImageZalouser( if (options.caption) { args.push("-m", options.caption.slice(0, 2000)); } - if (options.isGroup) args.push("-g"); + if (options.isGroup) { + args.push("-g"); + } try { const result = await runZca(args, { profile }); @@ -124,7 +130,9 @@ export async function sendLinkZalouser( ): Promise { const profile = options.profile || process.env.ZCA_PROFILE || "default"; const args = ["msg", "link", threadId.trim(), url.trim()]; - if (options.isGroup) args.push("-g"); + if (options.isGroup) { + args.push("-g"); + } try { const result = await runZca(args, { profile }); @@ -140,7 +148,9 @@ export async function sendLinkZalouser( function extractMessageId(stdout: string): string | undefined { // Try to extract message ID from output const match = stdout.match(/message[_\s]?id[:\s]+(\S+)/i); - if (match) return match[1]; + if (match) { + return match[1]; + } // Return first word if it looks like an ID const firstWord = stdout.trim().split(/\s+/)[0]; if (firstWord && /^[a-zA-Z0-9_-]+$/.test(firstWord)) { diff --git a/extensions/zalouser/src/status-issues.ts b/extensions/zalouser/src/status-issues.ts index fa3271c6b..08fc0f642 100644 --- a/extensions/zalouser/src/status-issues.ts +++ b/extensions/zalouser/src/status-issues.ts @@ -15,7 +15,9 @@ const asString = (value: unknown): string | undefined => typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined; function readZalouserAccountStatus(value: ChannelAccountSnapshot): ZalouserAccountStatus | null { - if (!isRecord(value)) return null; + if (!isRecord(value)) { + return null; + } return { accountId: value.accountId, enabled: value.enabled, @@ -26,7 +28,9 @@ function readZalouserAccountStatus(value: ChannelAccountSnapshot): ZalouserAccou } function isMissingZca(lastError?: string): boolean { - if (!lastError) return false; + if (!lastError) { + return false; + } const lower = lastError.toLowerCase(); return lower.includes("zca") && (lower.includes("not found") || lower.includes("enoent")); } @@ -37,10 +41,14 @@ export function collectZalouserStatusIssues( const issues: ChannelStatusIssue[] = []; for (const entry of accounts) { const account = readZalouserAccountStatus(entry); - if (!account) continue; + if (!account) { + continue; + } const accountId = asString(account.accountId) ?? "default"; const enabled = account.enabled !== false; - if (!enabled) continue; + if (!enabled) { + continue; + } const configured = account.configured === true; const lastError = asString(account.lastError)?.trim(); diff --git a/extensions/zalouser/src/tool.ts b/extensions/zalouser/src/tool.ts index b99486797..963f70a0e 100644 --- a/extensions/zalouser/src/tool.ts +++ b/extensions/zalouser/src/tool.ts @@ -62,7 +62,9 @@ export async function executeZalouserTool( throw new Error("threadId and message required for send action"); } const args = ["msg", "send", params.threadId, params.message]; - if (params.isGroup) args.push("-g"); + if (params.isGroup) { + args.push("-g"); + } const result = await runZca(args, { profile: params.profile }); if (!result.ok) { throw new Error(result.stderr || "Failed to send message"); @@ -78,8 +80,12 @@ export async function executeZalouserTool( throw new Error("url required for image action"); } const args = ["msg", "image", params.threadId, "-u", params.url]; - if (params.message) args.push("-m", params.message); - if (params.isGroup) args.push("-g"); + if (params.message) { + args.push("-m", params.message); + } + if (params.isGroup) { + args.push("-g"); + } const result = await runZca(args, { profile: params.profile }); if (!result.ok) { throw new Error(result.stderr || "Failed to send image"); @@ -92,7 +98,9 @@ export async function executeZalouserTool( throw new Error("threadId and url required for link action"); } const args = ["msg", "link", params.threadId, params.url]; - if (params.isGroup) args.push("-g"); + if (params.isGroup) { + args.push("-g"); + } const result = await runZca(args, { profile: params.profile }); if (!result.ok) { throw new Error(result.stderr || "Failed to send link"); @@ -142,10 +150,12 @@ export async function executeZalouserTool( }); } - default: + default: { + params.action satisfies never; throw new Error( - `Unknown action: ${params.action}. Valid actions: send, image, link, friends, groups, me, status`, + `Unknown action: ${String(params.action)}. Valid actions: send, image, link, friends, groups, me, status`, ); + } } } catch (err) { return json({ diff --git a/extensions/zalouser/src/zca.ts b/extensions/zalouser/src/zca.ts index 73a4aa738..55272afa9 100644 --- a/extensions/zalouser/src/zca.ts +++ b/extensions/zalouser/src/zca.ts @@ -109,6 +109,7 @@ export function runZcaInteractive(args: string[], options?: ZcaRunOptions): Prom } function stripAnsi(str: string): string { + // oxlint-disable-next-line no-control-regex return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, ""); }