Skip to content

Add Telegram bot with private linking, notification delivery, and commands#930

Open
JoaquinBN wants to merge 12 commits into
devfrom
JoaquinBN/telegram-bot-features-discussion
Open

Add Telegram bot with private linking, notification delivery, and commands#930
JoaquinBN wants to merge 12 commits into
devfrom
JoaquinBN/telegram-bot-features-discussion

Conversation

@JoaquinBN

@JoaquinBN JoaquinBN commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

  • New Features
    • Link and unlink your Telegram account from the profile header (private only).
    • Receive supported portal notifications and announcements through Telegram, including campaign fan-out.
    • Use the Telegram bot for help, rankings, points, missions, and mute/unmute; keep delivery cron-driven.
  • Bug Fixes
    • Telegram handle input is no longer accepted, saved, or searchable (profile edit and pending signup ignore it).
    • Recalling portal notifications cancels queued Telegram deliveries where possible, while already-sent messages can’t be recalled.
  • Documentation
    • Updated Telegram setup guidance and release notes.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Telegram account linking, bot commands, notification outbox delivery, recall handling, private profile integration, scheduled workflows, deployment configuration, and backend/frontend tests.

Changes

Telegram platform integration

Layer / File(s) Summary
Notification contracts and persistence
backend/social_connections/models.py, backend/social_connections/migrations/*, backend/notifications/registry.py, backend/notifications/services.py
Adds Telegram persistence, platform-scoped link state handling, Telegram-enabled event routing, and notification service integration.
Bot linking and command handling
backend/social_connections/telegram*.py, backend/social_connections/urls.py, backend/social_connections/management/commands/*, backend/social_connections/admin.py, backend/social_connections/tests/test_telegram_*
Adds deep-link account linking, webhook authentication, bot commands, disconnect behavior, local polling, webhook registration, administration, and tests.
Outbox rendering, enqueueing, and recall
backend/notifications/telegram.py, backend/notifications/campaigns.py, backend/notifications/admin.py, backend/notifications/admin_mixins.py, backend/notifications/tests.py
Adds rendered Telegram messages, deduplicated enqueueing, cancellation, campaign channel selection, recall messaging, and delivery-state coverage.
Cron and CLI delivery
backend/notifications/views.py, backend/notifications/management/commands/deliver_telegram_messages.py, .github/workflows/telegram-deliver*.yml
Adds cron-token delivery, a CLI drain command, scheduled/manual workflows, and endpoint tests.
Profile privacy and legacy handle removal
backend/users/*, backend/ethereum_auth/*, backend/social_connections/serializers.py
Removes editable/searchable Telegram handles and exposes restricted Telegram connection summaries.
Profile-header linking UI
frontend/src/components/TelegramLink.svelte, frontend/src/components/profile/ProfileHeader.svelte, frontend/src/lib/api.js, frontend/src/routes/ProfileEdit.svelte
Adds owner-only link/unlink controls with polling and removes the legacy Telegram profile input.
Runtime configuration and operational documentation
backend/tally/settings.py, backend/deploy-apprunner*.sh, backend/.env.example, CHANGELOG.md, backend/CLAUDE.md, frontend/CLAUDE.md
Adds Telegram runtime secrets, deployment mappings, environment guidance, documentation, and release notes.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a Telegram bot with private linking, notification delivery, and commands.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch JoaquinBN/telegram-bot-features-discussion

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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
@JoaquinBN JoaquinBN force-pushed the JoaquinBN/telegram-bot-features-discussion branch from 22c7215 to be5f844 Compare July 14, 2026 10:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Telegram "already-sent" caveat shown even for portal-only campaigns.

The message unconditionally appends "Already-sent Telegram messages cannot be recalled." whenever deleted or cancelled is truthy, regardless of whether the campaign's channels ever 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 win

Harden the workflow: least-privilege permissions, avoid direct secret interpolation in run:, add concurrency guard.

Three low-cost hardening items here (all flagged by zizmor):

  1. No permissions: block → defaults to broader-than-needed GITHUB_TOKEN scope for a job that only calls an external endpoint.
  2. ${{ secrets.CRON_SYNC_TOKEN }} / ${{ secrets.DEV_API_BASE_URL }} are expanded directly into the shell script rather than passed via env:; this is the standard GitHub Actions injection anti-pattern.
  3. 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 win

Harden 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): missing permissions: block, secrets interpolated directly into the shell script instead of via env:, and no concurrency: 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb31ac3 and 22c7215.

📒 Files selected for processing (40)
  • .github/workflows/telegram-deliver-dev.yml
  • .github/workflows/telegram-deliver.yml
  • CHANGELOG.md
  • backend/.env.example
  • backend/CLAUDE.md
  • backend/deploy-apprunner-dev.sh
  • backend/deploy-apprunner.sh
  • backend/ethereum_auth/email_verification.py
  • backend/ethereum_auth/views.py
  • backend/notifications/admin.py
  • backend/notifications/admin_mixins.py
  • backend/notifications/campaigns.py
  • backend/notifications/management/commands/deliver_telegram_messages.py
  • backend/notifications/registry.py
  • backend/notifications/services.py
  • backend/notifications/telegram.py
  • backend/notifications/tests.py
  • backend/notifications/views.py
  • backend/social_connections/admin.py
  • backend/social_connections/management/commands/run_telegram_polling.py
  • backend/social_connections/management/commands/set_telegram_webhook.py
  • backend/social_connections/migrations/0006_telegramconnection_telegrammessage.py
  • backend/social_connections/models.py
  • backend/social_connections/oauth_service.py
  • backend/social_connections/serializers.py
  • backend/social_connections/telegram.py
  • backend/social_connections/telegram_bot.py
  • backend/social_connections/tests/test_telegram_commands.py
  • backend/social_connections/tests/test_telegram_link.py
  • backend/social_connections/urls.py
  • backend/tally/settings.py
  • backend/users/serializers.py
  • backend/users/tests/test_profile_update.py
  • backend/users/tests/test_telegram_privacy.py
  • backend/users/views.py
  • frontend/CLAUDE.md
  • frontend/src/components/TelegramLink.svelte
  • frontend/src/components/profile/ProfileHeader.svelte
  • frontend/src/lib/api.js
  • frontend/src/routes/ProfileEdit.svelte
💤 Files with no reviewable changes (2)
  • backend/ethereum_auth/email_verification.py
  • frontend/src/routes/ProfileEdit.svelte

Comment thread backend/notifications/telegram.py Outdated
Comment thread backend/social_connections/models.py Outdated
Comment on lines +96 to +124
@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})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment thread CHANGELOG.md
Comment thread frontend/src/components/TelegramLink.svelte
…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)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (2)
CHANGELOG.md (1)

7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use 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 win

Still unbounded vs. App Runner's 120s request timeout.

This mirrors an unresolved prior review: telegram_deliver (views.py) runs deliver_pending synchronously with DEFAULT_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 rows sending until 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

📥 Commits

Reviewing files that changed from the base of the PR and between 22c7215 and be5f844.

📒 Files selected for processing (40)
  • .github/workflows/telegram-deliver-dev.yml
  • .github/workflows/telegram-deliver.yml
  • CHANGELOG.md
  • backend/.env.example
  • backend/CLAUDE.md
  • backend/deploy-apprunner-dev.sh
  • backend/deploy-apprunner.sh
  • backend/ethereum_auth/email_verification.py
  • backend/ethereum_auth/views.py
  • backend/notifications/admin.py
  • backend/notifications/admin_mixins.py
  • backend/notifications/campaigns.py
  • backend/notifications/management/commands/deliver_telegram_messages.py
  • backend/notifications/registry.py
  • backend/notifications/services.py
  • backend/notifications/telegram.py
  • backend/notifications/tests.py
  • backend/notifications/views.py
  • backend/social_connections/admin.py
  • backend/social_connections/management/commands/run_telegram_polling.py
  • backend/social_connections/management/commands/set_telegram_webhook.py
  • backend/social_connections/migrations/0007_telegramconnection_telegrammessage.py
  • backend/social_connections/models.py
  • backend/social_connections/oauth_service.py
  • backend/social_connections/serializers.py
  • backend/social_connections/telegram.py
  • backend/social_connections/telegram_bot.py
  • backend/social_connections/tests/test_telegram_commands.py
  • backend/social_connections/tests/test_telegram_link.py
  • backend/social_connections/urls.py
  • backend/tally/settings.py
  • backend/users/serializers.py
  • backend/users/tests/test_profile_update.py
  • backend/users/tests/test_telegram_privacy.py
  • backend/users/views.py
  • frontend/CLAUDE.md
  • frontend/src/components/TelegramLink.svelte
  • frontend/src/components/profile/ProfileHeader.svelte
  • frontend/src/lib/api.js
  • frontend/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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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-level permissions: contents: read and a concurrency: {group: telegram-deliver-dev, cancel-in-progress: false} block.
  • .github/workflows/telegram-deliver.yml#L3-12: add a top-level permissions: contents: read and a concurrency: {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

Comment thread .github/workflows/telegram-deliver-dev.yml
jobs:
deliver:
runs-on: ubuntu-latest
environment: cron-job

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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 -S

Repository: 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.

Comment thread .github/workflows/telegram-deliver.yml
Comment thread backend/notifications/telegram.py
Comment on lines +152 to +157
TelegramMessage.objects.create(
direction=TelegramMessage.DIRECTION_IN,
connection=connection,
chat_id=chat_id,
text=text,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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/null

Repository: 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/notifications

Repository: 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/commands

Repository: 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.

Comment thread backend/social_connections/telegram.py Outdated
Comment thread backend/social_connections/tests/test_telegram_commands.py Outdated
…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)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between be5f844 and 74cbcf9.

📒 Files selected for processing (7)
  • .github/workflows/telegram-deliver-dev.yml
  • .github/workflows/telegram-deliver.yml
  • backend/notifications/telegram.py
  • backend/notifications/tests.py
  • backend/social_connections/models.py
  • backend/social_connections/tests/test_telegram_commands.py
  • frontend/src/components/TelegramLink.svelte

Comment thread backend/notifications/tests.py
Comment thread backend/notifications/tests.py Outdated
Comment thread frontend/src/components/TelegramLink.svelte Outdated
…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)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/social_connections/telegram.py (1)

145-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trailing return is unreachable.

The three-attempt retry loop is well-designed. Every branch inside the loop body ends in either return or continue, and continue can only fire twice (once per poppable key). That means the 3rd iteration is always exited via an explicit return inside 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

📥 Commits

Reviewing files that changed from the base of the PR and between ed31eba and 06be31b.

📒 Files selected for processing (2)
  • backend/social_connections/telegram.py
  • backend/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant