diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index 9275e3b10..0beb20c10 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -11,7 +11,7 @@ struct GeneralSettings: View { @State private var remoteStatus: RemoteStatus = .idle @State private var showRemoteAdvanced = false -var body: some View { + var body: some View { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 18) { if !self.state.onboardingSeen { @@ -128,7 +128,8 @@ var body: some View { } } .buttonStyle(.borderedProminent) - .disabled(self.remoteStatus == .checking || self.state.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .disabled(self.remoteStatus == .checking || self.state.remoteTarget + .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) switch self.remoteStatus { case .idle: @@ -307,7 +308,7 @@ extension GeneralSettings { } @MainActor - fileprivate func testRemote() async { + private func testRemote() async { self.remoteStatus = .checking let command = CommandResolver.clawdisCommand(subcommand: "status", extraArgs: ["--json"]) let response = await ShellRunner.run(command: command, cwd: nil, env: nil, timeout: 10) diff --git a/apps/macos/Sources/Clawdis/HealthStore.swift b/apps/macos/Sources/Clawdis/HealthStore.swift index a19deaa26..b1dba486a 100644 --- a/apps/macos/Sources/Clawdis/HealthStore.swift +++ b/apps/macos/Sources/Clawdis/HealthStore.swift @@ -1,6 +1,6 @@ import Foundation -import SwiftUI import OSLog +import SwiftUI struct HealthSnapshot: Codable, Sendable { struct Web: Codable, Sendable { diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 2c314a787..769681370 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -257,7 +257,8 @@ struct OnboardingView: View { self.onboardingPage { Text("Link WhatsApp") .font(.largeTitle.weight(.semibold)) - Text("Run `clawdis login` where the relay runs (local if local mode, remote if remote). Scan the QR to pair your account.") + Text( + "Run `clawdis login` where the relay runs (local if local mode, remote if remote). Scan the QR to pair your account.") .font(.body) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -367,7 +368,11 @@ struct OnboardingView: View { .frame(width: self.pageWidth, alignment: .top) } - private func onboardingCard(spacing: CGFloat = 12, padding: CGFloat = 16, @ViewBuilder _ content: () -> some View) -> some View { + private func onboardingCard( + spacing: CGFloat = 12, + padding: CGFloat = 16, + @ViewBuilder _ content: () -> some View) -> some View + { VStack(alignment: .leading, spacing: spacing) { content() } diff --git a/apps/macos/Sources/Clawdis/Utilities.swift b/apps/macos/Sources/Clawdis/Utilities.swift index fe9aca90a..c62998207 100644 --- a/apps/macos/Sources/Clawdis/Utilities.swift +++ b/apps/macos/Sources/Clawdis/Utilities.swift @@ -282,7 +282,11 @@ enum CommandResolver { static func clawdisCommand(subcommand: String, extraArgs: [String] = []) -> [String] { let settings = self.connectionSettings() - if settings.mode == .remote, let ssh = self.sshCommand(subcommand: subcommand, extraArgs: extraArgs, settings: settings) { + if settings.mode == .remote, let ssh = self.sshCommand( + subcommand: subcommand, + extraArgs: extraArgs, + settings: settings) + { return ssh } if let bundled = self.bundledRelayCommand(subcommand: subcommand, extraArgs: extraArgs) { @@ -337,7 +341,11 @@ enum CommandResolver { let target = UserDefaults.standard.string(forKey: remoteTargetKey) ?? "" let identity = UserDefaults.standard.string(forKey: remoteIdentityKey) ?? "" let projectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey) ?? "" - return RemoteSettings(mode: mode, target: self.sanitizedTarget(target), identity: identity, projectRoot: projectRoot) + return RemoteSettings( + mode: mode, + target: self.sanitizedTarget(target), + identity: identity, + projectRoot: projectRoot) } private static func sanitizedTarget(_ raw: String) -> String { diff --git a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift index d3d85b33e..3db253c09 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift @@ -60,7 +60,6 @@ struct VoiceWakeSettings: View { self.micPicker self.levelMeter - VoiceWakeTestCard( testState: self.$testState, isTesting: self.$isTesting, @@ -267,7 +266,7 @@ struct VoiceWakeSettings: View { } } .labelsHidden() - .frame(width: 220) + .frame(width: 220) Button { guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return } diff --git a/apps/macos/Sources/Clawdis/WebChatServer.swift b/apps/macos/Sources/Clawdis/WebChatServer.swift index 55bfd6645..c4113bb09 100644 --- a/apps/macos/Sources/Clawdis/WebChatServer.swift +++ b/apps/macos/Sources/Clawdis/WebChatServer.swift @@ -162,27 +162,27 @@ final class WebChatServer: @unchecked Sendable { private func statusText(_ code: Int) -> String { switch code { - case 200: return "OK" - case 403: return "Forbidden" - case 404: return "Not Found" - default: return "Error" + case 200: "OK" + case 403: "Forbidden" + case 404: "Not Found" + default: "Error" } } private func mimeType(forExtension ext: String) -> String { switch ext.lowercased() { - case "html", "htm": return "text/html; charset=utf-8" - case "js", "mjs": return "application/javascript; charset=utf-8" - case "css": return "text/css; charset=utf-8" - case "json", "map": return "application/json; charset=utf-8" - case "svg": return "image/svg+xml" - case "png": return "image/png" - case "jpg", "jpeg": return "image/jpeg" - case "gif": return "image/gif" - case "woff2": return "font/woff2" - case "woff": return "font/woff" - case "ttf": return "font/ttf" - default: return "application/octet-stream" + case "html", "htm": "text/html; charset=utf-8" + case "js", "mjs": "application/javascript; charset=utf-8" + case "css": "text/css; charset=utf-8" + case "json", "map": "application/json; charset=utf-8" + case "svg": "image/svg+xml" + case "png": "image/png" + case "jpg", "jpeg": "image/jpeg" + case "gif": "image/gif" + case "woff2": "font/woff2" + case "woff": "font/woff" + case "ttf": "font/ttf" + default: "application/octet-stream" } } } diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index cffbf3c8f..3ca118589 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -9,6 +9,7 @@ final class WebChatWindowController: NSWindowController, WKScriptMessageHandler, private let webView: WKWebView private let sessionKey: String private let initialMessagesJSON: String + private let logger = Logger(subsystem: "com.steipete.clawdis", category: "WebChat") init(sessionKey: String) { webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)") diff --git a/src/cli/program.ts b/src/cli/program.ts index 4ebe56333..4cf869e11 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -579,10 +579,7 @@ Examples: "--webhook-secret ", "Secret token to verify Telegram webhook requests", ) - .option( - "--port ", - "Port for webhook server (default 8787)", - ) + .option("--port ", "Port for webhook server (default 8787)") .option( "--webhook-url ", "Public webhook URL to register (overrides localhost autodetect)", @@ -602,7 +599,9 @@ Examples: process.env.TELEGRAM_BOT_TOKEN ?? loadConfig().telegram?.botToken; if (!token) { defaultRuntime.error( - danger("Set TELEGRAM_BOT_TOKEN or telegram.botToken to use telegram relay"), + danger( + "Set TELEGRAM_BOT_TOKEN or telegram.botToken to use telegram relay", + ), ); defaultRuntime.exit(1); return; @@ -612,7 +611,9 @@ Examples: const port = opts.port ? Number.parseInt(String(opts.port), 10) : 8787; const path = opts.webhookPath ?? "/telegram-webhook"; try { - const { monitorTelegramProvider } = await import("../telegram/monitor.js"); + const { monitorTelegramProvider } = await import( + "../telegram/monitor.js" + ); await monitorTelegramProvider({ token, useWebhook: true, diff --git a/src/commands/send.test.ts b/src/commands/send.test.ts index c4bb90056..e7bc3d266 100644 --- a/src/commands/send.test.ts +++ b/src/commands/send.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, afterAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { CliDeps } from "../cli/deps.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -66,9 +66,7 @@ describe("sendCommand", () => { it("falls back to direct send when IPC fails", async () => { sendViaIpcMock.mockResolvedValueOnce({ success: false, error: "nope" }); const deps = makeDeps({ - sendMessageWhatsApp: vi - .fn() - .mockResolvedValue({ messageId: "direct1" }), + sendMessageWhatsApp: vi.fn().mockResolvedValue({ messageId: "direct1" }), }); await sendCommand( { @@ -104,9 +102,7 @@ describe("sendCommand", () => { it("emits json output", async () => { sendViaIpcMock.mockResolvedValueOnce(null); const deps = makeDeps({ - sendMessageWhatsApp: vi - .fn() - .mockResolvedValue({ messageId: "direct2" }), + sendMessageWhatsApp: vi.fn().mockResolvedValue({ messageId: "direct2" }), }); await sendCommand( { diff --git a/src/logging.ts b/src/logging.ts index 1a99cb86c..8cb44be01 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -32,7 +32,7 @@ export type LoggerSettings = { file?: string; }; -type LogObj = Record; +type LogObj = { date?: Date } & Record; type ResolvedSettings = { level: Level; @@ -48,7 +48,9 @@ let consolePatched = false; function normalizeLevel(level?: string): Level { if (isVerbose()) return "trace"; const candidate = level ?? "info"; - return ALLOWED_LEVELS.includes(candidate as Level) ? (candidate as Level) : "info"; + return ALLOWED_LEVELS.includes(candidate as Level) + ? (candidate as Level) + : "info"; } function resolveSettings(): ResolvedSettings { @@ -90,17 +92,15 @@ function buildLogger(settings: ResolvedSettings): TsLogger { type: "hidden", // no ansi formatting }); - logger.attachTransport( - (logObj) => { - try { - const time = (logObj as any)?.date?.toISOString?.() ?? new Date().toISOString(); - const line = JSON.stringify({ ...logObj, time }); - fs.appendFileSync(settings.file, line + "\n", { encoding: "utf8" }); - } catch { - // never block on logging failures - } + logger.attachTransport((logObj: LogObj) => { + try { + const time = logObj.date?.toISOString?.() ?? new Date().toISOString(); + const line = JSON.stringify({ ...logObj, time }); + fs.appendFileSync(settings.file, `${line}\n`, { encoding: "utf8" }); + } catch { + // never block on logging failures } - ); + }); return logger; } @@ -137,7 +137,9 @@ export function toPinoLikeLogger( ): PinoLikeLogger { const buildChild = (bindings?: Record) => toPinoLikeLogger( - logger.getSubLogger({ name: bindings ? JSON.stringify(bindings) : undefined }), + logger.getSubLogger({ + name: bindings ? JSON.stringify(bindings) : undefined, + }), level, ); diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 9482b5465..a6976b4b0 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -3,11 +3,12 @@ import { describe, expect, it, vi } from "vitest"; const useSpy = vi.fn(); const onSpy = vi.fn(); const stopSpy = vi.fn(); -const apiStub = { config: { use: useSpy } }; +type ApiStub = { config: { use: (arg: unknown) => void } }; +const apiStub: ApiStub = { config: { use: useSpy } }; vi.mock("grammy", () => ({ Bot: class { - api = apiStub as any; + api = apiStub; on = onSpy; stop = stopSpy; constructor(public token: string) {} diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index 7688a8487..d1fe292b6 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -1,8 +1,7 @@ import { Buffer } from "node:buffer"; - -import { Bot, InputFile, webhookCallback } from "grammy"; import { apiThrottler } from "@grammyjs/transformer-throttler"; -import type { ApiClientOptions } from "grammy"; +import type { ApiClientOptions, Message } from "grammy"; +import { Bot, InputFile, webhookCallback } from "grammy"; import { chunkText } from "../auto-reply/chunk.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; @@ -16,6 +15,19 @@ import { saveMediaBuffer } from "../media/store.js"; import type { RuntimeEnv } from "../runtime.js"; import { loadWebMedia } from "../web/media.js"; +type TelegramMessage = Message.CommonMessage; + +type TelegramContext = { + message: TelegramMessage; + me?: { username?: string; token?: string }; + api?: { token?: string }; + getFile: () => Promise<{ + getUrl?: (token?: string) => string | Promise; + download: () => Promise; + file_path?: string; + }>; +}; + export type TelegramBotOptions = { token: string; runtime?: RuntimeEnv; @@ -26,14 +38,13 @@ export type TelegramBotOptions = { }; export function createTelegramBot(opts: TelegramBotOptions) { - const runtime: RuntimeEnv = - opts.runtime ?? { - log: console.log, - error: console.error, - exit: (code: number): never => { - throw new Error(`exit ${code}`); - }, - }; + const runtime: RuntimeEnv = opts.runtime ?? { + log: console.log, + error: console.error, + exit: (code: number): never => { + throw new Error(`exit ${code}`); + }, + }; const client: ApiClientOptions | undefined = opts.proxyFetch ? { fetch: opts.proxyFetch as unknown as ApiClientOptions["fetch"] } : undefined; @@ -94,7 +105,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { From: isGroup ? `group:${chatId}` : `telegram:${chatId}`, To: `telegram:${chatId}`, ChatType: isGroup ? "group" : "direct", - GroupSubject: isGroup ? msg.chat.title ?? undefined : undefined, + GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined, SenderName: buildSenderName(msg), Surface: "telegram", MessageSid: String(msg.message_id), @@ -164,7 +175,7 @@ async function deliverReplies(params: { const media = await loadWebMedia(mediaUrl); const kind = mediaKindFromMime(media.contentType ?? undefined); const file = new InputFile(media.buffer, media.fileName ?? "file"); - const caption = first ? reply.text ?? undefined : undefined; + const caption = first ? (reply.text ?? undefined) : undefined; first = false; if (kind === "image") { await bot.api.sendPhoto(chatId, file, { caption }); @@ -179,14 +190,16 @@ async function deliverReplies(params: { } } -function buildSenderName(msg: any) { +function buildSenderName(msg: TelegramMessage) { const name = - [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || - msg.from?.username; + [msg.from?.first_name, msg.from?.last_name] + .filter(Boolean) + .join(" ") + .trim() || msg.from?.username; return name || undefined; } -function hasBotMention(msg: any, botUsername: string) { +function hasBotMention(msg: TelegramMessage, botUsername: string) { const text = (msg.text ?? msg.caption ?? "").toLowerCase(); if (text.includes(`@${botUsername}`)) return true; const entities = msg.entities ?? msg.caption_entities ?? []; @@ -202,7 +215,7 @@ function hasBotMention(msg: any, botUsername: string) { } async function resolveMedia( - ctx: any, + ctx: TelegramContext, maxBytes: number, ): Promise<{ path: string; contentType?: string; placeholder: string } | null> { const msg = ctx.message; diff --git a/src/telegram/download.test.ts b/src/telegram/download.test.ts index fb099c110..fdfb3ab54 100644 --- a/src/telegram/download.test.ts +++ b/src/telegram/download.test.ts @@ -8,7 +8,9 @@ import { describe("telegram download", () => { it("fetches file info", async () => { - const json = vi.fn().mockResolvedValue({ ok: true, result: { file_path: "photos/1.jpg" } }); + const json = vi + .fn() + .mockResolvedValue({ ok: true, result: { file_path: "photos/1.jpg" } }); vi.spyOn(global, "fetch" as never).mockResolvedValueOnce({ ok: true, status: 200, @@ -20,7 +22,10 @@ describe("telegram download", () => { }); it("downloads and saves", async () => { - const info: TelegramFileInfo = { file_id: "fid", file_path: "photos/1.jpg" }; + const info: TelegramFileInfo = { + file_id: "fid", + file_path: "photos/1.jpg", + }; const arrayBuffer = async () => new Uint8Array([1, 2, 3, 4]).buffer; vi.spyOn(global, "fetch" as never).mockResolvedValueOnce({ ok: true, diff --git a/src/telegram/download.ts b/src/telegram/download.ts index 7fb8a538f..9f1b7034c 100644 --- a/src/telegram/download.ts +++ b/src/telegram/download.ts @@ -1,5 +1,5 @@ -import { detectMime, extensionForMime } from "../media/mime.js"; -import { saveMediaBuffer, type SavedMedia } from "../media/store.js"; +import { detectMime } from "../media/mime.js"; +import { type SavedMedia, saveMediaBuffer } from "../media/store.js"; export type TelegramFileInfo = { file_id: string; diff --git a/src/telegram/index.ts b/src/telegram/index.ts index 89dec800d..7effa5e31 100644 --- a/src/telegram/index.ts +++ b/src/telegram/index.ts @@ -1,4 +1,4 @@ -export { sendMessageTelegram } from "./send.js"; -export { monitorTelegramProvider } from "./monitor.js"; export { createTelegramBot, createTelegramWebhookCallback } from "./bot.js"; +export { monitorTelegramProvider } from "./monitor.js"; +export { sendMessageTelegram } from "./send.js"; export { startTelegramWebhook } from "./webhook.js"; diff --git a/src/telegram/monitor.test.ts b/src/telegram/monitor.test.ts index 4b43395d1..1453ffc82 100644 --- a/src/telegram/monitor.test.ts +++ b/src/telegram/monitor.test.ts @@ -2,8 +2,18 @@ import { describe, expect, it, vi } from "vitest"; import { monitorTelegramProvider } from "./monitor.js"; +type MockCtx = { + message: { + chat: { id: number; type: string; title?: string }; + text?: string; + caption?: string; + }; + me?: { username: string }; + getFile: () => Promise; +}; + // Fake bot to capture handler and API calls -const handlers: Record Promise | void> = {}; +const handlers: Record Promise | void> = {}; const api = { sendMessage: vi.fn(), sendPhoto: vi.fn(), @@ -16,7 +26,7 @@ const api = { vi.mock("./bot.js", () => ({ createTelegramBot: () => { - handlers.message = async (ctx: any) => { + handlers.message = async (ctx: MockCtx) => { const chatId = ctx.message.chat.id; const isGroup = ctx.message.chat.type !== "private"; const text = ctx.message.text ?? ctx.message.caption ?? ""; @@ -36,12 +46,16 @@ vi.mock("./bot.js", () => ({ })); vi.mock("../auto-reply/reply.js", () => ({ - getReplyFromConfig: async (ctx: any) => ({ text: `echo:${ctx.Body}` }), + getReplyFromConfig: async (ctx: { Body?: string }) => ({ + text: `echo:${ctx.Body}`, + }), })); describe("monitorTelegramProvider (grammY)", () => { it("processes a DM and sends reply", async () => { - Object.values(api).forEach((fn) => fn?.mockReset?.()); + Object.values(api).forEach((fn) => { + fn?.mockReset?.(); + }); await monitorTelegramProvider({ token: "tok" }); expect(handlers.message).toBeDefined(); await handlers.message?.({ @@ -51,7 +65,7 @@ describe("monitorTelegramProvider (grammY)", () => { text: "hi", }, me: { username: "mybot" }, - getFile: vi.fn(), + getFile: vi.fn(async () => ({})), }); expect(api.sendMessage).toHaveBeenCalledWith(123, "echo:hi", { parse_mode: "Markdown", @@ -59,7 +73,9 @@ describe("monitorTelegramProvider (grammY)", () => { }); it("requires mention in groups by default", async () => { - Object.values(api).forEach((fn) => fn?.mockReset?.()); + Object.values(api).forEach((fn) => { + fn?.mockReset?.(); + }); await monitorTelegramProvider({ token: "tok" }); await handlers.message?.({ message: { @@ -68,7 +84,7 @@ describe("monitorTelegramProvider (grammY)", () => { text: "hello all", }, me: { username: "mybot" }, - getFile: vi.fn(), + getFile: vi.fn(async () => ({})), }); expect(api.sendMessage).not.toHaveBeenCalled(); }); diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 7c120cbca..3c5492c99 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -1,8 +1,6 @@ -import { Bot } from "grammy"; - import { loadConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; -import { createTelegramBot, createTelegramWebhookCallback } from "./bot.js"; +import { createTelegramBot } from "./bot.js"; import { makeProxyFetch } from "./proxy.js"; import { startTelegramWebhook } from "./webhook.js"; @@ -21,13 +19,15 @@ export type MonitorTelegramOpts = { export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { const token = (opts.token ?? process.env.TELEGRAM_BOT_TOKEN)?.trim(); if (!token) { - throw new Error("TELEGRAM_BOT_TOKEN or telegram.botToken is required for Telegram relay"); + throw new Error( + "TELEGRAM_BOT_TOKEN or telegram.botToken is required for Telegram relay", + ); } const proxyFetch = opts.proxyFetch ?? (loadConfig().telegram?.proxy - ? makeProxyFetch(loadConfig().telegram!.proxy as string) + ? makeProxyFetch(loadConfig().telegram?.proxy as string) : undefined); const bot = createTelegramBot({ diff --git a/src/telegram/proxy.ts b/src/telegram/proxy.ts index d1a412c00..ee4068326 100644 --- a/src/telegram/proxy.ts +++ b/src/telegram/proxy.ts @@ -3,5 +3,5 @@ import { ProxyAgent } from "undici"; export function makeProxyFetch(proxyUrl: string): typeof fetch { const agent = new ProxyAgent(proxyUrl); return (input: RequestInfo | URL, init?: RequestInit) => - fetch(input, { ...(init as any), dispatcher: agent } as RequestInit); + fetch(input, { ...(init ?? {}), dispatcher: agent }); } diff --git a/src/telegram/send.test.ts b/src/telegram/send.test.ts index 438a2419b..093fc6f37 100644 --- a/src/telegram/send.test.ts +++ b/src/telegram/send.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, afterAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { sendMessageTelegram } from "./send.js"; @@ -60,9 +60,9 @@ describe("sendMessageTelegram", () => { it("throws on api error", async () => { apiMock.sendMessage.mockRejectedValueOnce(new Error("bad token")); - await expect(sendMessageTelegram("1", "hi", { api: apiMock as never })).rejects.toThrow( - /bad token/i, - ); + await expect( + sendMessageTelegram("1", "hi", { api: apiMock as never }), + ).rejects.toThrow(/bad token/i); }); it("sends media via appropriate method", async () => { diff --git a/src/telegram/send.ts b/src/telegram/send.ts index 023e6751e..c90640bb1 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -19,7 +19,9 @@ type TelegramSendResult = { function resolveToken(explicit?: string): string { const token = explicit ?? process.env.TELEGRAM_BOT_TOKEN; if (!token) { - throw new Error("TELEGRAM_BOT_TOKEN is required for Telegram sends (Bot API)"); + throw new Error( + "TELEGRAM_BOT_TOKEN is required for Telegram sends (Bot API)", + ); } return token.trim(); } @@ -39,7 +41,7 @@ export async function sendMessageTelegram( const token = resolveToken(opts.token); const chatId = normalizeChatId(to); const bot = opts.api ? null : new Bot(token); - const api = opts.api ?? bot!.api; + const api = opts.api ?? bot?.api; const mediaUrl = opts.mediaUrl?.trim(); if (mediaUrl) { @@ -50,7 +52,11 @@ export async function sendMessageTelegram( media.fileName ?? inferFilename(kind) ?? "file", ); const caption = text?.trim() || undefined; - let result; + let result: + | Awaited> + | Awaited> + | Awaited> + | Awaited>; if (kind === "image") { result = await api.sendPhoto(chatId, file, { caption }); } else if (kind === "video") { diff --git a/src/telegram/webhook.test.ts b/src/telegram/webhook.test.ts index 18832b242..657cab274 100644 --- a/src/telegram/webhook.test.ts +++ b/src/telegram/webhook.test.ts @@ -2,10 +2,15 @@ import { describe, expect, it, vi } from "vitest"; import { startTelegramWebhook } from "./webhook.js"; -const handlerSpy = vi.fn((req: any, res: any) => { - res.writeHead(200); - res.end("ok"); -}); +const handlerSpy = vi.fn( + ( + _req: unknown, + res: { writeHead: (status: number) => void; end: (body?: string) => void }, + ) => { + res.writeHead(200); + res.end("ok"); + }, +); const setWebhookSpy = vi.fn(); const stopSpy = vi.fn();