Add Telegram bot with private linking, notification delivery, and commands#930
Add Telegram bot with private linking, notification delivery, and commands#930JoaquinBN wants to merge 12 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Telegram account linking, bot commands, notification outbox delivery, recall handling, private profile integration, scheduled workflows, deployment configuration, and backend/frontend tests. ChangesTelegram platform integration
Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Portal contributors can now link Telegram from their profile header via a
one-time deep link to the portal bot. The connection is strictly private:
the API serializes it for the owner (and staff) only, it never renders on
other users' profiles, and the old manually-typed public Telegram handle
was removed from profile editing, public payloads, and user search.
High-signal notification events (submission reviews, highlights, validator
graduation, missions, node versions, alerts) and admin campaigns with the
new Telegram channel checkbox now also deliver to linked accounts through
a message outbox. A cron-triggered drain endpoint sends at Telegram's rate
budget with retries, dedupe (re-broadcasts and campaign resends never
double-send), and automatic disabling of connections that blocked the bot.
The bot answers /rank, /points, /missions, /mute, /unmute, /unlink and
/help; every inbound and outbound message is logged, laying the groundwork
for a future per-user agent experience.
- backend/social_connections/models.py: TelegramConnection (extends SocialConnection; notifications_enabled, blocked_at) and TelegramMessage (message log + delivery outbox, partial unique (notification, connection) for idempotent enqueues); PendingOAuthState.consume() user_id now optional for token-only lookup
- backend/social_connections/telegram.py: sendMessage client (HTML mode, escape/truncate, 403 marks blocked, 429 retry_after, parse-error plain-text fallback)
- backend/social_connections/telegram_bot.py: link-token + disconnect endpoints, secret-validated webhook, handle_update command router (/start token consume, rank/points/missions/mute/unmute/unlink/help)
- backend/social_connections/urls.py: /api/webhooks/telegram/, /api/v1/users/telegram/link-token/ and /disconnect/
- backend/social_connections/management/commands/: set_telegram_webhook, run_telegram_polling (local dev getUpdates loop)
- backend/social_connections/{serializers,admin}.py + migrations/0005: owner-only TelegramConnectionSerializer (no public variant), admin registrations
- backend/notifications/telegram.py: render_notification_text, enqueue_personal/broadcast/campaign, cancel_pending_for_campaign, deliver_pending (skip_locked claims, 25 msg/s pacing, 3 attempts, per-run reclaim guard)
- backend/notifications/services.py: users_for_audience() (estimate_broadcast_reach refactored onto it); notify()/broadcast() enqueue when the event has the telegram channel
- backend/notifications/campaigns.py: send_campaign enqueues on telegram channel; recall_campaign returns (deleted, cancelled) and cancels pending outbox rows
- backend/notifications/views.py: cron-protected POST notifications/telegram/deliver/ action
- backend/notifications/registry.py: telegram channel on submission.*, contribution.highlighted, validator.graduated, mission/node_version/alert published, custom.announcement
- backend/notifications/admin.py: campaign channels checkboxes (portal forced on), recall messaging includes cancelled Telegram sends
- backend/notifications/management/commands/deliver_telegram_messages.py: CLI drain wrapper
- backend/users/serializers.py: telegram_connection SerializerMethodField (owner/staff only) + strip telegram_connection/telegram_handle for other viewers; telegram_handle removed from UserProfileUpdateSerializer
- backend/users/views.py: search no longer matches telegram_handle
- backend/tally/settings.py + backend/.env.example: TELEGRAM_BOT_USERNAME, TELEGRAM_WEBHOOK_SECRET
- .github/workflows/telegram-deliver.yml: 5-minute cron drain via X-Cron-Token
- frontend/src/components/TelegramLink.svelte: connect pill (deep link + poll) / connected pill with unlink
- frontend/src/components/profile/ProfileHeader.svelte: TelegramLink rendered only in the isOwnProfile branch
- frontend/src/lib/api.js: telegramAPI (getLinkToken, disconnect)
- frontend/src/routes/ProfileEdit.svelte: Telegram text input removed
- backend/social_connections/tests/test_telegram_{link,commands}.py, backend/users/tests/test_telegram_privacy.py, backend/notifications/tests.py: link/webhook/command/delivery/campaign coverage + mandatory privacy leak tests; users/tests/test_profile_update.py updated for the removed field
- backend/CLAUDE.md, frontend/CLAUDE.md: endpoints, env vars, channel + component docs
…count linking Recalling a broadcast now cancels its queued Telegram deliveries before the notification row is deleted; previously the pending outbox rows survived the recall (the FK nulls on delete) and the next delivery run would still send the recalled content. The delivery drain also refuses any queued row whose notification has disappeared, so every deletion path is covered. Telegram identity is now DB-enforced unique: one Telegram account can link to exactly one portal account. The /start handler's duplicate check alone was racy under concurrent linking, which could have left commands routing to an arbitrary account; the race loser now gets the same friendly "already linked" reply. ## Claude Implementation Notes - backend/notifications/telegram.py: cancel_pending_for_campaign generalized into cancel_pending_for_notifications (delete pending/sending rows BEFORE their notifications); deliver_pending fails rows with notification_id None as 'notification_recalled' instead of sending - backend/notifications/services.py: recall_broadcast() cancels pending Telegram rows before deleting the broadcast notification - backend/social_connections/models.py + migrations/0006: UniqueConstraint on TelegramConnection.platform_user_id - backend/social_connections/telegram_bot.py: /start wraps update_or_create in IntegrityError handling for the concurrent-link race (safe: no ATOMIC_REQUESTS, update_or_create uses a savepoint) - backend/notifications/tests.py: TelegramRecallBroadcastTests (recall cancels pending; drain never sends orphaned rows) - backend/social_connections/tests/test_telegram_link.py: TelegramIdentityUniquenessTests (constraint + race-loser reply)
Every Telegram notification now renders as a card: an event-specific emoji next to the bold title, an italic "Category · GenLayer Portal" byline, the body inside Telegram's accent-bar blockquote (collapsing behind an expandable quote when long), and the portal link as a tappable inline button instead of a bare link in the text. Each registered event has its own accent (accepted, rejected, highlighted, graduated, mission, node version, alert, announcement), with category fallbacks for future events. If Telegram rejects a button (invalid URL), the send degrades gracefully and retries without it, alongside the existing plain-text fallback for formatting errors. Interactive command replies get matching emoji headers. ## Claude Implementation Notes - backend/notifications/telegram.py: EVENT_EMOJI/CATEGORY_EMOJI maps, card layout in render_notification_text (emoji header, italic byline via get_category_display, blockquote body, expandable over 400 chars), notification_link_button() builds the inline keyboard from the notification at send time (outbox stores text only), drain passes reply_markup and select_relates notification - backend/social_connections/telegram.py: send_telegram_message accepts reply_markup; 400 handling now degrades in two steps (drop parse_mode on entity errors, drop reply_markup on button/url errors) - backend/social_connections/telegram_bot.py: emoji headers on /help, /rank, /points, /missions and the welcome reply - backend/notifications/tests.py: enqueue test asserts the card pieces and that the link moved out of the text; drain test asserts the button payload - backend/social_connections/tests/test_telegram_commands.py: reply_markup passthrough + rejected-button fallback tests
Address the second external review round. Deployment: the three Telegram settings now ship to App Runner via SSM secrets in both deploy scripts, and a dev delivery cron workflow joins the prod one, so the deployed bot can actually link, authenticate webhooks, and send. The bot token is redacted from logged network errors so it can no longer leak into CloudWatch, and the local polling command refuses to hijack a token with a registered webhook (that is production) unless explicitly forced. Delivery-state correctness: receiving any Telegram message clears a stale blocked flag so users who unblock the bot (and /unmute) recover; a recall racing an in-flight send no longer aborts the delivery run; a worker crash on a row's final attempt no longer strands it in sending forever; edited notifications refresh their still-queued Telegram text instead of sending stale copy; and escaping-heavy bodies are budgeted after escaping so long cards can no longer degrade into visible raw markup. Broadcast recall feedback in the admin now says that already-delivered Telegram messages remain, the signup flow no longer accepts the removed public telegram handle, and the mobile linking flow recovers cleanly when returning via the back button. ## Claude Implementation Notes - backend/deploy-apprunner-dev.sh + deploy-apprunner.sh: TELEGRAM_BOT_TOKEN/TELEGRAM_BOT_USERNAME/TELEGRAM_WEBHOOK_SECRET added to all four RuntimeEnvironmentSecrets blocks (lowercase SSM names per convention; params must exist before deploying) - backend/social_connections/telegram.py: redact_token() strips the token from logged requests exceptions (URL embeds it) - backend/social_connections/management/commands/run_telegram_polling.py: getWebhookInfo guard + --delete-webhook flag; redacted error output - backend/social_connections/telegram_bot.py: any inbound message clears blocked_at (unblock recovery); webhook docstring no longer overclaims replay idempotency (update_id dedupe deliberately skipped: duplicate replies on a lost 200 are cosmetic and state-safe) - backend/notifications/telegram.py: _finish() uses queryset update (recall-during-send race), drain sweeps stale sending rows with exhausted attempts to failed, _refresh_pending_text() syncs queued rows after dedupe/campaign/broadcast refreshes, render budgets the ESCAPED body and never cuts inside an entity - backend/notifications/admin_mixins.py: broadcast recall message notes Telegram limits for telegram-channel events - backend/ethereum_auth/views.py + email_verification.py: telegram_handle dropped from pending-signup profile fields and user creation - frontend/src/components/TelegramLink.svelte: isLinking reset before same-tab navigation (popup-blocked path), pageshow/bfcache return does a one-shot link check, JSDoc types fix the svelte-check diagnostics - .github/workflows/telegram-deliver-dev.yml: dev drain cron mirroring sync-validator-grafana-dev.yml (DEV_API_BASE_URL) - Tests: unblock recovery, pending-text refresh, escape-safe truncation, stranded-row sweep, recall-during-send, signup field drop
The Telegram migrations were never applied in any deployed environment, so instead of renumbering the pair around dev's new 0005, both schema changes (models + the unique Telegram-id constraint) collapse into a single fresh migration on top of dev's graph. ## Claude Implementation Notes - backend/social_connections/migrations/0006_telegramconnection_telegrammessage.py: single regenerated migration (TelegramConnection + TelegramMessage with all indexes and constraints, incl. uniq_telegram_platform_user_id), depends on dev's 0005_pendingoauthstate_session_key; the former 0006/0007 pair is gone
Pending-state cleanup is now platform-scoped, so an unrelated OAuth initiation (10-minute lifetime) can no longer sweep Telegram deep-link tokens that are advertised to live 15 minutes. Deactivated portal accounts consistently lose Telegram access: their connection resolves as unlinked for commands, their link tokens are rejected, and both enqueue and the delivery drain skip them. The drain also refetches each connection immediately before sending instead of using the batch preload, so a relink mid-batch delivers to the live chat id and a 403 marks current state rather than a stale instance. The setup and polling commands redact the bot token from transport-error output, the polling loop fails fast on an invalid token and backs off on other failures instead of busy-looping, and the dev delivery schedule starts at :01 so the five-minute cadence has no ten-minute hourly gap. ## Claude Implementation Notes - backend/social_connections/models.py: PendingOAuthState.cleanup_old(minutes, platform=None) filters by platform - backend/social_connections/oauth_service.py + telegram_bot.py: cleanup calls pass their platform - backend/social_connections/telegram_bot.py: connection lookup requires user__is_active; /start rejects tokens for deactivated users - backend/notifications/telegram.py: eligible_connections + enqueue_personal require active users; deliver_pending refetches the connection (select_related user) per message and checks is_active; claim no longer preloads connections - backend/social_connections/management/commands/set_telegram_webhook.py + run_telegram_polling.py: requests failures raise CommandError with redact_token(); polling exits on 401/403/404 and sleeps 5s on other failures - .github/workflows/telegram-deliver-dev.yml: cron 1-59/5 (was 6-59/5, which gapped :56 to :06) - Tests: platform-scoped cleanup spares Telegram tokens; deactivated-user start/command/enqueue/drain gating
22c7215 to
be5f844
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
backend/notifications/admin.py (1)
241-267: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTelegram "already-sent" caveat shown even for portal-only campaigns.
The message unconditionally appends "Already-sent Telegram messages cannot be recalled." whenever
deleted or cancelledis truthy, regardless of whether the campaign'schannelsever included'telegram'. For a portal-only campaign this is misleading — it implies Telegram delivery happened when it never did.admin_mixins.py's_recall_broadcast(same PR) already gates the equivalent caveat on the event's channels; this should follow the same pattern.🐛 Proposed fix
if deleted or cancelled: message = f'Recalled {deleted} delivered portal notification(s).' if cancelled: message += f' Cancelled {cancelled} queued Telegram message(s).' - message += ( - ' Already-sent Telegram messages cannot be recalled.' - ' The campaign record was kept.' - ) + if 'telegram' in (campaign.channels or []): + message += ' Already-sent Telegram messages cannot be recalled.' + message += ' The campaign record was kept.' self.message_user(request, message, level=messages.SUCCESS)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/notifications/admin.py` around lines 241 - 267, Update _recall_campaign so the “Already-sent Telegram messages cannot be recalled” caveat is appended only when the campaign’s channels include 'telegram', matching _recall_broadcast’s channel-gating behavior. Keep the recalled portal and cancelled Telegram counts and existing messages unchanged..github/workflows/telegram-deliver-dev.yml (1)
1-34: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winHarden the workflow: least-privilege permissions, avoid direct secret interpolation in
run:, add concurrency guard.Three low-cost hardening items here (all flagged by zizmor):
- No
permissions:block → defaults to broader-than-neededGITHUB_TOKENscope for a job that only calls an external endpoint.${{ secrets.CRON_SYNC_TOKEN }}/${{ secrets.DEV_API_BASE_URL }}are expanded directly into the shell script rather than passed viaenv:; this is the standard GitHub Actions injection anti-pattern.- No
concurrency:group, so overlapping 5-minute runs aren't prevented (low risk given the idempotent server-side claim, but still avoidable waste).🔒 Proposed fix
name: Deliver Telegram Notifications (dev) +permissions: {} + on: schedule: # GitHub Actions schedules are best-effort; avoid crowded :00/:15/:30/:45 slots. # Start at :01 so the last slot (:56) rolls to :01 with no hourly gap. - cron: '1-59/5 * * * *' workflow_dispatch: jobs: deliver: runs-on: ubuntu-latest environment: cron-job + concurrency: + group: telegram-deliver-dev + cancel-in-progress: false steps: - name: Drain Telegram outbox (dev) + env: + CRON_SYNC_TOKEN: ${{ secrets.CRON_SYNC_TOKEN }} + API_BASE_URL: ${{ secrets.DEV_API_BASE_URL }} run: | response=$(curl -s -w "\n%{http_code}" -X POST \ -H "Content-Type: application/json" \ - -H "X-Cron-Token: ${{ secrets.CRON_SYNC_TOKEN }}" \ - "${{ secrets.DEV_API_BASE_URL }}/api/v1/notifications/telegram/deliver/") + -H "X-Cron-Token: $CRON_SYNC_TOKEN" \ + "$API_BASE_URL/api/v1/notifications/telegram/deliver/")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/telegram-deliver-dev.yml around lines 1 - 34, Harden the Telegram delivery workflow by adding a top-level permissions block with contents: read, pass CRON_SYNC_TOKEN and DEV_API_BASE_URL through the step’s env mapping instead of interpolating secrets directly in run, and add a concurrency group for this workflow with cancellation disabled to prevent overlapping scheduled runs.Sources: Path instructions, Linters/SAST tools
.github/workflows/telegram-deliver.yml (1)
1-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winHarden the workflow: least-privilege permissions, avoid direct secret interpolation in
run:, add concurrency guard.Same three items as the dev workflow (
.github/workflows/telegram-deliver-dev.yml): missingpermissions:block, secrets interpolated directly into the shell script instead of viaenv:, and noconcurrency:group.🔒 Proposed fix
name: Deliver Telegram Notifications +permissions: {} + on: schedule: # GitHub Actions schedules are best-effort; avoid crowded :00/:15/:30/:45 slots. - cron: '4-59/5 * * * *' workflow_dispatch: jobs: deliver: runs-on: ubuntu-latest environment: cron-job + concurrency: + group: telegram-deliver-prod + cancel-in-progress: false steps: - name: Drain Telegram outbox + env: + CRON_SYNC_TOKEN: ${{ secrets.CRON_SYNC_TOKEN }} + API_BASE_URL: ${{ secrets.API_BASE_URL }} run: | response=$(curl -s -w "\n%{http_code}" -X POST \ -H "Content-Type: application/json" \ - -H "X-Cron-Token: ${{ secrets.CRON_SYNC_TOKEN }}" \ - "${{ secrets.API_BASE_URL }}/api/v1/notifications/telegram/deliver/") + -H "X-Cron-Token: $CRON_SYNC_TOKEN" \ + "$API_BASE_URL/api/v1/notifications/telegram/deliver/")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/telegram-deliver.yml around lines 1 - 33, Harden the workflow by adding a least-privilege permissions block and a concurrency group matching the Telegram delivery workflow’s needs, with an appropriate cancellation policy. Update the “Drain Telegram outbox” step to consume CRON_SYNC_TOKEN and API_BASE_URL through step-level env variables instead of interpolating secrets directly in the run script, while preserving the existing request and status handling.Sources: Path instructions, Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/notifications/telegram.py`:
- Around line 356-457: The deliver_pending loop can exceed App Runner’s
120-second request limit when processing the default batch. Add a wall-clock
deadline inside deliver_pending, checked before claiming additional batches and
while processing messages, using a timeout comfortably below 120 seconds; stop
processing and leave unprocessed or remaining claimed rows safely pending so the
request exits without exceeding the platform limit.
In `@backend/social_connections/models.py`:
- Around line 132-216: Update TelegramMessage to inherit from
utils.models.BaseModel rather than models.Model, following the repository model
convention. Remove its hand-rolled created_at and updated_at fields so the
inherited BaseModel fields are used, and add the appropriate
utils.models.BaseModel reference/import while preserving the remaining model
fields and behavior.
In `@backend/social_connections/telegram_bot.py`:
- Around line 96-124: The telegram_webhook handler must stop executing
handle_update synchronously on the request thread. Offload update processing,
including interactive _reply/send_telegram_message calls, to the project’s
existing background task or outbox mechanism so the webhook returns promptly
while preserving retry, deduplication, and rate-limit behavior; keep
authentication and the immediate successful response unchanged.
In `@CHANGELOG.md`:
- Line 7: Update the Telegram account privacy wording in the changelog entry to
say it is not visible to other users, while accurately allowing visibility to
the profile owner and staff; preserve the surrounding notification and
bot-command details.
In `@frontend/src/components/TelegramLink.svelte`:
- Around line 104-120: Update disconnectTelegram so telegramAPI.disconnect()
success is handled separately from the follow-up getCurrentUser() refresh:
preserve the unlink success state and invoke the appropriate UI update even if
getCurrentUser() fails, while reporting refresh failure separately rather than
claiming the unlink failed. Keep the isDisconnecting cleanup and existing
confirmation flow unchanged.
---
Outside diff comments:
In @.github/workflows/telegram-deliver-dev.yml:
- Around line 1-34: Harden the Telegram delivery workflow by adding a top-level
permissions block with contents: read, pass CRON_SYNC_TOKEN and DEV_API_BASE_URL
through the step’s env mapping instead of interpolating secrets directly in run,
and add a concurrency group for this workflow with cancellation disabled to
prevent overlapping scheduled runs.
In @.github/workflows/telegram-deliver.yml:
- Around line 1-33: Harden the workflow by adding a least-privilege permissions
block and a concurrency group matching the Telegram delivery workflow’s needs,
with an appropriate cancellation policy. Update the “Drain Telegram outbox” step
to consume CRON_SYNC_TOKEN and API_BASE_URL through step-level env variables
instead of interpolating secrets directly in the run script, while preserving
the existing request and status handling.
In `@backend/notifications/admin.py`:
- Around line 241-267: Update _recall_campaign so the “Already-sent Telegram
messages cannot be recalled” caveat is appended only when the campaign’s
channels include 'telegram', matching _recall_broadcast’s channel-gating
behavior. Keep the recalled portal and cancelled Telegram counts and existing
messages unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: bf497504-c39f-4675-bb83-ecf64d954d48
📒 Files selected for processing (40)
.github/workflows/telegram-deliver-dev.yml.github/workflows/telegram-deliver.ymlCHANGELOG.mdbackend/.env.examplebackend/CLAUDE.mdbackend/deploy-apprunner-dev.shbackend/deploy-apprunner.shbackend/ethereum_auth/email_verification.pybackend/ethereum_auth/views.pybackend/notifications/admin.pybackend/notifications/admin_mixins.pybackend/notifications/campaigns.pybackend/notifications/management/commands/deliver_telegram_messages.pybackend/notifications/registry.pybackend/notifications/services.pybackend/notifications/telegram.pybackend/notifications/tests.pybackend/notifications/views.pybackend/social_connections/admin.pybackend/social_connections/management/commands/run_telegram_polling.pybackend/social_connections/management/commands/set_telegram_webhook.pybackend/social_connections/migrations/0006_telegramconnection_telegrammessage.pybackend/social_connections/models.pybackend/social_connections/oauth_service.pybackend/social_connections/serializers.pybackend/social_connections/telegram.pybackend/social_connections/telegram_bot.pybackend/social_connections/tests/test_telegram_commands.pybackend/social_connections/tests/test_telegram_link.pybackend/social_connections/urls.pybackend/tally/settings.pybackend/users/serializers.pybackend/users/tests/test_profile_update.pybackend/users/tests/test_telegram_privacy.pybackend/users/views.pyfrontend/CLAUDE.mdfrontend/src/components/TelegramLink.sveltefrontend/src/components/profile/ProfileHeader.sveltefrontend/src/lib/api.jsfrontend/src/routes/ProfileEdit.svelte
💤 Files with no reviewable changes (2)
- backend/ethereum_auth/email_verification.py
- frontend/src/routes/ProfileEdit.svelte
| @csrf_exempt | ||
| def telegram_webhook(request): | ||
| """Receive Telegram updates. | ||
|
|
||
| Auth: Telegram echoes TELEGRAM_WEBHOOK_SECRET in a header on every call | ||
| (set via setWebhook). An unset secret rejects everything. | ||
|
|
||
| Always returns 200 after auth, even on handler errors: Telegram retries | ||
| non-200 responses, and a poison update must not loop forever. Updates are | ||
| NOT deduplicated by update_id: if our 200 is lost in transit, Telegram's | ||
| retry produces a duplicate reply (and, for /start, an "expired token" | ||
| follow-up). That is cosmetic; state stays correct because the token | ||
| consume is atomic and mute/unlink are idempotent. | ||
| """ | ||
| if request.method != 'POST': | ||
| return HttpResponseNotAllowed(['POST']) | ||
|
|
||
| provided = request.headers.get('X-Telegram-Bot-Api-Secret-Token', '') | ||
| if not settings.TELEGRAM_WEBHOOK_SECRET or not hmac.compare_digest( | ||
| provided, settings.TELEGRAM_WEBHOOK_SECRET | ||
| ): | ||
| return HttpResponseForbidden('invalid webhook secret') | ||
|
|
||
| try: | ||
| update = json.loads(request.body) | ||
| handle_update(update) | ||
| except Exception: | ||
| logger.exception("Telegram webhook update handling failed") | ||
| return JsonResponse({'ok': True}) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Webhook blocks the request thread on outbound Telegram calls.
telegram_webhook calls handle_update synchronously (Line 121), and every command handler ultimately calls _reply (Lines 190-201) → send_telegram_message, which can issue up to 3 sequential blocking requests.post calls at 10s timeout each (telegram.py Lines 86-129) — up to ~30s held on the request thread per webhook delivery. Under Telegram API slowness/rate-limiting or a burst of commands, this can exhaust worker capacity and delay/queue other incoming requests (webhook and otherwise) sharing the same worker pool.
Given the PR already implements a deduplicated, retryable outbox with rate-limit pacing for notification delivery, consider whether interactive command replies should also be offloaded (e.g., a background task/thread) so the webhook can return quickly and outbound sends happen out-of-band, while preserving fast perceived reply latency for the user.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/social_connections/telegram_bot.py` around lines 96 - 124, The
telegram_webhook handler must stop executing handle_update synchronously on the
request thread. Offload update processing, including interactive
_reply/send_telegram_message calls, to the project’s existing background task or
outbox mechanism so the webhook returns promptly while preserving retry,
deduplication, and rate-limit behavior; keep authentication and the immediate
successful response unchanged.
…ntion
The Telegram delivery run now stops at a 90-second wall-clock budget,
checked before each batch claim and before each send, returning unfinished
rows to pending for the next cron tick. The drain runs inside an HTTP
request and App Runner kills requests at 120 seconds; a run slowed down by
Telegram timeouts could previously blow through that and get killed
mid-batch. The rate-limit and budget stop paths now share one unclaim
helper.
Unlinking Telegram in the profile header no longer reports failure when
the unlink succeeded but the follow-up profile refresh failed; the pill
clears either way. TelegramMessage now inherits the repo's BaseModel
instead of hand-rolling identical timestamp fields (schema-unchanged, no
migration).
Not changed: interactive webhook replies stay synchronous. A command
reply is one bounded API call, Telegram serializes webhook deliveries per
bot, and pushing replies through the cron-drained outbox would turn
sub-second answers into five-minute ones; the outbox remains the path for
notification fan-out only.
## Claude Implementation Notes
- backend/notifications/telegram.py: MAX_RUN_SECONDS=90; deliver_pending(limit, max_run_seconds) checks the deadline in the while condition and per message; _unclaim() shared by the 429 and deadline stops (rate_limited flag renamed stopped)
- backend/social_connections/models.py: TelegramMessage(BaseModel); dropped duplicate created_at/updated_at (makemigrations --check clean)
- frontend/src/components/TelegramLink.svelte: disconnect separates unlink failure from refresh failure; on refresh failure merges {telegram_connection: null} so the pill still clears
- backend/notifications/tests.py: TelegramDeliveryDeadlineTests (budget stop leaves rows pending with attempts intact)
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (2)
CHANGELOG.md (1)
7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse consistent Telegram privacy wording.
The documentation says “visible only to them”/“owner-only,” while the documented behavior permits the owner and staff to view the connection.
CHANGELOG.md#L7-L7: state that the connection is not visible to other users and is available to the owner and staff.backend/CLAUDE.md#L326-L326: replace “owner-only” with “owner and staff” wording.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CHANGELOG.md` at line 7, Update the Telegram privacy wording in CHANGELOG.md line 7 to state that the connection is not visible to other users and is available to the owner and staff; update backend/CLAUDE.md line 326 by replacing “owner-only” with equivalent “owner and staff” wording.backend/notifications/telegram.py (1)
356-457: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winStill unbounded vs. App Runner's 120s request timeout.
This mirrors an unresolved prior review:
telegram_deliver(views.py) runsdeliver_pendingsynchronously withDEFAULT_RUN_LIMIT=400, but the loop has no wall-clock deadline. A large backlog can still run past App Runner's 120s HTTP timeout, get killed mid-batch, and leave rowssendinguntil the 10-minute stale sweep recovers them.⏱️ Proposed deadline guard
+MAX_RUN_SECONDS = 90 # comfortably under App Runner's 120s request timeout + def deliver_pending(limit=DEFAULT_RUN_LIMIT): ... + deadline = time.monotonic() + MAX_RUN_SECONDS sent = failed = processed = 0 rate_limited = False seen_pks = set() - while processed < limit and not rate_limited: + while processed < limit and not rate_limited and time.monotonic() < deadline:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/notifications/telegram.py` around lines 356 - 457, Update deliver_pending to enforce a wall-clock deadline shorter than App Runner’s 120-second request timeout, using a monotonic start time and checking it before claiming additional work and between message deliveries. Stop cleanly when the deadline is reached, preserving claimed-row recovery semantics and returning the existing sent, failed, and remaining counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/telegram-deliver-dev.yml:
- Line 1: Add top-level permissions and concurrency hardening blocks to both
Telegram delivery workflows: grant only contents: read, and define non-canceling
concurrency groups telegram-deliver-dev for the development workflow and
telegram-deliver-prod for the production workflow.
- Around line 3-13: Add an explicit least-privilege permissions block to the
workflow, granting only the access required by the deliver job, and define a
concurrency group for this workflow with cancellation disabled so overlapping
scheduled runs queue rather than execute concurrently. Place these settings at
the workflow level alongside the existing on and jobs configuration.
- Line 13: Separate the cron delivery secret scopes by updating the environment
declarations in .github/workflows/telegram-deliver-dev.yml (line 13) and
.github/workflows/telegram-deliver.yml (line 12) to distinct environment names
such as cron-job-dev and cron-job-prod, while keeping the existing
CRON_SYNC_TOKEN references intact.
In @.github/workflows/telegram-deliver.yml:
- Around line 3-12: Add a top-level permissions block to the workflow with the
minimum required access, and add a concurrency group for the deliver
job/workflow so overlapping scheduled or manually dispatched runs are prevented.
Use the corresponding hardened configuration from telegram-deliver-dev.yml as
the reference, while preserving the existing schedule and job behavior.
In `@backend/notifications/telegram.py`:
- Around line 277-296: The deliver_pending flow must revalidate each outbox row
immediately before calling send_telegram_message, because a stale SENDING object
may have been deleted or recalled. Refresh or query the TelegramMessage record
by its primary key, confirm it still exists with an eligible status, and skip
sending when it is missing or no longer eligible; preserve the existing send
behavior for valid rows.
In `@backend/social_connections/telegram_bot.py`:
- Around line 152-157: Add an explicit retention mechanism for TelegramMessage
records, covering the inbound persistence in the TelegramMessage.objects.create
flow and the corresponding outbound path. Implement a purge job or configured
retention window that deletes messages older than the allowed period, while
preserving newer conversation records.
In `@backend/social_connections/telegram.py`:
- Around line 60-129: Remove the unreachable trailing return from
send_telegram_message; every loop iteration already exits through return or
continue, and the existing retry and failure behavior should remain unchanged.
In `@backend/social_connections/tests/test_telegram_commands.py`:
- Line 211: Update the unpacking assignments around the send_telegram_message
calls in the test to mark unused return values with underscore-prefixed names:
use _description at the first call and _retry_after at the later call, while
preserving the values that the assertions or test logic use.
---
Duplicate comments:
In `@backend/notifications/telegram.py`:
- Around line 356-457: Update deliver_pending to enforce a wall-clock deadline
shorter than App Runner’s 120-second request timeout, using a monotonic start
time and checking it before claiming additional work and between message
deliveries. Stop cleanly when the deadline is reached, preserving claimed-row
recovery semantics and returning the existing sent, failed, and remaining
counts.
In `@CHANGELOG.md`:
- Line 7: Update the Telegram privacy wording in CHANGELOG.md line 7 to state
that the connection is not visible to other users and is available to the owner
and staff; update backend/CLAUDE.md line 326 by replacing “owner-only” with
equivalent “owner and staff” wording.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 46743a96-3e58-4cce-9b8d-66230cb6119f
📒 Files selected for processing (40)
.github/workflows/telegram-deliver-dev.yml.github/workflows/telegram-deliver.ymlCHANGELOG.mdbackend/.env.examplebackend/CLAUDE.mdbackend/deploy-apprunner-dev.shbackend/deploy-apprunner.shbackend/ethereum_auth/email_verification.pybackend/ethereum_auth/views.pybackend/notifications/admin.pybackend/notifications/admin_mixins.pybackend/notifications/campaigns.pybackend/notifications/management/commands/deliver_telegram_messages.pybackend/notifications/registry.pybackend/notifications/services.pybackend/notifications/telegram.pybackend/notifications/tests.pybackend/notifications/views.pybackend/social_connections/admin.pybackend/social_connections/management/commands/run_telegram_polling.pybackend/social_connections/management/commands/set_telegram_webhook.pybackend/social_connections/migrations/0007_telegramconnection_telegrammessage.pybackend/social_connections/models.pybackend/social_connections/oauth_service.pybackend/social_connections/serializers.pybackend/social_connections/telegram.pybackend/social_connections/telegram_bot.pybackend/social_connections/tests/test_telegram_commands.pybackend/social_connections/tests/test_telegram_link.pybackend/social_connections/urls.pybackend/tally/settings.pybackend/users/serializers.pybackend/users/tests/test_profile_update.pybackend/users/tests/test_telegram_privacy.pybackend/users/views.pyfrontend/CLAUDE.mdfrontend/src/components/TelegramLink.sveltefrontend/src/components/profile/ProfileHeader.sveltefrontend/src/lib/api.jsfrontend/src/routes/ProfileEdit.svelte
💤 Files with no reviewable changes (2)
- frontend/src/routes/ProfileEdit.svelte
- backend/ethereum_auth/email_verification.py
| @@ -0,0 +1,33 @@ | |||
| name: Deliver Telegram Notifications (dev) | |||
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
Both Telegram delivery workflows lack explicit permissions: and concurrency: blocks. Root cause: neither workflow was scaffolded with a hardening block, so both default to broad token permissions and allow overlapping scheduled runs.
.github/workflows/telegram-deliver-dev.yml#L3-13: add a top-levelpermissions: contents: readand aconcurrency: {group: telegram-deliver-dev, cancel-in-progress: false}block..github/workflows/telegram-deliver.yml#L3-12: add a top-levelpermissions: contents: readand aconcurrency: {group: telegram-deliver-prod, cancel-in-progress: false}block.
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 1-34: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block
(excessive-permissions)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/telegram-deliver-dev.yml at line 1, Add top-level
permissions and concurrency hardening blocks to both Telegram delivery
workflows: grant only contents: read, and define non-canceling concurrency
groups telegram-deliver-dev for the development workflow and
telegram-deliver-prod for the production workflow.
Source: Linters/SAST tools
| jobs: | ||
| deliver: | ||
| runs-on: ubuntu-latest | ||
| environment: cron-job |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd 'deploy-apprunner' backend --exec cat {}
fd '.env.example' backend --exec grep -n -i 'CRON_SYNC_TOKEN' {}Repository: genlayer-foundation/points
Length of output: 37287
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for f in .github/workflows/telegram-deliver-dev.yml .github/workflows/telegram-deliver.yml; do
echo "===== $f ====="
cat -n "$f" | sed -n '1,200p'
echo
done
echo "===== CRON_SYNC_TOKEN references ====="
rg -n "CRON_SYNC_TOKEN|environment:" .github/workflows -SRepository: genlayer-foundation/points
Length of output: 4579
Split cron delivery into separate environments
Both workflows use environment: cron-job, so secrets.CRON_SYNC_TOKEN comes from the same secret scope in dev and prod. Use separate environments (for example cron-job-dev / cron-job-prod) or distinct secret names if the tokens should differ.
📍 Affects 2 files
.github/workflows/telegram-deliver-dev.yml#L13-L13(this comment).github/workflows/telegram-deliver.yml#L12-L12
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/telegram-deliver-dev.yml at line 13, Separate the cron
delivery secret scopes by updating the environment declarations in
.github/workflows/telegram-deliver-dev.yml (line 13) and
.github/workflows/telegram-deliver.yml (line 12) to distinct environment names
such as cron-job-dev and cron-job-prod, while keeping the existing
CRON_SYNC_TOKEN references intact.
| TelegramMessage.objects.create( | ||
| direction=TelegramMessage.DIRECTION_IN, | ||
| connection=connection, | ||
| chat_id=chat_id, | ||
| text=text, | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "TelegramMessage" backend --type=py | rg -i "delete|cleanup|purge|retention|expire|older_than"
fd . backend/social_connections/management/commands backend/notifications/management/commands 2>/dev/nullRepository: genlayer-foundation/points
Length of output: 755
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== TelegramMessage references ==\n'
rg -n "TelegramMessage" backend --type=py
printf '\n== Model definition ==\n'
fd -a "telegram*.py" backend/social_connections
fd -a "models.py" backend/social_connections backend/notificationsRepository: genlayer-foundation/points
Length of output: 13695
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== TelegramMessage references ==\n'
rg -n "TelegramMessage" backend --type=py
printf '\n== social_connections tree ==\n'
fd -a . backend/social_connections
printf '\n== notification management commands ==\n'
fd -a . backend/notifications/management/commandsRepository: genlayer-foundation/points
Length of output: 16970
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== TelegramMessage model ==\n'
sed -n '120,230p' backend/social_connections/models.py
printf '\n== run_telegram_polling ==\n'
sed -n '1,220p' backend/social_connections/management/commands/run_telegram_polling.py
printf '\n== deliver_telegram_messages ==\n'
sed -n '1,260p' backend/notifications/management/commands/deliver_telegram_messages.py
printf '\n== cleanup/retention search ==\n'
rg -n "TelegramMessage|telegram" backend -g '!backend/**/tests/**' | rg -i "cleanup|purge|delete|retention|expire|ttl|prune|vacuum|cron|schedule|celery|beat|task"Repository: genlayer-foundation/points
Length of output: 13283
Add a retention policy for TelegramMessage. The model explicitly treats inbound rows as a permanent conversation log, and both inbound/outbound paths persist full message text, so arbitrary PII can live indefinitely. Add a purge job or explicit retention window for this table.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/social_connections/telegram_bot.py` around lines 152 - 157, Add an
explicit retention mechanism for TelegramMessage records, covering the inbound
persistence in the TelegramMessage.objects.create flow and the corresponding
outbound path. Implement a purge job or configured retention window that deletes
messages older than the allowed period, while preserving newer conversation
records.
…ening Each outbox row is now re-checked against the database immediately before its send, not just at claim time: a recall landing while a batch is being processed previously left later rows in the batch deliverable from their stale in-memory snapshots for up to the full batch duration. A deleted row is skipped silently and a nulled notification link still fails the row as recalled, shrinking the recall race to the single in-flight request that is inherently unstoppable. Both Telegram delivery workflows get a least-privilege token (contents: read) and a non-cancelling concurrency group so a slow run queues the next scheduled tick instead of overlapping it. Two unused unpacked variables in tests are underscore-prefixed for the linter. Not changed: the shared cron-job environment (all of the repo's cron workflows use it; splitting environments is org-level infrastructure, not this PR), the permanent TelegramMessage log (a deliberate product decision mirroring the validator observation log, revisit if retention policy requires), and the defensive trailing return in send_telegram_message. ## Claude Implementation Notes - backend/notifications/telegram.py: deliver_pending re-reads (status, notification_id) by pk before each send; row gone or no longer 'sending' -> skip, notification FK nulled -> failed 'notification_recalled' (replaces the batch-snapshot notification_id check) - .github/workflows/telegram-deliver.yml + telegram-deliver-dev.yml: permissions contents:read + concurrency groups (cancel-in-progress false) - backend/social_connections/tests/test_telegram_commands.py: RUF059 underscore prefixes - backend/notifications/tests.py: TelegramRecallMidBatchTests (second row of a recalled batch never reaches Telegram)
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/notifications/tests.py`:
- Line 1833: Update the test around deliver_pending to mock time.monotonic with
advancing side effects so the while loop executes and reaches the mid-batch
deadline path. Ensure the test exercises _claim_batch followed by the in-flight
_unclaim and stopped = True behavior instead of using max_run_seconds=0, which
skips the loop.
- Line 1866: Update the send_and_recall_everything mock function signature so
intentionally unused arguments are prefixed with underscores, satisfying Ruff
ARG001 while preserving the existing chat_id, text, and kwargs behavior.
In `@frontend/src/components/TelegramLink.svelte`:
- Around line 114-127: Keep isDisconnecting true through the entire unlink flow,
including the getCurrentUser refresh, by moving the reset from the current
finally block to after the refresh handling completes. In the getCurrentUser
catch path, do not call onLinked with a partial object; leave the existing
parent state unchanged while preserving the successful refresh behavior and
success notification.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 09f467f1-98d5-4897-82a2-2bafc56c616c
📒 Files selected for processing (7)
.github/workflows/telegram-deliver-dev.yml.github/workflows/telegram-deliver.ymlbackend/notifications/telegram.pybackend/notifications/tests.pybackend/social_connections/models.pybackend/social_connections/tests/test_telegram_commands.pyfrontend/src/components/TelegramLink.svelte
…ner honest
The delivery deadline now has real in-loop coverage: a new test advances a
mocked monotonic clock so the budget trips after a batch is claimed,
proving the claimed rows are unclaimed (status and attempts restored) and
the run stops. The existing zero-budget test only covered the pre-claim
check. The unlink button now stays disabled through the follow-up profile
refresh instead of re-enabling the moment the disconnect call returns.
Kept as-is: the refresh-failure fallback still passes
{telegram_connection: null} to the parent. The review claimed this wipes
the profile state, but the parent handler merges into the existing
participant object, so it clears exactly the one key and prevents a stale
connected pill; a comment now documents that contract at the call site.
## Claude Implementation Notes
- backend/notifications/tests.py: TelegramDeliveryMidBatchDeadlineTests (advancing time.monotonic mock hits the in-loop _unclaim + stop branch); underscore-prefixed unused mock args (ARG001)
- frontend/src/components/TelegramLink.svelte: isDisconnecting reset moved after the refresh (finally on the refresh block; explicit reset on the disconnect-failure return); merge-contract comment on the fallback
…elink races Marking a connection blocked after a Telegram 403 now uses a conditional queryset update keyed on the row's pk AND the chat id that received the 403, instead of saving the in-memory instance. Saving the stale instance could raise on a row deleted by a concurrent disconnect (aborting the delivery run mid-batch, since send_telegram_message promises never to raise) and, because the connection row is per-user, a relink completing while the send was in flight would have disabled the freshly linked Telegram account based on the old chat's 403. Both cases are now no-ops, mirroring the queryset-update pattern already used for finishing outbox rows. ## Claude Implementation Notes - backend/social_connections/telegram.py: _mark_blocked(connection, chat_id) filters on pk + platform_user_id and updates via queryset (explicit updated_at); 403 branch passes the messaged chat_id - backend/social_connections/tests/test_telegram_commands.py: MarkBlockedRaceTests (403 after disconnect is a silent no-op; 403 for the old chat after a relink leaves the new account enabled)
There was a problem hiding this comment.
♻️ Duplicate comments (1)
backend/social_connections/telegram.py (1)
145-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTrailing
returnis unreachable.The three-attempt retry loop is well-designed. Every branch inside the loop body ends in either
returnorcontinue, andcontinuecan only fire twice (once per poppable key). That means the 3rd iteration is always exited via an explicitreturninside the loop, so this return can never execute.♻️ Optional cleanup
- return False, None, 'unreachable' + # Unreachable: every loop-body branch above returns or continues, and the + # two poppable degrade-keys bound `continue` to at most 2 uses within + # range(3). Kept only as a defensive fallback for future edits. + return False, None, 'unreachable' # pragma: no cover🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/social_connections/telegram.py` at line 145, Remove the unreachable trailing return after the retry loop in the relevant Telegram connection function. Preserve the loop’s existing return and continue branches and its three-attempt retry behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@backend/social_connections/telegram.py`:
- Line 145: Remove the unreachable trailing return after the retry loop in the
relevant Telegram connection function. Preserve the loop’s existing return and
continue branches and its three-attempt retry behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4b5702b5-1beb-472b-b91c-0e1e99223a2c
📒 Files selected for processing (2)
backend/social_connections/telegram.pybackend/social_connections/tests/test_telegram_commands.py
…etry loop The send retry loop's trailing return could never execute: each of the two degrade retries pops its payload key exactly once, so at most two continues occur and every other branch returns. Restructured from a counted loop to while True, which removes the dead line without weakening the guarantee: termination is still bounded at three requests by the same two one-shot pops, and the retry/degradation behavior is byte-for-byte identical. ## Claude Implementation Notes - backend/social_connections/telegram.py: send_telegram_message loop is now `while True` with a termination note; the unreachable `return False, None, 'unreachable'` is gone
Contributors can now link a Telegram account from their profile header via a one-time deep link to the portal bot; the connection is private (owner-only in the API and UI) and replaces the public Telegram handle field, which is removed from profile editing, public payloads, search, and signup. High-signal notification events and admin campaigns (new Telegram channel checkbox) deliver as styled cards with inline link buttons through a message outbox, drained by cron-protected endpoints with dedupe, retries, rate-limit pacing, and cancellation of queued sends on recall. The bot answers /rank, /points, /missions, /mute, /unmute, /unlink, and /help, and every inbound and outbound message is logged. Deployment is wired end to end: Telegram secrets in both App Runner scripts, prod and dev delivery workflows, and management commands for webhook registration and local polling. Post-deploy: create the telegram_bot_token / telegram_bot_username / telegram_webhook_secret SSM parameters (App Runner will not start without them), then run
python manage.py set_telegram_webhook.Summary by CodeRabbit