Let validators link Telegram support groups through the Deckard bot - #960
Conversation
Validators can now connect Telegram groups to their validator from the
portal. A new "Telegram Support" page in the validator area issues
one-time group bind codes: the validator creates a Telegram group, adds
the Deckard support bot, and runs /bindcode <code> there; the bot
redeems the code server-to-server and the group is bound to the
validator. A validator can bind multiple groups (one code per group),
codes expire after 48 hours, and unredeemed codes can be revoked.
The bind code follows the service-account token design: only a SHA-256
digest is stored, lookup is by a non-secret identifier embedded in the
code, and redemption compares digests in constant time. Redemption is
gated by a new telegram_bind:redeem service-account scope reserved for
the Deckard bot and is atomic and single-use: it records the bound
group chat id and the redeeming Telegram uid, and upserts a new
TelegramConnection (social connection pattern, bot-confirmed instead of
OAuth) so the portal account carries a verifiable Telegram identity.
Issuance is throttled per user; the plaintext code is returned exactly
once and never listed again.
## Claude Implementation Notes
- backend/validators/models.py: TelegramGroupBindCode model — identifier
+ digest storage (hash_code/identifier_from_plaintext/issue mirroring
ServiceAccountToken), issued/redeemed/expired/revoked statuses, 48h
default TTL, lazy effective_status, is_redeemable helper
- backend/validators/migrations/0018_telegramgroupbindcode.py: schema
- backend/validators/views.py: TelegramBindCodeViewSet (create issues +
returns plaintext once, mine lists metadata, revoke with row lock and
409 for redeemed codes) and TelegramBindCodeRedeemView (service
account auth, required_scopes {'*': telegram_bind:redeem}, payload
validation, select_for_update single-use redemption, constant-time
digest compare, TelegramConnection upsert that never blanks an
existing username)
- backend/validators/serializers.py: TelegramBindCodeSerializer —
metadata only, status is the lazy-expiry effective status
- backend/validators/urls.py: explicit redeem path before the router so
the viewset detail route never swallows it; telegram-bind-codes
registered before the catch-all validator route
- backend/social_connections/models.py: TelegramConnection subclass of
the SocialConnection abstract base (numeric uid identity, display-only
username, no OAuth), db_table social_connections_telegram
- backend/social_connections/migrations/0008_telegramconnection.py: schema
- backend/social_connections/admin.py: TelegramConnection admin
- backend/service_accounts/scopes.py: TELEGRAM_BIND_REDEEM_SCOPE added
to ALLOWED_SERVICE_ACCOUNT_SCOPES so admin/command token issuance
accepts it
- backend/utils/throttling.py + backend/tally/settings.py:
TelegramBindCodeIssueRateThrottle, telegram_bind_issue 10/hour
- backend/validators/tests/test_telegram_bind_codes.py: 22 tests —
issuance auth + validator gate + single-shot plaintext, mine
isolation and lazy expiry, revoke rules, redeem scope gate,
single-use, expiry, invalid/tampered codes, connection upsert
- frontend/src/routes/ValidatorTelegram.svelte: Link-a-Telegram-group
page (Svelte 5 runes) — generate code, show once with /bindcode copy
command, list codes with status chips and revoke
- frontend/src/App.svelte: /validators/telegram role-gated route
- frontend/src/components/Sidebar.svelte: Telegram Support link in both
validator sections (role-locked pattern)
- frontend/src/lib/api.js: issue/list/revoke bind-code helpers
- frontend/src/lib/config.js: DECKARD_BOT_USERNAME from
VITE_DECKARD_BOT_USERNAME with generic-wording fallback
- backend/CLAUDE.md + frontend/CLAUDE.md: documented the new model,
endpoints, scope, route, and API helpers
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds one-time, expiring Telegram group bind codes for validators. The backend supports issuance, listing, revocation, and scoped bot redemption. The frontend provides a role-gated page for managing codes and Telegram support links. ChangesTelegram validator group binding
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Validator
participant ValidatorTelegram
participant ValidatorsAPI
participant TelegramBindCodeViewSet
participant TelegramBindCodeRedeemView
participant DeckardTelegramBot
participant TelegramConnection
Validator->>ValidatorTelegram: Generate group code
ValidatorTelegram->>ValidatorsAPI: Issue bind code
ValidatorsAPI->>TelegramBindCodeViewSet: Create code
TelegramBindCodeViewSet-->>ValidatorTelegram: Return plaintext once
Validator->>DeckardTelegramBot: Submit bind code in group
DeckardTelegramBot->>TelegramBindCodeRedeemView: Redeem with scoped service token
TelegramBindCodeRedeemView->>TelegramConnection: Upsert Telegram connection
TelegramBindCodeRedeemView-->>DeckardTelegramBot: Return binding result
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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/service_accounts/scopes.py`:
- Around line 13-15: Update the ALLOWED_SERVICE_ACCOUNT_SCOPES definition to
build the frozenset using iterable unpacking for AI_REVIEW_SCOPES and
TELEGRAM_BIND_REDEEM_SCOPE instead of tuple concatenation, preserving the same
allowed scope values.
In `@backend/social_connections/models.py`:
- Around line 51-67: The requested change is a broader follow-up: the social
connection model hierarchy currently does not inherit BaseModel. Do not modify
TelegramConnection in this PR; track updating SocialConnection and its
GitHubConnection, TwitterConnection, DiscordConnection, and TelegramConnection
subclasses to inherit utils.models.BaseModel as a separate migration-aware
change.
In `@backend/validators/views.py`:
- Around line 1300-1345: Add an explicit throttle scope to
TelegramBindCodeRedeemView and configure the corresponding scoped rate limit so
the inherited ScopedRateThrottle is active for redemption requests. Keep the
existing redemption validation and response behavior unchanged, using the
project’s established throttle-scope configuration pattern.
- Around line 1183-1199: Update the logging in the create method to remove
request.user.address from the Telegram bind-code issuance message, while
retaining request.user.id and bind_code.id as identifiers.
In `@frontend/CLAUDE.md`:
- Line 481: Update the ## Environment Variables section in frontend/CLAUDE.md to
include VITE_DECKARD_BOT_USERNAME in the environment-variable checklist, while
retaining the existing Telegram bind-code documentation and references.
In `@frontend/src/lib/config.js`:
- Around line 16-19: Document the new VITE_DECKARD_BOT_USERNAME environment
variable in the frontend environment example or documentation alongside the
existing VITE_ variables, describing that it should contain the bot username
without the @ symbol. Keep the existing DECKARD_BOT_USERNAME configuration
behavior 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: bd9b324e-5248-497d-a81f-3e36deff1c9e
📒 Files selected for processing (20)
CHANGELOG.mdbackend/CLAUDE.mdbackend/service_accounts/scopes.pybackend/social_connections/admin.pybackend/social_connections/migrations/0008_telegramconnection.pybackend/social_connections/models.pybackend/tally/settings.pybackend/utils/throttling.pybackend/validators/migrations/0018_telegramgroupbindcode.pybackend/validators/models.pybackend/validators/serializers.pybackend/validators/tests/test_telegram_bind_codes.pybackend/validators/urls.pybackend/validators/views.pyfrontend/CLAUDE.mdfrontend/src/App.sveltefrontend/src/components/Sidebar.sveltefrontend/src/lib/api.jsfrontend/src/lib/config.jsfrontend/src/routes/ValidatorTelegram.svelte
| ALLOWED_SERVICE_ACCOUNT_SCOPES = frozenset( | ||
| AI_REVIEW_SCOPES + (TELEGRAM_BIND_REDEEM_SCOPE,) | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Minor: prefer unpacking over concatenation.
Ruff flags the tuple concatenation; unpacking is idiomatic and avoids the implicit tuple-type check.
♻️ Suggested fix
-ALLOWED_SERVICE_ACCOUNT_SCOPES = frozenset(
- AI_REVIEW_SCOPES + (TELEGRAM_BIND_REDEEM_SCOPE,)
-)
+ALLOWED_SERVICE_ACCOUNT_SCOPES = frozenset(
+ (*AI_REVIEW_SCOPES, TELEGRAM_BIND_REDEEM_SCOPE)
+)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ALLOWED_SERVICE_ACCOUNT_SCOPES = frozenset( | |
| AI_REVIEW_SCOPES + (TELEGRAM_BIND_REDEEM_SCOPE,) | |
| ) | |
| ALLOWED_SERVICE_ACCOUNT_SCOPES = frozenset( | |
| (*AI_REVIEW_SCOPES, TELEGRAM_BIND_REDEEM_SCOPE) | |
| ) |
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 14-14: Consider (*AI_REVIEW_SCOPES, TELEGRAM_BIND_REDEEM_SCOPE) instead of concatenation
Replace with (*AI_REVIEW_SCOPES, TELEGRAM_BIND_REDEEM_SCOPE)
(RUF005)
🤖 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/service_accounts/scopes.py` around lines 13 - 15, Update the
ALLOWED_SERVICE_ACCOUNT_SCOPES definition to build the frozenset using iterable
unpacking for AI_REVIEW_SCOPES and TELEGRAM_BIND_REDEEM_SCOPE instead of tuple
concatenation, preserving the same allowed scope values.
Source: Linters/SAST tools
| class TelegramConnection(SocialConnection): | ||
| """Telegram identity for a portal account. | ||
|
|
||
| Telegram has no OAuth flow here: the connection is created/confirmed when | ||
| the Deckard support bot redeems a Telegram group bind code issued by this | ||
| user (see validators.TelegramGroupBindCode). `platform_user_id` holds the | ||
| numeric Telegram user id (the stable identity); `platform_username` is the | ||
| display-only @username, which Telegram users can change at any time. The | ||
| inherited OAuth token fields stay empty. | ||
| """ | ||
|
|
||
| class Meta: | ||
| db_table = 'social_connections_telegram' | ||
| verbose_name = 'Telegram Connection' | ||
| verbose_name_plural = 'Telegram Connections' | ||
|
|
||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff
TelegramConnection doesn't inherit utils.models.BaseModel.
It subclasses the abstract SocialConnection, which is models.Model, not BaseModel. This mirrors existing GitHubConnection/TwitterConnection/DiscordConnection siblings, but as per coding guidelines, "All Django models must inherit from utils.models.BaseModel." Fixing this cleanly would require migrating the whole social_connections app's base, so this is best tracked as a follow-up rather than blocking this PR.
🤖 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/models.py` around lines 51 - 67, The requested
change is a broader follow-up: the social connection model hierarchy currently
does not inherit BaseModel. Do not modify TelegramConnection in this PR; track
updating SocialConnection and its GitHubConnection, TwitterConnection,
DiscordConnection, and TelegramConnection subclasses to inherit
utils.models.BaseModel as a separate migration-aware change.
Source: Coding guidelines
|
|
||
| if bind_code.status == TelegramGroupBindCode.STATUS_REVOKED: | ||
| return Response( | ||
| {'error': 'This bind code was revoked.', 'code': 'revoked'}, | ||
| status=status.HTTP_409_CONFLICT, | ||
| ) | ||
| if bind_code.status == TelegramGroupBindCode.STATUS_REDEEMED: | ||
| return Response( | ||
| {'error': 'This bind code was already redeemed.', 'code': 'already_redeemed'}, | ||
| status=status.HTTP_409_CONFLICT, | ||
| ) | ||
| if not bind_code.is_redeemable(now): | ||
| if bind_code.status == TelegramGroupBindCode.STATUS_ISSUED: | ||
| bind_code.status = TelegramGroupBindCode.STATUS_EXPIRED | ||
| bind_code.save(update_fields=['status', 'updated_at']) | ||
| return Response( | ||
| {'error': 'This bind code has expired.', 'code': 'expired'}, | ||
| status=status.HTTP_410_GONE, | ||
| ) | ||
|
|
||
| bind_code.status = TelegramGroupBindCode.STATUS_REDEEMED | ||
| bind_code.redeemed_at = now | ||
| bind_code.redeemed_group_chat_id = group_chat_id | ||
| bind_code.redeemed_by_telegram_uid = telegram_uid | ||
| bind_code.save(update_fields=[ | ||
| 'status', | ||
| 'redeemed_at', | ||
| 'redeemed_group_chat_id', | ||
| 'redeemed_by_telegram_uid', | ||
| 'updated_at', | ||
| ]) | ||
|
|
||
| # Verifiable Telegram identity for the issuing portal account. The | ||
| # numeric uid is the stable identity; the display-only username is | ||
| # kept only when the bot supplies one, so a later redemption | ||
| # without it never blanks an existing handle. | ||
| connection_defaults = { | ||
| 'platform_user_id': telegram_uid, | ||
| 'linked_at': now, | ||
| } | ||
| if telegram_username: | ||
| connection_defaults['platform_username'] = telegram_username | ||
| TelegramConnection.objects.update_or_create( | ||
| user=bind_code.created_by, | ||
| defaults=connection_defaults, | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
Redeem endpoint has no throttle.
TelegramBindCodeRedeemView inherits the default ScopedRateThrottle with no throttle_scope, so it is effectively unthrottled. Even with high-entropy secrets, adding a scoped throttle bounds bot-side loops and abuse of a compromised service-account token.
🤖 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/validators/views.py` around lines 1300 - 1345, Add an explicit
throttle scope to TelegramBindCodeRedeemView and configure the corresponding
scoped rate limit so the inherited ScopedRateThrottle is active for redemption
requests. Keep the existing redemption validation and response behavior
unchanged, using the project’s established throttle-scope configuration pattern.
|
|
||
| // Deckard Telegram support bot @username (without the @). When unset, the | ||
| // Telegram group linking page falls back to generic wording. | ||
| export const DECKARD_BOT_USERNAME = import.meta.env.VITE_DECKARD_BOT_USERNAME || ''; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Document the new VITE_DECKARD_BOT_USERNAME variable.
Add it to the frontend env example/docs alongside the other VITE_ variables so deployments don't silently fall back to the generic wording.
Based on learnings: "Keep this reference updated when adding routes, components, API functions, pages, navigation, stores, Svelte 5 patterns, common fixes, or environment variables." and "Required environment variables include VITE_API_URL, VITE_VALIDATOR_RPC_URL, VITE_EXPLORER_URL, and VITE_RECAPTCHA_SITE_KEY."
🤖 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 `@frontend/src/lib/config.js` around lines 16 - 19, Document the new
VITE_DECKARD_BOT_USERNAME environment variable in the frontend environment
example or documentation alongside the existing VITE_ variables, describing that
it should contain the bot username without the @ symbol. Keep the existing
DECKARD_BOT_USERNAME configuration behavior unchanged.
Source: Learnings
…v var The bind code issuance log now identifies the issuing user by portal id only, keeping full wallet addresses out of application log sinks in line with the portal's address-privacy stance. The Deckard bot username setting is now part of the documented frontend environment so deployments configure it instead of silently falling back to generic wording. ## Claude Implementation Notes - backend/validators/views.py: issuance log drops request.user.address; user_id + code_id remain as identifiers - frontend/CLAUDE.md: add VITE_DECKARD_BOT_USERNAME to the Environment Variables section - frontend/.env.example: add VITE_DECKARD_BOT_USERNAME with a note that unset falls back to generic wording
Dev independently introduced its own TelegramConnection (Deckard support group bind codes, PR #960) on the same table this branch creates, so this merge reconciles the two Telegram identities into one model: - social_connections/models.py keeps a single TelegramConnection carrying this branch's notifications_enabled/blocked_at fields and unique platform_user_id constraint, with a docstring covering both verifiable creation paths (portal bot /start deep link, Deckard bind-code redemption). Dev's duplicate class definition is removed. - Dev's 0008_telegramconnection migration (already applied in deployed environments) is kept as the base; this branch's migration is regenerated as 0009_telegram_notifications_and_outbox, which only adds the new fields and constraint and creates TelegramMessage. - admin.py keeps one TelegramConnection registration (both sides had one, which would have raised AlreadyRegistered) merging dev's platform_user_id search field into the richer admin. - The Deckard bind-code redemption now rejects a Telegram account already linked to a different portal user with a clean 409 (pre-check before consuming the code, IntegrityError guard for the race) instead of crashing on the new unique constraint; the rejected code stays redeemable. - CHANGELOG.md keeps both sides' entries.
Summary
Adds a portal flow for validators to authorize Deckard (the Foundation's Telegram support bot) into their own Telegram groups. This PR is the portal half of the feature and coordinates with the Deckard bot (deckard-testnet-agent), which will implement the
/bindcodecommand against the contract below.The flow
/validators/telegram)./bindcode <code>there.group_chat_idand redeeming Telegram uid, and upserts a TelegramConnection (social_connections pattern, bot-confirmed — Telegram has no OAuth here) so the portal account carries a verifiable Telegram identity.Multiple groups per validator are supported (one code per group). DM bindings are refused bot-side, before the portal is ever called.
Backend
validators.TelegramGroupBindCodemodel + migration (statuses issued/redeemed/expired/revoked, lazy expiry, revoke).social_connections.TelegramConnectionmodel + migration (numeric Telegram uid = identity, username display-only).POST /api/v1/validators/telegram-bind-codes/— auth + validator profile; returns the plaintext once; throttled 10/hour (telegram_bind_issue).GET /api/v1/validators/telegram-bind-codes/mine/— metadata only, never raw codes.POST /api/v1/validators/telegram-bind-codes/{id}/revoke/— owner-only; 409 once redeemed.POST /api/v1/validators/telegram-bind-codes/redeem/— bot-only, gated byHasServiceAccountScopewith the newtelegram_bind:redeemscope.Bot-side contract (for Deckard's
/bindcode)Success
200:{"success": true, "bind_code_id", "validator_id", "user_id", "user_name", "group_chat_id", "redeemed_at"}.Errors carry a machine
code:invalid_request(400),invalid_code(404),already_redeemed/revoked(409),expired(410).Frontend
/validators/telegrampage (Svelte 5 runes): generate a code, one-time display with a copyable/bindcodecommand, code list with status chips and revoke.VITE_DECKARD_BOT_USERNAME.Tests
22 new Django tests (
validators/tests/test_telegram_bind_codes.py): issuance auth + validator gate, one-shot plaintext, mine isolation + lazy expiry, revoke rules, redeem scope gate, single-use, expiry, tampered/unknown codes, TelegramConnection upsert.validators+social_connections+service_accountssuites green (243 tests).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation