mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
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.
This commit is contained in:
parent
dcbe93be91
commit
bd7a8f2632
39 changed files with 1165 additions and 162 deletions
|
|
@ -37,59 +37,70 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
|||
return matchRoute(route, event);
|
||||
})
|
||||
.map(async (route) => {
|
||||
const target = route.target.threadId
|
||||
? `${route.target.channelId}/${route.target.threadId}`
|
||||
: route.target.channelId;
|
||||
const targets = route.targets && route.targets.length > 0 ? route.targets : [];
|
||||
if (targets.length === 0) return;
|
||||
|
||||
const base: {
|
||||
ts: number;
|
||||
routeId: string;
|
||||
event: string;
|
||||
repo: string | undefined;
|
||||
target: string;
|
||||
deliveryId: string | undefined;
|
||||
actor: string | undefined;
|
||||
action: string | undefined;
|
||||
} = {
|
||||
ts: Date.now(),
|
||||
routeId: route.id,
|
||||
event: event.event,
|
||||
repo: (event.payload.repository as { full_name?: string } | undefined)?.full_name,
|
||||
target,
|
||||
deliveryId: event.deliveryId,
|
||||
actor: (event.payload.sender as { login?: string } | undefined)?.login,
|
||||
action: (event.payload.action as string | undefined),
|
||||
};
|
||||
const tr = trMap.get(route.lang ?? "en")!;
|
||||
const group = route.groupId ? groupById.get(route.groupId) : undefined;
|
||||
const showEmoji = group?.emoji !== false;
|
||||
const message = formatEvent(route, event, tr, showEmoji);
|
||||
|
||||
const started = Date.now();
|
||||
try {
|
||||
const tr = trMap.get(route.lang ?? "en")!;
|
||||
const group = route.groupId ? groupById.get(route.groupId) : undefined;
|
||||
const showEmoji = group?.emoji !== false;
|
||||
const message = formatEvent(route, event, tr, showEmoji);
|
||||
const driver = getDriver(route.target);
|
||||
const result = await driver.send(message, route.target, env);
|
||||
const durationMs = Date.now() - started;
|
||||
if (!result.ok) throw new Error(result.error ?? "Send failed");
|
||||
await recordSend(env.DB, {
|
||||
...base,
|
||||
ok: true,
|
||||
status: result.status,
|
||||
messageId: result.messageId,
|
||||
platform: driver.id,
|
||||
attempts: result.attempts,
|
||||
durationMs,
|
||||
errorCode: result.errorCode,
|
||||
});
|
||||
} catch (err) {
|
||||
const durationMs = Date.now() - started;
|
||||
await recordSend(env.DB, {
|
||||
...base,
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
durationMs,
|
||||
});
|
||||
log.error({ routeId: route.id, err }, "Route failed");
|
||||
for (const target of targets) {
|
||||
const targetStr =
|
||||
target.platform === "telegram"
|
||||
? target.topicId
|
||||
? `${target.chatId}/${target.topicId}`
|
||||
: (target.chatId ?? "")
|
||||
: target.threadId
|
||||
? `${target.channelId}/${target.threadId}`
|
||||
: (target.channelId ?? "");
|
||||
|
||||
const base: {
|
||||
ts: number;
|
||||
routeId: string;
|
||||
event: string;
|
||||
repo: string | undefined;
|
||||
target: string;
|
||||
deliveryId: string | undefined;
|
||||
actor: string | undefined;
|
||||
action: string | undefined;
|
||||
} = {
|
||||
ts: Date.now(),
|
||||
routeId: route.id,
|
||||
event: event.event,
|
||||
repo: (event.payload.repository as { full_name?: string } | undefined)?.full_name,
|
||||
target: targetStr,
|
||||
deliveryId: event.deliveryId,
|
||||
actor: (event.payload.sender as { login?: string } | undefined)?.login,
|
||||
action: (event.payload.action as string | undefined),
|
||||
};
|
||||
|
||||
const started = Date.now();
|
||||
try {
|
||||
const driver = getDriver(target);
|
||||
const result = await driver.send(message, target, env);
|
||||
const durationMs = Date.now() - started;
|
||||
if (!result.ok) throw new Error(result.error ?? "Send failed");
|
||||
await recordSend(env.DB, {
|
||||
...base,
|
||||
ok: true,
|
||||
status: result.status,
|
||||
messageId: result.messageId,
|
||||
platform: driver.id,
|
||||
attempts: result.attempts,
|
||||
durationMs,
|
||||
errorCode: result.errorCode,
|
||||
});
|
||||
} catch (err) {
|
||||
const durationMs = Date.now() - started;
|
||||
await recordSend(env.DB, {
|
||||
...base,
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
durationMs,
|
||||
});
|
||||
log.error({ routeId: route.id, target: targetStr, err }, "Route failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue