Parent directory

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};