feat: implement Telegram bot for Claude status webhooks

Cloudflare Workers bot that forwards status.claude.com (Atlassian
Statuspage) incident and component updates to subscribed Telegram
users via CF Queues fan-out.

- Hono.js routing with grammY webhook handler
- Bot commands: /start, /stop, /status, /subscribe
- Supergroup topic support (message_thread_id)
- KV store with claude-status: prefix and composite keys
- Queue consumer with batch send, 403 auto-removal, 429 retry
- Timing-safe webhook secret validation
- HTML escaping for Telegram messages
This commit is contained in:
2026-04-08 22:59:37 +07:00
parent 902b46720d
commit 01320abacd
9 changed files with 2123 additions and 0 deletions

53
src/status-fetcher.js Normal file
View File

@@ -0,0 +1,53 @@
const STATUS_API = "https://status.claude.com/api/v2/summary.json";
/**
* Fetch all components from status.claude.com (excludes group-level entries)
*/
export async function fetchAllComponents() {
const res = await fetch(STATUS_API);
if (!res.ok) throw new Error(`Status API returned ${res.status}`);
const data = await res.json();
return data.components.filter((c) => !c.group);
}
/**
* Fuzzy match a component by name (case-insensitive includes)
*/
export async function fetchComponentByName(name) {
const components = await fetchAllComponents();
return components.find((c) =>
c.name.toLowerCase().includes(name.toLowerCase())
);
}
/**
* Human-readable status label
*/
export function humanizeStatus(status) {
const map = {
operational: "Operational",
degraded_performance: "Degraded Performance",
partial_outage: "Partial Outage",
major_outage: "Major Outage",
under_maintenance: "Under Maintenance",
investigating: "Investigating",
identified: "Identified",
monitoring: "Monitoring",
resolved: "Resolved",
};
return map[status] || status;
}
/**
* Escape HTML special chars for Telegram's HTML parse mode
*/
export function escapeHtml(s) {
return s?.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") ?? "";
}
/**
* Format a single component as HTML line
*/
export function formatComponentLine(component) {
return `<b>${escapeHtml(component.name)}</b>: <code>${humanizeStatus(component.status)}</code>`;
}