feat(telegram): add CLI project with Bot API client

This commit is contained in:
opencode-agent 2026-07-31 17:33:47 +00:00
parent 43b60d6101
commit 3aef215663
9 changed files with 2202 additions and 0 deletions

1
.opencode/telegram/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

62
.opencode/telegram/cli.ts Normal file
View file

@ -0,0 +1,62 @@
import { loadConfig } from "./src/config.ts"
import { sendMessage, sendDocument, sendPhoto } from "./src/api.ts"
function parseArgs(argv: string[]): { action: string; opts: Record<string, string> } {
const action = argv[0] ?? ""
const opts: Record<string, string> = {}
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a.startsWith("--")) {
const key = a.slice(2)
const val = argv[i + 1] ?? ""
opts[key] = val
i++
}
}
return { action, opts }
}
async function main(): Promise<void> {
const argv = process.argv.slice(2)
if (argv.length < 1) {
process.stderr.write('usage: node cli.ts <text|document|photo> --text "..." [--chat-id ...] [--path ...] [--caption ...] [--parse-mode MarkdownV2|HTML|plain]\n')
process.exit(2)
}
const { action, opts } = parseArgs(argv)
const chatIdOverride = opts["chat-id"] ? { chatId: opts["chat-id"] } : undefined
const { token, chatId } = loadConfig(chatIdOverride)
const parseMode = (opts["parse-mode"] ?? "MarkdownV2") as "MarkdownV2" | "HTML" | "plain"
let result
if (action === "text") {
if (!opts.text) {
process.stderr.write('error: --text is required for action "text"\n')
process.exit(2)
}
result = await sendMessage(token, chatId, opts.text, parseMode)
} else if (action === "document") {
if (!opts.path) {
process.stderr.write('error: --path is required for action "document"\n')
process.exit(2)
}
result = await sendDocument(token, chatId, opts.path, opts.caption, parseMode)
} else if (action === "photo") {
if (!opts.path) {
process.stderr.write('error: --path is required for action "photo"\n')
process.exit(2)
}
result = await sendPhoto(token, chatId, opts.path, opts.caption, parseMode)
} else {
process.stderr.write(`error: unknown action "${action}" (expected: text|document|photo)\n`)
process.exit(2)
}
console.log(JSON.stringify(result))
}
main().catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err)
process.stderr.write(`⚠️ telegram-send failed (exit 1): ${msg}\n`)
process.exit(1)
})

1979
.opencode/telegram/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
{
"name": "telegram",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Telegram Bot API client for sending messages, documents, and photos",
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"@types/node": "^26.1.1",
"typescript": "^7.0.2",
"vitest": "^3.2.4"
}
}

View file

@ -0,0 +1,87 @@
import { readFileSync } from "node:fs"
import { basename } from "node:path"
export interface TelegramResult {
ok: true
message_id: number
chat_id: string
}
type ParseMode = "MarkdownV2" | "HTML" | "plain"
const API_BASE = "https://api.telegram.org/bot"
async function telegramFetch(
token: string,
method: string,
body: BodyInit,
headers?: Record<string, string>,
): Promise<TelegramResult> {
const url = `${API_BASE}${token}/${method}`
const res = await fetch(url, { method: "POST", body, headers })
const data = await res.json() as { ok: boolean; description?: string; result?: { message_id: number; chat: { id: number | string } } }
if (!data.ok) {
throw new Error(data.description ?? `Telegram API error (HTTP ${res.status})`)
}
if (!data.result) {
throw new Error("Telegram API returned no result")
}
return {
ok: true,
message_id: data.result.message_id,
chat_id: String(data.result.chat.id),
}
}
function shouldIncludeParseMode(parseMode?: ParseMode): parseMode is "MarkdownV2" | "HTML" {
return parseMode === "MarkdownV2" || parseMode === "HTML"
}
export async function sendMessage(
token: string,
chatId: string,
text: string,
parseMode?: ParseMode,
): Promise<TelegramResult> {
const body: Record<string, string> = { chat_id: chatId, text }
if (shouldIncludeParseMode(parseMode)) {
body.parse_mode = parseMode
}
return telegramFetch(token, "sendMessage", JSON.stringify(body), {
"Content-Type": "application/json",
})
}
export async function sendDocument(
token: string,
chatId: string,
filePath: string,
caption?: string,
parseMode?: ParseMode,
): Promise<TelegramResult> {
const buf = readFileSync(filePath)
const blob = new Blob([buf])
const form = new FormData()
form.append("chat_id", chatId)
form.append("document", blob, basename(filePath))
if (caption) form.append("caption", caption)
if (shouldIncludeParseMode(parseMode)) form.append("parse_mode", parseMode)
return telegramFetch(token, "sendDocument", form)
}
export async function sendPhoto(
token: string,
chatId: string,
filePath: string,
caption?: string,
parseMode?: ParseMode,
): Promise<TelegramResult> {
const buf = readFileSync(filePath)
const blob = new Blob([buf])
const form = new FormData()
form.append("chat_id", chatId)
form.append("photo", blob, basename(filePath))
if (caption) form.append("caption", caption)
if (shouldIncludeParseMode(parseMode)) form.append("parse_mode", parseMode)
return telegramFetch(token, "sendPhoto", form)
}

View file

@ -0,0 +1,18 @@
export interface TelegramConfig {
token: string
chatId: string
}
export function loadConfig(overrides?: { chatId?: string }): TelegramConfig {
const token = process.env.TELEGRAM_BOT_TOKEN
if (!token) {
throw new Error("TELEGRAM_BOT_TOKEN env var is required (see .env.example)")
}
const chatId = overrides?.chatId ?? process.env.TELEGRAM_CHAT_ID
if (!chatId) {
throw new Error("chat_id is required: pass --chat-id or set TELEGRAM_CHAT_ID env var")
}
return { token, chatId }
}

View file

@ -0,0 +1,15 @@
export const MARKDOWN_V2_SPECIAL_CHARS = [
"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!",
] as const
export function escapeMarkdownV2(text: string): string {
let out = ""
for (const ch of text) {
if ((MARKDOWN_V2_SPECIAL_CHARS as readonly string[]).includes(ch)) {
out += "\\" + ch
} else {
out += ch
}
}
return out
}

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts", "cli.ts", "tests/**/*.ts"]
}

View file

@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
environment: "node",
include: ["tests/**/*.test.ts"],
testTimeout: 30000,
},
})