{"id":42433595,"url":"https://github.com/brucehart/celebrity-death-bot","last_synced_at":"2026-01-28T04:53:11.364Z","repository":{"id":310204871,"uuid":"1038856799","full_name":"brucehart/celebrity-death-bot","owner":"brucehart","description":"Cloudflare Workers app that watches Wikipedia for newly reported notable deaths, LLM‑filters and de‑duplicates them, then publishes concise memorial posts (Telegram + X) via a lightweight public JSON API. Automates detection, verification, and multi‑platform distribution with low latency and minimal ops overhead.","archived":false,"fork":false,"pushed_at":"2026-01-19T17:26:17.000Z","size":2221,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-19T22:48:54.423Z","etag":null,"topics":["ai-filter","cloudflare-workers","json-api","llm-evaluation","replicate-api","telegrambot","twitter-bot","typescript","wikipedia","wikipedia-scraper"],"latest_commit_sha":null,"homepage":"https://celebritydeathbot.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brucehart.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-08-16T00:27:40.000Z","updated_at":"2026-01-19T17:26:19.000Z","dependencies_parsed_at":"2025-08-16T14:30:37.568Z","dependency_job_id":"7f324787-d10f-41d9-8857-8a80a9e4dec0","html_url":"https://github.com/brucehart/celebrity-death-bot","commit_stats":null,"previous_names":["brucehart/celebrity-death-bot"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/brucehart/celebrity-death-bot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucehart%2Fcelebrity-death-bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucehart%2Fcelebrity-death-bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucehart%2Fcelebrity-death-bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucehart%2Fcelebrity-death-bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brucehart","download_url":"https://codeload.github.com/brucehart/celebrity-death-bot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucehart%2Fcelebrity-death-bot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28839175,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-28T02:10:51.810Z","status":"ssl_error","status_checked_at":"2026-01-28T02:10:50.806Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-filter","cloudflare-workers","json-api","llm-evaluation","replicate-api","telegrambot","twitter-bot","typescript","wikipedia","wikipedia-scraper"],"created_at":"2026-01-28T04:53:10.633Z","updated_at":"2026-01-28T04:53:11.358Z","avatar_url":"https://github.com/brucehart.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"public/Celebrity-Death-Bot.png\" alt=\"Project Logo\" width=\"50%\" /\u003e\n\u003c/p\u003e\n\n# Celebrity Death Bot\n\nCelebrity Death Bot is a Cloudflare Worker that runs on a scheduled Cron trigger. It checks the latest Wikipedia page of notable deaths for the current year and notifies subscribed users when a new entry appears.\n\n## How it works\n\n1. **Fetches Wikipedia**: The worker retrieves `https://en.wikipedia.org/wiki/Deaths_in_\u003cyear\u003e` where `\u003cyear\u003e` is the current year in the America/New_York timezone.\n2. **Parses entries**: From the page, it extracts each person's name, Wikipedia path, age, description and cause of death.\n3. **Stores in D1**: Entries are stored in a D1 database. Items already in the database are ignored.\n4. **LLM evaluation**: Newly discovered entries are evaluated via OpenAI (default) or Replicate (optional). Replicate uses a webhook callback; OpenAI is evaluated inline.\n\nIf `OPENAI_WEBHOOK_SECRET` is set, OpenAI requests are sent in background mode and results are processed via `POST /openai/webhook`. Configure the webhook in the OpenAI dashboard to subscribe to `response.completed`.\n5. **Telegram notifications**: When the callback provides results, the worker sends a message via Telegram to subscribed chats.\n6. **X (Twitter) posting**: If X OAuth 2.0 is connected, each approved result is also posted to the timeline.\n\n## Configuration\n\nThe worker expects the following bindings and environment variables:\n\n- `DB` – D1 database binding used to persist entries.\n- `OPENAI_API_KEY` – OpenAI API key used for Responses API calls (default provider).\n- `OPENAI_WEBHOOK_SECRET` – OpenAI webhook signing secret (optional; enables background Responses + `/openai/webhook` processing).\n- `LLM_PROVIDER` – Optional provider override: `openai` (default) or `replicate`.\n- `REPLICATE_API_TOKEN` – API token for Replicate (required only when `LLM_PROVIDER=replicate`).\n- `TELEGRAM_BOT_TOKEN` – Telegram bot token used for sending messages.\n- `BASE_URL` – Public URL of the worker, used when building Replicate webhook URLs.\n- `REPLICATE_WEBHOOK_SECRET` – Replicate webhook signing secret (only when using Replicate). When set, the\n  worker verifies all Replicate webhook callbacks using HMAC (recommended).\n- `MANUAL_RUN_SECRET` – Secret token required to call the manual `/run` endpoint.\n- X (Twitter) OAuth 2.0 (PKCE) configuration:\n  - `X_CLIENT_ID` – OAuth 2.0 client ID for your X App\n  - `X_CLIENT_SECRET` – (optional) client secret; included when present\n  - `X_ENC_KEY` – base64 AES-256-GCM key to encrypt tokens in D1\n\nWhen connected once via OAuth 2.0, the worker stores and refreshes tokens and posts via `POST /2/tweets` with a Bearer token.\n\nStore secrets with Wrangler (once per environment):\n```bash\nwrangler secret put OPENAI_API_KEY\nwrangler secret put OPENAI_WEBHOOK_SECRET     # optional (OpenAI webhooks)\nwrangler secret put REPLICATE_API_TOKEN      # only if using Replicate\nwrangler secret put TELEGRAM_BOT_TOKEN\nwrangler secret put MANUAL_RUN_SECRET\nwrangler secret put REPLICATE_WEBHOOK_SECRET # optional (Replicate only)\n```\n\n### Cron schedule\n\nThe default schedule runs hourly at minute 5 (see `wrangler.jsonc`). Adjust as needed.\n\n## Development\n\nInstall dependencies and start the development server using Wrangler:\n\n```bash\nnpm install\nnpm run dev\n```\n\nDeploy the worker with:\n\n```bash\nnpm run deploy\n```\n\n## Endpoints\n- `POST /run` – Manually trigger the job. Requires `MANUAL_RUN_SECRET`.\n  - **Auth:** Send the secret in the `Authorization` header:\n    ```\n    Authorization: Bearer \u003cMANUAL_RUN_SECRET\u003e\n    ```\n  - **Full run (curl):**\n    ```bash\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      https://\u003cyour-worker\u003e/run\n    ```\n  - **Targeted reprocess by IDs:** Re-evaluate specific `deaths.id` rows via the configured LLM provider (OpenAI default). These IDs are explicitly flagged in the prompt as MUST INCLUDE so the model accepts them as notable.\n    ```bash\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"ids\":[123,124,130]}' \\\n      https://\u003cyour-worker\u003e/run\n    ```\n    Notes: The worker also avoids marking these IDs as `llm_result = 'no'` if the model unexpectedly omits them; they'll remain pending so you can retry.\n  - **Targeted reprocess by wiki_path(s):** If you prefer specifying the Wikipedia ID(s) instead of database IDs, send `wiki_paths` or a single `wiki_path`. Accepts raw IDs like `Jane_Doe`, full article paths like `/wiki/Jane_Doe`, or edit/redlink URLs like `/w/index.php?title=Jane_Doe\u0026action=edit\u0026redlink=1`.\n    ```bash\n    # Single\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"wiki_path\":\"Jane_Doe\"}' \\\n      https://\u003cyour-worker\u003e/run\n\n    # Multiple\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"wiki_paths\":[\"Jane_Doe\",\"/wiki/John_Smith\",\"/w/index.php?title=Greg_O%2727Connell\u0026action=edit\u0026redlink=1\"]}' \\\n      https://\u003cyour-worker\u003e/run\n    ```\n    Behavior mirrors the ID-based mode: These paths are treated as MUST INCLUDE in the LLM prompt and won’t be auto-marked `no` if omitted.\n  - **Retry pending LLM batches:** Re-evaluate rows stuck with `llm_result = 'pending'` (useful after LLM outages). For OpenAI (default), omit `pending_limit` to drain all pending rows; add `pending_limit` to cap volume. For Replicate, the default remains 120 rows per call, split into batches of 30 for safer prompts.\n    ```bash\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"retry_pending\":true}' \\\n      https://\u003cyour-worker\u003e/run\n    ```\n    To cap the batch size, include `pending_limit`:\n    ```bash\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"retry_pending\":true,\"pending_limit\":150}' \\\n      https://\u003cyour-worker\u003e/run\n    ```\n  - **Use a different provider/model (full run, retry, or targeted reprocess):** Include `provider` in the JSON body to switch between `openai` and `replicate`. For OpenAI, use model IDs like `gpt-5-mini`. For Replicate, use model paths like `openai/gpt-5-mini` or `google/gemini-3-pro`.\n    ```bash\n    curl -X POST \\\n      -H \"Authorization: Bearer $MANUAL_RUN_SECRET\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"retry_pending\":true,\"provider\":\"replicate\",\"model\":\"google/gemini-3-pro\"}' \\\n      https://\u003cyour-worker\u003e/run\n    ```\n- `POST /replicate/callback` – Endpoint for Replicate webhook callbacks (only when using `LLM_PROVIDER=replicate`; verified via HMAC if `REPLICATE_WEBHOOK_SECRET` is set).\n  - Manual override: send the same `Authorization: Bearer \u003cMANUAL_RUN_SECRET\u003e` header that `/run` uses to bypass the HMAC requirement. Useful when you need to craft a callback payload to force a “yes” decision.\n- `POST /openai/webhook` – Endpoint for OpenAI webhook callbacks (enable by setting `OPENAI_WEBHOOK_SECRET` and configuring the webhook in the OpenAI dashboard for `response.completed`, `response.failed`, and `response.cancelled`).\n- `POST /telegram/webhook` – Telegram webhook endpoint for subscription commands. If `TELEGRAM_WEBHOOK_SECRET` is set, Telegram must send header `X-Telegram-Bot-Api-Secret-Token` with the same secret.\n- `GET /health` – Simple health check returning `ok`.\n\n## Rate Limiting\n\nThe `POST /run` endpoint is rate-limited to protect the worker from abuse and accidental overload.\n\n- Default limits: 3 requests per 60 seconds and 20 requests per hour per client IP.\n- Configuration: Override with the env var `RUN_RATE_LIMITS` as a comma-separated list of `\u003cwindowInSeconds\u003e:\u003climit\u003e` pairs.\n  - Example: `RUN_RATE_LIMITS=\"60:5,3600:50\"` sets 5/minute and 50/hour.\n- Behavior: If a limit is exceeded, the endpoint responds with `429 Too Many Requests` and includes a `Retry-After` header (seconds).\n- Logging: Exceed events are logged with IP and window info for operational visibility.\n\nSchema\n- The limiter uses a small D1 table `rate_limits` for counters.\n- Apply the migration:\n  ```bash\n  wrangler d1 execute celebrity-death-bot --file=./migrations/002_create_rate_limits.sql\n  ```\n\n## Database Schema\n\nThe worker stores parsed entries and subscriber/chat state in D1.\n\n- Death entries: `migrations/003_create_deaths.sql`\n  ```bash\n  wrangler d1 execute celebrity-death-bot --file=./migrations/003_create_deaths.sql\n  ```\n- Telegram subscribers: `migrations/001_create_subscribers.sql`\n  ```bash\n  wrangler d1 execute celebrity-death-bot --file=./migrations/001_create_subscribers.sql\n  ```\n\n## Telegram Webhook \u0026 Commands\n\nConfigure your bot to send updates to the Worker and let users manage subscriptions via chat.\n\n- Set the Telegram webhook URL and provide a secret token that Telegram will send in header `X-Telegram-Bot-Api-Secret-Token` with every webhook request:\n  ```bash\n  export BASE_URL=\u003cyour-worker-url\u003e\n  export TELEGRAM_BOT_TOKEN=\u003cyour-token\u003e\n  export TELEGRAM_WEBHOOK_SECRET=\u003cyour-secret\u003e # allowed chars: A-Z a-z 0-9 _ -\n\n  # Store secret in Worker\n  wrangler secret put TELEGRAM_WEBHOOK_SECRET \u003c\u003c\u003c \"$TELEGRAM_WEBHOOK_SECRET\"\n\n  # Configure webhook and secret_token on Telegram side\n  curl -X POST \\\n    \"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook\" \\\n    -d url=\"${BASE_URL}/telegram/webhook\" \\\n    -d secret_token=\"${TELEGRAM_WEBHOOK_SECRET}\"\n  ```\n- Supported commands (send in a DM to your bot):\n  - `/start` or `/subscribe` – Subscribe this chat to alerts.\n  - `/stop` or `/unsubscribe` – Unsubscribe this chat (we delete your chat ID).\n  - `/status` – Show current subscription status.\n  - `/commands` – Show the list of available commands.\n\nNotes\n- Subscriptions are stored in the D1 table `subscribers` with fields: `id`, `type`, `chat_id`, `enabled`, `created_at` (unique on `(type, chat_id)`).\n- Only `type = 'telegram'` is used currently; the schema allows future channels (SMS, Signal, etc.).\n- Schema is managed outside runtime. A sample migration exists at `migrations/001_create_subscribers.sql`.\n- Apply schema (option A — migration file):\n  ```bash\n  wrangler d1 execute celebrity-death-bot --file=./migrations/001_create_subscribers.sql\n  ```\n- Or create manually (option B):\n  ```sql\n  CREATE TABLE IF NOT EXISTS subscribers (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    type TEXT NOT NULL,\n    chat_id TEXT NOT NULL,\n    enabled INTEGER NOT NULL DEFAULT 1,\n    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    UNIQUE (type, chat_id)\n  );\n  ```\n- Add secrets via Wrangler: `wrangler secret put TELEGRAM_WEBHOOK_SECRET` and ensure `TELEGRAM_BOT_TOKEN` is set.\n\n## X (Twitter) Posting\n\nThe worker can post each LLM-approved death to X (Twitter) at `x.com/CelebDeathBot`.\n\n- Format matches Telegram, but the Wikipedia link is appended at the end (since X posts cannot embed clickable HTML links):\n  - Example: `🚨💀Jane Doe (88) : American actor and philanthropist - cancer 💀🚨\\nhttps://en.wikipedia.org/wiki/Jane_Doe`\n- Length is constrained to 280 characters with t.co URL weighting (23 chars). The body text is truncated with an ellipsis if necessary.\n\nSetup (OAuth 2.0, PKCE)\n- In your X developer app, enable OAuth 2.0 user auth with scopes: `tweet.read tweet.write users.read offline.access`.\n- Store secrets (never commit these):\n  ```bash\n  wrangler secret put X_CLIENT_ID\n  wrangler secret put X_CLIENT_SECRET   # optional; included when present\n  wrangler secret put X_ENC_KEY         # base64 32-byte key for AES-GCM\n  ```\n- Apply the migration for token storage:\n  ```bash\n  wrangler d1 execute celebrity-death-bot --file=./migrations/004_create_x_oauth.sql\n  ```\n- Connect the bot account by visiting:\n  - `GET ${BASE_URL}/x/oauth/start` → authorizes via X; callback goes to `${BASE_URL}/x/oauth/callback`.\n  - Verify status: `GET ${BASE_URL}/x/oauth/status` → `{ connected: true, expires_at: \u003cunix\u003e }`.\n\nSecurity notes\n- Access and refresh tokens are encrypted at rest in D1 via AES-256-GCM using `X_ENC_KEY`.\n- Tokens are auto-refreshed as they near expiry; no interactive login is needed after the first connect.\n\n## Replicate Webhook Signing (HMAC)\n\nOnly required when using `LLM_PROVIDER=replicate`.\n\nReplicate signs each webhook delivery. This worker verifies signatures to prevent spoofed or replayed requests.\n\n- Headers used: `webhook-id`, `webhook-timestamp` (seconds), `webhook-signature`.\n- Signed content: `${webhook-id}.${webhook-timestamp}.${rawBody}` (raw, unmodified body string).\n- Algorithm: HMAC-SHA256 with your Replicate webhook signing key.\n- Timestamp window: 5 minutes (requests older than this are rejected).\n\nSetup\n- Retrieve your signing key from Replicate (associated with your API token):\n  ```bash\n  curl -s -H \"Authorization: Bearer $REPLICATE_API_TOKEN\" \\\n    https://api.replicate.com/v1/webhooks/default/secret\n  # { \"key\": \"whsec_...\" }\n  ```\n- Store the key as a Worker secret:\n  ```bash\n  wrangler secret put REPLICATE_WEBHOOK_SECRET\n  ```\n\nNotes\n- Do not append secrets to the webhook URL. This worker no longer uses `?secret=...` for Replicate callbacks; it relies on HMAC verification only.\n- The secret format is `whsec_\u003cbase64\u003e`. Only the base64 part is used as the raw HMAC key.\n- The worker uses constant-time comparison and enforces a 5-minute timestamp tolerance to mitigate replay attacks.\n\n## OpenAI Webhooks\n\nWhen `OPENAI_WEBHOOK_SECRET` is set, OpenAI requests are sent in background mode and results are processed via `POST /openai/webhook`. Configure a webhook endpoint in the OpenAI dashboard and subscribe to `response.completed`, `response.failed`, and `response.cancelled`.\n\n- Headers used: `webhook-id`, `webhook-timestamp`, `webhook-signature`\n- Signed content: `${webhook-id}.${webhook-timestamp}.${rawBody}`\n- Algorithm: HMAC-SHA256 with your OpenAI webhook signing secret (supports `whsec_...` format)\n\n## Testing\n\nThis repo uses Node’s built-in `node:test` for a few focused unit tests around webhook verification, Telegram HTML sanitization, Wikipedia parsing, and JSON extraction helpers.\n\n- Run all tests:\n  ```bash\n  npm test\n  ```\n\nIf you prefer Vitest, you can add it later; place tests beside code as `*.test.ts`.\n\n## Release Notes\n\n- See `release-notes/` for version-specific notes (e.g., v1.1, v1.2).\n\n## License\n\nThis project is licensed under the [MIT License](LICENSE.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrucehart%2Fcelebrity-death-bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrucehart%2Fcelebrity-death-bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrucehart%2Fcelebrity-death-bot/lists"}