main.ts
3066 bytes
1import type { Plugin } from "@opencode-ai/plugin";
2import { tool } from "@opencode-ai/plugin";
3import type { Bot } from "grammy";
4import { readFileSync } from "node:fs";
5import { join } from "node:path";
6import { homedir } from "node:os";
7import * as log from "./log.js";
8import { init, setAttachedSession, clearAttachedSession } from "./opencode.js";
9import { handleEvent } from "./events.js";
10import { BOT_COMMANDS, createBot } from "./telegram.js";
11
12interface Config {
13 token: string;
14 allowedUsers: number[];
15}
16
17function loadConfig(): Config {
18 const configPath = join(homedir(), ".config", "opencode", "telegram.json");
19 let raw: string;
20 try {
21 raw = readFileSync(configPath, "utf-8");
22 } catch {
23 throw new Error(`cannot read config: ${configPath}`);
24 }
25 const json = JSON.parse(raw) as Record<string, unknown>;
26 const token = json.token;
27 if (typeof token !== "string" || !token) {
28 throw new Error(`"token" missing in ${configPath}`);
29 }
30 const users = json.allowedUsers;
31 if (!Array.isArray(users) || users.length === 0) {
32 throw new Error(
33 `"allowedUsers" must be a non-empty array in ${configPath}`,
34 );
35 }
36 return { token, allowedUsers: users.map(Number) };
37}
38
39export const OpenCodeTelegram: Plugin = async ({ client, directory }) => {
40 init(client, directory);
41
42 let config: Config | null = null;
43 let configError: string | null = null;
44 try {
45 config = loadConfig();
46 } catch (err) {
47 configError = String(err instanceof Error ? err.message : err);
48 log.error("config load failed:", configError);
49 }
50
51 let bot: Bot | null = null;
52 const autoConnect = process.env.TELEGRAM_AUTOCONNECT === "1";
53
54 async function connect(): Promise<string> {
55 if (!config) return `error: ${configError}`;
56 if (bot) return "already connected";
57 bot = createBot(config.token, config.allowedUsers);
58 await bot.api.setMyCommands(BOT_COMMANDS);
59 log.info("telegram bot starting long-polling...");
60 bot.start();
61 return "connected";
62 }
63
64 async function disconnect(): Promise<string> {
65 if (!bot) return "not connected";
66 await bot.stop();
67 bot = null;
68 clearAttachedSession();
69 log.info("telegram bot stopped");
70 return "disconnected";
71 }
72
73 if (autoConnect) {
74 connect().catch((err) => log.error("autoconnect failed:", err));
75 }
76
77 return {
78 event: async ({ event }) => {
79 try {
80 handleEvent(event);
81 } catch (err) {
82 log.error("event handler error:", err);
83 }
84 },
85 tool: {
86 telegram: tool({
87 description:
88 "Connect or disconnect the Telegram bot. Actions: connect, disconnect, status.",
89 args: {
90 action: tool.schema.enum(["connect", "disconnect", "status"]),
91 },
92 async execute(args, context) {
93 if (args.action === "connect") {
94 setAttachedSession(context.sessionID);
95 return connect();
96 }
97 if (args.action === "disconnect") return disconnect();
98 return bot ? "connected" : "disconnected";
99 },
100 }),
101 },
102 };
103};