mirror of
https://github.com/tiennm99/claude-status-webhook.git
synced 2026-04-17 15:20:37 +00:00
refactor: per-subscriber KV keys, HMAC verification, cron trigger
Major refactor addressing scalability, security, and reliability:
- KV schema: single-key → per-subscriber keys (sub:{chatId}:{threadId})
eliminates read-modify-write race conditions
- Component-specific subscriptions: /subscribe component <name>
- HMAC-SHA256 webhook verification with URL secret fallback
- Cron trigger (every 5 min) polls status.claude.com as safety net
- Shared telegram-api.js module (DRY fix)
- Error logging in all catch blocks
- Migration endpoint for existing subscribers
- Setup moved to standalone script (scripts/setup-bot.js)
- Removed setup HTTP route to reduce attack surface
This commit is contained in:
@@ -3,9 +3,10 @@ import {
|
||||
addSubscriber,
|
||||
removeSubscriber,
|
||||
updateSubscriberTypes,
|
||||
getSubscribers,
|
||||
buildSubscriberKey,
|
||||
updateSubscriberComponents,
|
||||
getSubscriber,
|
||||
} from "./kv-store.js";
|
||||
import { fetchComponentByName, escapeHtml } from "./status-fetcher.js";
|
||||
import { registerInfoCommands } from "./bot-info-commands.js";
|
||||
|
||||
/**
|
||||
@@ -46,7 +47,50 @@ export async function handleTelegramWebhook(c) {
|
||||
|
||||
bot.command("subscribe", async (ctx) => {
|
||||
const { chatId, threadId } = getChatTarget(ctx);
|
||||
const arg = ctx.match?.trim().toLowerCase();
|
||||
const args = ctx.match?.trim().toLowerCase().split(/\s+/) || [];
|
||||
const arg = args[0];
|
||||
|
||||
// Handle "/subscribe component <name>" or "/subscribe component all"
|
||||
if (arg === "component" && args.length > 1) {
|
||||
const componentArg = args.slice(1).join(" ");
|
||||
const sub = await getSubscriber(kv, chatId, threadId);
|
||||
if (!sub) {
|
||||
await ctx.reply("Not subscribed yet. Use /start first.", { parse_mode: "HTML" });
|
||||
return;
|
||||
}
|
||||
if (componentArg === "all") {
|
||||
await updateSubscriberComponents(kv, chatId, threadId, []);
|
||||
await ctx.reply("Component filter cleared — receiving all component updates.", {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Validate component name against live API
|
||||
const component = await fetchComponentByName(componentArg);
|
||||
if (!component) {
|
||||
await ctx.reply(`Component "<code>${escapeHtml(componentArg)}</code>" not found.`, {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Add to component filter (deduplicate)
|
||||
const components = sub.components || [];
|
||||
if (!components.some((c) => c.toLowerCase() === component.name.toLowerCase())) {
|
||||
components.push(component.name);
|
||||
}
|
||||
await updateSubscriberComponents(kv, chatId, threadId, components);
|
||||
// Ensure "component" is in types
|
||||
if (!sub.types.includes("component")) {
|
||||
sub.types.push("component");
|
||||
await updateSubscriberTypes(kv, chatId, threadId, sub.types);
|
||||
}
|
||||
await ctx.reply(
|
||||
`Subscribed to component: <code>${escapeHtml(component.name)}</code>\n` +
|
||||
`Active filters: <code>${components.join(", ")}</code>`,
|
||||
{ parse_mode: "HTML" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const validTypes = {
|
||||
incident: ["incident"],
|
||||
@@ -55,19 +99,26 @@ export async function handleTelegramWebhook(c) {
|
||||
};
|
||||
|
||||
if (!arg || !validTypes[arg]) {
|
||||
const key = buildSubscriberKey(chatId, threadId);
|
||||
const subs = await getSubscribers(kv);
|
||||
const current = subs[key]?.types?.join(", ") || "none (use /start first)";
|
||||
const sub = await getSubscriber(kv, chatId, threadId);
|
||||
const current = sub?.types?.join(", ") || "none (use /start first)";
|
||||
const compFilter = sub?.components?.length ? sub.components.join(", ") : "all";
|
||||
await ctx.reply(
|
||||
"<b>Usage:</b> /subscribe <type>\n\n" +
|
||||
"<b>Usage:</b> /subscribe <type> [component]\n\n" +
|
||||
"Types: <code>incident</code>, <code>component</code>, <code>all</code>\n" +
|
||||
`\nCurrent: <code>${current}</code>`,
|
||||
"Component filter: <code>/subscribe component api</code>\n" +
|
||||
"Clear filter: <code>/subscribe component all</code>\n" +
|
||||
`\nCurrent types: <code>${current}</code>\n` +
|
||||
`Components: <code>${compFilter}</code>`,
|
||||
{ parse_mode: "HTML" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateSubscriberTypes(kv, chatId, threadId, validTypes[arg]);
|
||||
const updated = await updateSubscriberTypes(kv, chatId, threadId, validTypes[arg]);
|
||||
if (!updated) {
|
||||
await ctx.reply("Not subscribed yet. Use /start first.", { parse_mode: "HTML" });
|
||||
return;
|
||||
}
|
||||
await ctx.reply(`Subscription updated: <code>${validTypes[arg].join(", ")}</code>`, {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user