mirror of
https://github.com/tiennm99/claude-status-webhook.git
synced 2026-04-17 13:21:01 +00:00
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
85 lines
2.2 KiB
JavaScript
85 lines
2.2 KiB
JavaScript
const KV_KEY = "claude-status:subscribers";
|
|
|
|
/**
|
|
* Build composite key: "chatId" or "chatId:threadId" for supergroup topics
|
|
*/
|
|
export function buildSubscriberKey(chatId, threadId) {
|
|
return threadId != null ? `${chatId}:${threadId}` : `${chatId}`;
|
|
}
|
|
|
|
/**
|
|
* Parse composite key back into { chatId, threadId }
|
|
*/
|
|
export function parseSubscriberKey(key) {
|
|
const parts = key.split(":");
|
|
if (parts.length >= 2 && parts[0].startsWith("-")) {
|
|
// Supergroup IDs start with "-100", so key is "-100xxx:threadId"
|
|
// Find the last ":" — everything before is chatId, after is threadId
|
|
const lastColon = key.lastIndexOf(":");
|
|
return {
|
|
chatId: key.slice(0, lastColon),
|
|
threadId: parseInt(key.slice(lastColon + 1), 10),
|
|
};
|
|
}
|
|
return { chatId: key, threadId: null };
|
|
}
|
|
|
|
/**
|
|
* Get all subscribers from KV
|
|
*/
|
|
export async function getSubscribers(kv) {
|
|
const data = await kv.get(KV_KEY, "json");
|
|
return data || {};
|
|
}
|
|
|
|
/**
|
|
* Write all subscribers to KV
|
|
*/
|
|
async function setSubscribers(kv, data) {
|
|
await kv.put(KV_KEY, JSON.stringify(data));
|
|
}
|
|
|
|
/**
|
|
* Add subscriber with default types
|
|
*/
|
|
export async function addSubscriber(kv, chatId, threadId, types = ["incident", "component"]) {
|
|
const subs = await getSubscribers(kv);
|
|
const key = buildSubscriberKey(chatId, threadId);
|
|
subs[key] = { types };
|
|
await setSubscribers(kv, subs);
|
|
}
|
|
|
|
/**
|
|
* Remove subscriber
|
|
*/
|
|
export async function removeSubscriber(kv, chatId, threadId) {
|
|
const subs = await getSubscribers(kv);
|
|
const key = buildSubscriberKey(chatId, threadId);
|
|
delete subs[key];
|
|
await setSubscribers(kv, subs);
|
|
}
|
|
|
|
/**
|
|
* Update subscriber's notification type preferences
|
|
*/
|
|
export async function updateSubscriberTypes(kv, chatId, threadId, types) {
|
|
const subs = await getSubscribers(kv);
|
|
const key = buildSubscriberKey(chatId, threadId);
|
|
if (!subs[key]) {
|
|
subs[key] = { types };
|
|
} else {
|
|
subs[key].types = types;
|
|
}
|
|
await setSubscribers(kv, subs);
|
|
}
|
|
|
|
/**
|
|
* Get subscribers filtered by event type, returns [{ chatId, threadId }, ...]
|
|
*/
|
|
export async function getSubscribersByType(kv, eventType) {
|
|
const subs = await getSubscribers(kv);
|
|
return Object.entries(subs)
|
|
.filter(([, val]) => val.types.includes(eventType))
|
|
.map(([key]) => parseSubscriberKey(key));
|
|
}
|