chore(ci): fix lint and swiftformat failures
This commit is contained in:
@@ -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) {}
|
||||
|
||||
@@ -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<string>;
|
||||
download: () => Promise<Uint8Array | ArrayBuffer>;
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<unknown>;
|
||||
};
|
||||
|
||||
// Fake bot to capture handler and API calls
|
||||
const handlers: Record<string, (ctx: any) => Promise<void> | void> = {};
|
||||
const handlers: Record<string, (ctx: MockCtx) => Promise<void> | 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();
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<ReturnType<typeof api.sendPhoto>>
|
||||
| Awaited<ReturnType<typeof api.sendVideo>>
|
||||
| Awaited<ReturnType<typeof api.sendAudio>>
|
||||
| Awaited<ReturnType<typeof api.sendDocument>>;
|
||||
if (kind === "image") {
|
||||
result = await api.sendPhoto(chatId, file, { caption });
|
||||
} else if (kind === "video") {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user