WebHooker/src/config.ts
RhenCloud bd7a8f2632
feat: add Telegram push support with multi-target routes
Add a platform-aware route target system so a single route can forward
to several destinations at once (e.g. a Discord channel and a Telegram
group). Route.target becomes Route.targets[] with per-entry platform,
channelId/threadId for Discord and chatId/topicId for Telegram; the
legacy single-target format is normalized on load and accepted by the
admin API.

Implement the Telegram driver with HTML rendering and Bot API
sendMessage (chat_id + message_thread_id for topics, retry on 429/5xx),
plus /gh commands served over POST /telegram/webhook: login, logout,
comment, merge and close. The comment/merge/close commands resolve the
issue or PR from the replied-to notification message. OAuth binding now
stores a D1 telegram_links mapping and replies with a confirmation.

Sync the Telegram webhook from the scheduled trigger via setWebhook.
2026-08-03 05:19:05 +08:00

64 lines
1.9 KiB
TypeScript

import type { Env, Config, Route } from "./types";
import { log } from "./lib/log";
const CONFIG_CACHE_TTL = 60_000;
const ROUTES_KEY = "config:routes";
let configCache: { config: Config; expiresAt: number } | null = null;
export async function loadRoutes(kv: KVNamespace): Promise<Route[]> {
try {
const stored = await kv.get<Route[]>(ROUTES_KEY, "json");
if (stored) return normalizeRoutes(stored);
} catch (err) {
log.warn({ err }, "Failed to load routes from KV");
}
return [];
}
/**
* Migrate legacy single-target routes (`target`) to the array form (`targets`).
*/
function normalizeRoutes(routes: Route[]): Route[] {
return routes.map((r) => {
if (r.targets && r.targets.length > 0) return r;
const legacy = (r as Route & { target?: Route["targets"][number] }).target;
if (!legacy) return r;
const { target: _target, ...rest } = r as Route & { target?: Route["targets"][number] };
return { ...rest, targets: [legacy] };
});
}
export async function saveRoutes(kv: KVNamespace, routes: Route[]): Promise<void> {
await kv.put(ROUTES_KEY, JSON.stringify(routes));
configCache = null;
}
export function invalidateConfigCache(): void {
configCache = null;
}
export async function loadConfig(env: Env): Promise<Config> {
if (configCache && Date.now() < configCache.expiresAt) {
return configCache.config;
}
const routes = await loadRoutes(env.KV);
const config: Config = {
baseUrl: env.BASE_URL ?? "https://webhooker.example.workers.dev",
github: {
webhookSecret: env.GITHUB_WEBHOOK_SECRET,
appId: Number(env.GITHUB_APP_ID ?? 0),
privateKey: env.GITHUB_PRIVATE_KEY ?? "",
clientId: env.GITHUB_CLIENT_ID ?? "",
clientSecret: env.GITHUB_CLIENT_SECRET ?? "",
},
discord: {
token: env.DISCORD_TOKEN ?? "",
},
routes,
};
configCache = { config, expiresAt: Date.now() + CONFIG_CACHE_TTL };
return config;
}