Merge remote-tracking branch 'upstream/main' into merge-gramsrv-9106877
This commit is contained in:
commit
ac6a50c5ff
697 changed files with 100880 additions and 8052 deletions
359
docs/bot_verification.md
Normal file
359
docs/bot_verification.md
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
# Third-party bot verification
|
||||
|
||||
Third-party verification is an **attributed** mark: a bot that the operator
|
||||
appointed as a *verifier* attaches its own custom-emoji icon and one line of
|
||||
description to a peer. Official clients draw that icon **before** the peer's name
|
||||
and show the description in the profile, together with the name of the company the
|
||||
verifier vouches under.
|
||||
|
||||
Reference material:
|
||||
|
||||
- <https://core.telegram.org/api/bots/verification>
|
||||
- <https://telegram.org/verify#third-party-verification>
|
||||
|
||||
Applications are collected by the built-in `@verifierbot`, decided in the admin
|
||||
panel, and the decision commits together with the mark write. The protocol edge then
|
||||
drops the cached peer projections and pushes the ordinary peer-refresh update, so an
|
||||
online client shows the icon without a restart.
|
||||
|
||||
## What this is not
|
||||
|
||||
This is **not** the platform checkmark. That one is a single boolean on the peer
|
||||
(`users.verified` / `channels.verified`), granted by the operator after platform
|
||||
review, collected by `@verifybot`, and documented in [`verification.md`](verification.md).
|
||||
|
||||
The two mechanisms are deliberately disjoint:
|
||||
|
||||
| | Official verification | Third-party verification |
|
||||
| --- | --- | --- |
|
||||
| Stored as | `users.verified`, `channels.verified` (boolean) | `bot_verifier_settings`, `custom_verifications` (attributed rows) |
|
||||
| Granted by | the platform operator | a verifier bot the operator appointed |
|
||||
| Rendered as | the standard checkmark **after** the name | the verifier's custom emoji **before** the name, plus a profile description |
|
||||
| Front door | `@verifybot` | `@verifierbot` (or `bots.setCustomVerification` directly) |
|
||||
| Panel section | *Official verification* (`/verification`) | *Third-party verification* (`/bot-verification`) |
|
||||
| Permissions | `verification.review`, `verification.revoke` | `botverification.review`, `botverification.manage` |
|
||||
|
||||
Neither reads the other's tables. Both can sit on one peer at the same time, an
|
||||
approval on one side never writes the other side's state, and revoking one leaves
|
||||
the other alone. The admin panel repeats that distinction in the section header and
|
||||
on every decision page, because "verified" in a ticket almost always means the other
|
||||
one.
|
||||
|
||||
## TL constructors and flags (Layer 228)
|
||||
|
||||
Checked against the schema snapshot the server is built for,
|
||||
`/tmp/td/_schema/layers/layer-228.tl`.
|
||||
|
||||
| Constructor / method | Field |
|
||||
| --- | --- |
|
||||
| `botVerification#f93cd45c` | `bot_id:long icon:long description:string` |
|
||||
| `botVerifierSettings#b0cd6617` | `can_modify_custom_description:flags.1?true icon:long company:string custom_description:flags.0?string` |
|
||||
| `bots.setCustomVerification#8b89dfbd` | `enabled:flags.1?true bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?string = Bool` |
|
||||
| `user#b1b8cc83` | `bot_verification_icon:flags2.14?long` |
|
||||
| `channel#d49f34c6` | `bot_verification_icon:flags2.13?long` |
|
||||
| `userFull#6cbe645` | `bot_verification:flags2.12?BotVerification` |
|
||||
| `channelFull#a04e8d3a` | `bot_verification:flags2.17?BotVerification` |
|
||||
| `chatInvite#5c9d3702` | `bot_verification:flags.13?BotVerification` |
|
||||
| `botInfo#4d8a0299` | `verifier_settings:flags.9?BotVerifierSettings` |
|
||||
|
||||
One fact — "verifier *B* marked peer *P* with icon *I* and description *D*" — is
|
||||
spread over six unrelated constructors, and a client renders the badge only when the
|
||||
exact bit is set. Every projection therefore goes through the generated `Set*`
|
||||
helpers (`internal/rpc/bot_verification_projection.go`): a struct field assigned
|
||||
without its flag bit encodes as an absent field, and the badge silently disappears.
|
||||
|
||||
Note the asymmetry inside `botVerifierSettings`: `custom_description:flags.0` is the
|
||||
operator-configured *default* description, while
|
||||
`can_modify_custom_description:flags.1` is the permission that lets the verifier
|
||||
override it per peer. `botVerification.description` is the resolved text actually
|
||||
shown on a marked peer.
|
||||
|
||||
## The icon is a custom emoji document
|
||||
|
||||
`botVerification.icon` and `botVerifierSettings.icon` are custom emoji **document
|
||||
ids**. A client resolves them through `messages.getCustomEmojiDocuments` — exactly
|
||||
the reader `files.Service.GetDocuments` answers from on this server.
|
||||
|
||||
Consequences that shape the whole feature:
|
||||
|
||||
- An id that names no fetchable document renders as **nothing at all**: the peer is
|
||||
marked in the database and the client draws an empty space. Nothing errors, nothing
|
||||
logs on the client, and the operator sees a granted mark that users cannot see.
|
||||
- Therefore the icon is never a free-form number. `verification_icons` is a
|
||||
catalogue, `botverification.Service.UpsertIcon` resolves the document before
|
||||
writing the row and refuses anything that is not a custom emoji
|
||||
(`domain.Document.IsCustomEmoji`), and a grant may only reference a catalogue entry
|
||||
that is `active` and either shared or reserved for that bot
|
||||
(`VerificationIcon.UsableBy`).
|
||||
- The mark denormalises the icon at grant time (`custom_verifications.icon_document_id`),
|
||||
so a verifier changing its own icon later does not silently re-brand the peers it
|
||||
already marked.
|
||||
|
||||
The panel exposes the same rule: the icon catalogue tab is where document ids are
|
||||
registered and named, and the grant form only offers active catalogue entries.
|
||||
|
||||
## End-to-end path
|
||||
|
||||
1. **Icon catalogue.** An operator adds a custom emoji document to
|
||||
`verification_icons` (panel: *Third-party verification → Icon catalogue*, or
|
||||
`POST /api/actions/upsert-verification-icon`). Entries can be shared or reserved
|
||||
for one bot, and retiring an entry (`set-verification-icon-active`) stops new
|
||||
grants without touching marks that already carry it.
|
||||
2. **Verifier status.** The operator grants a bot verifier status — an icon from the
|
||||
catalogue, a company name, an optional default description and
|
||||
`can_modify_custom_description` (panel: *Verifiers*, or
|
||||
`POST /api/actions/grant-bot-verifier`). The row in `bot_verifier_settings` *is*
|
||||
verifier status: it is the only authority `bots.setCustomVerification` consults,
|
||||
and it is projected as `botInfo.verifier_settings`. Nothing seeds it — not even
|
||||
migration `0155` for the built-in bot — because seeding verifier status would ship
|
||||
a badge printer with the schema.
|
||||
3. **Two ways to reach a mark.**
|
||||
- **Direct RPC.** The verifier bot (or the user who owns it) calls
|
||||
`bots.setCustomVerification`. `internal/rpc/bots_longtail.go` resolves the two
|
||||
TL branches — `bot:flags.0` unset means "the caller is the bot", set means "a
|
||||
user acting through a bot it owns" — validates shape, and hands a
|
||||
`domain.SetCustomVerificationRequest` to the service. A missing or disabled
|
||||
verifier row answers `403 BOT_VERIFIER_FORBIDDEN`, and the error deliberately
|
||||
does not distinguish "never was a verifier" from "switched off".
|
||||
- **Application queue.** A peer owner talks to `@verifierbot`
|
||||
(`/verify`, `/status`, `/revoke`, `/cancel`, `/help`), picks one of their own
|
||||
bots, channels or their own account, states a reason and optionally a wanted
|
||||
description. The bot writes `custom_verification_requests` with status
|
||||
`pending`; a partial unique index keeps one pending row per
|
||||
(verifier, peer) pair. `@verifierbot` decides nothing — it says so in `/start`.
|
||||
4. **Review.** The panel lists the queue, the verifier roster, the icon catalogue and
|
||||
every granted mark. BFF routes are `GET /api/botverification/{verifiers,icons,marks,requests,counts}`,
|
||||
`GET /api/botverification/requests/{id}` and
|
||||
`POST /api/botverification/requests/{id}/{approve,reject,revoke}`; the manage-only
|
||||
mutations are the `/api/actions/...` commands listed above plus
|
||||
`set-bot-verifier-enabled`, `revoke-bot-verifier` and
|
||||
`revoke-custom-verification`. Every mutation goes through the shared admin command
|
||||
journal (reason → dry run → confirm), so it lands in `admin_commands` /
|
||||
`admin_audit_logs`.
|
||||
5. **Second gate at approval.** `botverification.Service.Approve` re-loads a fresh
|
||||
snapshot: the verifier must still exist and be enabled, the peer must still
|
||||
resolve, and the per-verifier quota is spent only when the approval would create a
|
||||
mark rather than update one. A queue that sat for days cannot launder a state the
|
||||
RPC path would refuse. `version` is an optimistic lock — a stale panel gets a
|
||||
`409` and the page reloads instead of overwriting a fresher decision.
|
||||
6. **Decision transaction.** The status transition and the mark write commit
|
||||
together: `DecideCustomVerificationRequest` runs the grant (or the revoke)
|
||||
through a callback whose context carries the decision's own transaction. "Approved"
|
||||
and "the peer carries the mark" can never disagree. The description is resolved by
|
||||
`BotVerifierSettings.DescriptionFor` — the applicant's wording only when
|
||||
`can_modify_custom_description` is set, otherwise the operator default — which is
|
||||
the single place that rule lives, so the RPC edge, the bot dialog and the panel
|
||||
preview cannot drift.
|
||||
7. **Protocol edge.** After the commit the service calls
|
||||
`rpc.Router.NotifyPeerBotVerification(ctx, domain.Peer)`
|
||||
(`internal/rpc/bot_verification_notify.go`), which:
|
||||
- drops the cached peer projections for the peer, and for a channel also the
|
||||
`channelFull` bot-info cache that carries `botInfo.verifier_settings`;
|
||||
- for a user or bot, reuses `NotifyUserModerationFlagsChanged` — the audience-wide,
|
||||
non-PTS `updateUser` fan-out the scam/fake flags use, filtered to online
|
||||
sessions, with the peer re-projected per recipient;
|
||||
- for a channel, reuses `NotifyChannelChanged` → `updateChannel` plus the refreshed
|
||||
`channel#d49f34c6` object to members (and a linked monoforum when there is one);
|
||||
- is a no-op on a nil receiver and reports an error rather than panicking. A push
|
||||
failure never invalidates the committed decision.
|
||||
8. **Applicant notification.** `@verifierbot` messages the applicant with the
|
||||
outcome (`SendVerificationDecision`). `internal_note` is never rendered there
|
||||
under any status — only `decision_reason` reaches the applicant.
|
||||
9. **What the client sees.** The icon appears before the name in the dialog list,
|
||||
search results, message headers and the profile, and the description appears in the
|
||||
profile. `bots.setCustomVerification` returns `BoolTrue` for every successful
|
||||
application, including an idempotent re-apply or revoke; official clients
|
||||
treat `BoolFalse` as failure.
|
||||
|
||||
## Where the mark surfaces
|
||||
|
||||
| Surface | Method | Field |
|
||||
| --- | --- | --- |
|
||||
| Dialog list, search, history, difference | `messages.getDialogs`, `contacts.search`, `contacts.resolveUsername`, `messages.getHistory`, `updates.getDifference`, … | `user.bot_verification_icon`, `channel.bot_verification_icon` |
|
||||
| User profile | `users.getFullUser` | `userFull.bot_verification` |
|
||||
| Channel / supergroup info | `channels.getFullChannel` | `channelFull.bot_verification` |
|
||||
| Invite preview (non-member) | `messages.checkChatInvite` | `chatInvite.bot_verification` |
|
||||
| Verifier bot's own profile | `users.getFullUser`, `channels.getFullChannel` bot list | `botInfo.verifier_settings` |
|
||||
| Live updates | pushed `updates` envelopes | `updateUser` / `updateChannel` plus the peer object |
|
||||
|
||||
The icon overlay runs at the **response boundary**, not inside `tgUser`/`tgChannel`:
|
||||
`applyPeerReadModels` (`internal/rpc/story_peer_projection.go`) is the single hook
|
||||
every handler funnels through, so all ~40 call sites get the field with one batched
|
||||
read per response instead of an N+1 per peer. The `userFull` / `channelFull` /
|
||||
`chatInvite` variants are post-cache overlays for the same reason — a cached full
|
||||
object is still stamped with the current mark. A nil service or any read error leaves
|
||||
every flag unset, which is byte-identical to the pre-feature wire shape.
|
||||
|
||||
## Migrations
|
||||
|
||||
- **`0155_bot_verification`** creates the four tables:
|
||||
- `verification_icons` — the catalogue. `document_id` is unique and positive,
|
||||
`owner_bot_id = 0` means shared, `active` retires an entry without deleting it.
|
||||
- `bot_verifier_settings` — verifier status, keyed by `bot_id` with an optimistic
|
||||
`version`, `enabled` as the per-verifier kill switch, and the operator's
|
||||
`granted_by` / `grant_reason` for the audit trail.
|
||||
- `custom_verifications` — granted marks. `UNIQUE (peer_type, peer_id)`
|
||||
matches the single `BotVerification` value on the wire: a different verifier
|
||||
replaces the current mark rather than leaving hidden fallback rows. It also has
|
||||
`peer_type IN ('user','channel')`,
|
||||
`icon_document_id` denormalised from the verifier, `ON DELETE CASCADE` from the
|
||||
verifier row.
|
||||
- `custom_verification_requests` — the review queue. `status IN ('pending','approved','rejected','revoked')`,
|
||||
a partial unique index for one `pending` row per (verifier, peer), and check
|
||||
constraints that pair each stamp with its status
|
||||
(`(status = 'approved') = (approved_at IS NOT NULL)`) and refuse a rejection
|
||||
without a reason.
|
||||
- **`0156_verifier_service_bot`** seeds `@verifierbot` (id `1250000013`, fixed
|
||||
`access_hash` double-written with `domain.VerifierBotAccessHash`), its `bots` row
|
||||
and command list, and its `peer_usernames` registry entry, so the handle is occupied
|
||||
from the moment the schema is current. `verified = false` on purpose: a third-party
|
||||
verifier wearing the platform checkmark would blur the exact distinction it has to
|
||||
explain to every applicant. The seed grants **no** verifier status — an operator
|
||||
does that by hand in the panel.
|
||||
|
||||
Neither migration adds read-model triggers: the marks are read live at the response
|
||||
boundary rather than cached in a peer read model.
|
||||
|
||||
## Configuration
|
||||
|
||||
Third-party verification (`internal/config/config.go`, `.env.example`):
|
||||
|
||||
| Key | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `TELESRV_BOT_VERIFICATION_ENABLED` | `true` | Master switch. When off, every third-party mutation is refused (grants, revocations, applications, catalogue edits) while marks already granted keep rendering — blanking one verifier's badges is what its per-verifier kill switch is for. |
|
||||
| `TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER` | `10000` | Peers one verifier may mark. `0` disables the service bound and leaves only the storage bound (`domain.MaxCustomVerificationsPerVerifier`), which is also the maximum this key accepts. |
|
||||
| `TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT` | `5` | Applications one applicant may file per window, across all verifier bots. `0` disables the budget. Looser than the official `3` on purpose: a deployment can run several verifier companies, and filing with a second one is not a retry of the first. |
|
||||
| `TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW` | `24h` | That window. A positive limit requires a positive window. |
|
||||
|
||||
Operator access:
|
||||
|
||||
| Key | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `TELESRV_ADMIN_UI_PERMISSIONS` | `*` | Permissions of an Admin UI session. Reading the section and deciding applications needs `botverification.review`; the verifier roster, the icon catalogue and stripping a granted mark need `botverification.manage`. |
|
||||
| `TELESRV_ADMIN_SCOPED_TOKENS` | *(empty)* | `name:token:perm1,perm2` entries separated by `;`, for integrations that should get `botverification.review` and nothing else. |
|
||||
|
||||
The two rights are independent of the official ones: a reviewer may hold
|
||||
`verification.review` without `botverification.review`, and vice versa. The panel
|
||||
hides the nav entry, gates the route and hides the manage-only buttons accordingly;
|
||||
every route is checked again server-side.
|
||||
|
||||
## Manual check
|
||||
|
||||
### Telegram Desktop
|
||||
|
||||
1. In the panel, open *Third-party verification → Icon catalogue* and add a custom
|
||||
emoji document id. The *Emoji* section lists the documents this deployment holds
|
||||
with their ids; pick one that a client can actually fetch.
|
||||
2. Open *Verifiers*, grant `@verifierbot` verifier status with that icon, a company
|
||||
name (say `Acme Verification Ltd`), a default description
|
||||
(`Verified by Acme`) and `can_modify_custom_description` off for the first pass.
|
||||
Confirm the audit entry appeared.
|
||||
3. Log in to the deployment with official Telegram Desktop and open `@verifierbot`.
|
||||
Its profile now carries a **"verified by" block** built from
|
||||
`botInfo.verifier_settings` — the company and the icon — while its name has **no**
|
||||
platform checkmark. That contrast is the point.
|
||||
4. Send `/verify`, pick one of your channels from the inline picker, state a reason,
|
||||
confirm. `/status` lists the application as pending.
|
||||
5. In the panel open the queue, open the application, read the *Description the mark
|
||||
would carry* preview (with `can_modify_custom_description` off it shows the
|
||||
verifier default, not what you asked for), and approve.
|
||||
6. Within a moment `@verifierbot` messages you the decision.
|
||||
7. Without restarting the client, check the icon on the approved channel:
|
||||
- **Profile** — the icon sits immediately **before** the title, and the
|
||||
description line ("Verified by Acme") appears in the profile body
|
||||
(`channelFull.bot_verification`).
|
||||
- **Dialog list** — the chat row shows the icon before the title.
|
||||
- **Message header** — open the chat; the header title carries the icon.
|
||||
- **Search** — type the `@username` in global search; the result row carries it.
|
||||
- **Invite preview** — from a second account that is **not** a member, open an
|
||||
invite link to that channel: the join box carries the icon
|
||||
(`chatInvite.bot_verification`).
|
||||
8. If the peer also holds the platform checkmark, both are visible at once: the
|
||||
custom icon before the name, the checkmark after it.
|
||||
9. In the panel, revoke from the application's danger zone (or *Granted marks →
|
||||
Remove mark*). The icon disappears from all of those surfaces on the next push or
|
||||
read, and the platform checkmark stays untouched.
|
||||
10. To check the invisible-badge failure mode on purpose, retire the icon and grant
|
||||
a verifier a catalogue entry whose document was deleted: the peer is marked in
|
||||
the database and the client draws nothing. That is why the catalogue validates
|
||||
documents up front.
|
||||
|
||||
### Telegram Android
|
||||
|
||||
1. Log in with the official Android client, force-close it and reopen it after the
|
||||
grant so the profile cache is cold.
|
||||
2. `@verifierbot` profile: the verifier block ("verified by *company*" with the
|
||||
icon) is rendered under the bot's info, and the bot's name has no checkmark.
|
||||
3. Approve an application for a **user account** (your own) and open that account's
|
||||
profile from a second device: the icon is drawn before the name in the profile
|
||||
header and in the chat header, and the description is a line in the profile
|
||||
(`userFull.bot_verification`).
|
||||
4. Chat list and global search rows carry the icon before the name
|
||||
(`user.bot_verification_icon`).
|
||||
5. Custom emoji rendering follows the client's animated-emoji setting: with animated
|
||||
emoji disabled the icon shows as a static frame, and while the document is still
|
||||
being fetched the slot is briefly empty. Neither is a server-side problem.
|
||||
6. Revoke from the panel and pull-to-refresh the profile: the icon is gone.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **A verifier can mark a peer that never asked.** `bots.setCustomVerification`
|
||||
authorises the *caller* (the bot itself, or a user who owns it) and the *verifier
|
||||
status*, not the target's consent. Verifier status is the trust boundary; that is
|
||||
why granting it is an operator-only action, why it has a kill switch, and why
|
||||
`TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER` bounds it. A peer cannot refuse or
|
||||
remove a mark itself — only the verifier (`/revoke` in the bot dialog, or the RPC
|
||||
with `enabled` unset) or an operator can.
|
||||
- **No per-application event history.** Unlike official verification, there is no
|
||||
`*_events` table: an application keeps only `decided_by`, `decision_reason`,
|
||||
`internal_note` and its stamps. The full trail lives in the shared
|
||||
`admin_commands` / `admin_audit_logs` journal, so the panel's decision page shows a
|
||||
decision, not a timeline.
|
||||
- **A revocation clears `approved_at`.** `0155` pairs each stamp with its status, so
|
||||
leaving the approved state nulls the approval stamp. After a revoke, "when was this
|
||||
approved?" can only be answered from the audit journal.
|
||||
- **Applicant notifications are best-effort.** They are sent directly by
|
||||
`@verifierbot` after the decision commits, not through a durable outbox like the
|
||||
official flow's `verification_notification_outbox`. A delivery failure is logged
|
||||
and swallowed (`notifyApplicant`); the decision itself stands, and nothing retries
|
||||
the message, so an applicant can end up with a decided application they were never
|
||||
told about.
|
||||
- **Only users and channels can be marked.** `peer_type` is constrained to
|
||||
`user`/`channel`, matching the TL surface: legacy basic groups (`chat#…`) have no
|
||||
`bot_verification` field in Layer 228, so a non-migrated basic group can never
|
||||
show a mark.
|
||||
- **Only bots can be verifiers, and system bots cannot** — except the built-in
|
||||
`@verifierbot`. `botInfo.verifier_settings` exists only on a bot, so a user account
|
||||
granted verifier status would carry a status no client can see; seeded service
|
||||
accounts are refused outright (`@verifybot` in particular, which owns the *other*
|
||||
mechanism).
|
||||
- **One icon per verifier, one mark per peer.** A verifier cannot vary
|
||||
its icon per peer, there are no verification tiers, and no expiry: a mark lives
|
||||
until somebody removes it. Nothing re-validates a marked peer over time — losing
|
||||
its username or picking up a scam flag later does not clear the mark.
|
||||
- **A new verifier replaces the current peer mark.** `user.bot_verification_icon`
|
||||
and `channel.bot_verification_icon` are single `long` fields, so the database
|
||||
stores one matching mark. Replacing it cannot leave an older hidden mark that
|
||||
unexpectedly reappears after a revoke or kill-switch change.
|
||||
- **A retired icon keeps rendering on existing marks.** Retiring a catalogue entry
|
||||
only blocks new grants, because the mark copied the document id at grant time.
|
||||
Blanking an already-granted icon means revoking the marks (or the verifier).
|
||||
- **An unresolvable document is an invisible badge.** The catalogue validates the
|
||||
document when the entry is written, not continuously. A document deleted afterwards
|
||||
leaves marks that render as nothing, and the server has no way to notice.
|
||||
- **The live push only reaches online sessions**, and the user fan-out is bounded by
|
||||
the same capped moderation audience the scam/fake flags use. Everybody else
|
||||
converges on their next authoritative read, which is always correct: the icon is an
|
||||
overlay read live at the response boundary rather than a cached read-model column.
|
||||
- **`updateUser` / `updateChannel` carry no `pts`.** They are not persisted as
|
||||
message-box events, so a session that was offline during the decision never replays
|
||||
the push; it picks the mark up as *state* on its next read, not as an *event*.
|
||||
- **`channelFull`'s bot-info cache is per process.** `NotifyPeerBotVerification`
|
||||
drops it on the instance that handled the decision. On other instances a cached
|
||||
`channelFull.bot_info` can still carry a stale `verifier_settings` block (the
|
||||
company/icon *of the verifier bot*, not the peer's mark) until that entry expires.
|
||||
The peer's own `bot_verification` fields are overlaid post-cache and are not
|
||||
affected.
|
||||
- **`TELESRV_BOT_VERIFICATION_ENABLED=false` is not a badge switch.** It refuses new
|
||||
mutations; the marks already granted keep being projected. Use the per-verifier kill
|
||||
switch, or revoke, to actually clear badges.
|
||||
|
|
@ -21,7 +21,9 @@ This document describes every setting loaded by `internal/config`. Defaults and
|
|||
| `TELESRV_LISTEN` | string / `0.0.0.0:2398` | MTProto TCP listen address. Must match the address/port reachable by patched clients. |
|
||||
| `TELESRV_ADVERTISE_IP` | string / `127.0.0.1` | Client-reachable server IP used by media/call fallbacks. The current static Desktop DC patch does not derive its MTProto endpoint from this value. |
|
||||
| `TELESRV_RSA_KEY` | path / `data/server_rsa.pem` | MTProto RSA private key. Generated when missing. Treat the file as a secret and keep it stable across restarts. |
|
||||
| `TELESRV_DC` | int / `2` | Server DC ID. Must match patched client expectations and stored media/DC metadata. |
|
||||
| `TELESRV_DC` | int / `2` | Canonical server DC ID used in server-originated configuration and media/DC metadata. It does not partition key-exchange state on the current single backend. |
|
||||
| `TELESRV_DEFAULT_COUNTRY_CODE` | ISO alpha-2 / `CN` | Country returned by `help.getNearestDc` for login-page preselection. Clients map `CN` to calling code `+86`, `US` to `+1`, and so on. Input is trimmed, uppercased, and validated as a country or autonomous area; malformed or unknown values fail startup. |
|
||||
| `TELESRV_STRICT_DC_CHECK` | bool / `false` | Default `false` accepts every wire int32 DC label for permanent and temporary key exchange. `true` requires permanent `dc_id == TELESRV_DC` and temporary `abs(dc_id) == TELESRV_DC`; it is only a diagnostic and does not provide multi-DC isolation. |
|
||||
| `TELESRV_WEBSOCKET_ENABLE` | bool / `true` | Enables MTProto-over-WebSocket demultiplexing on the MTProto listener. |
|
||||
| `TELESRV_WEBSOCKET_ALLOWED_ORIGINS` | list / `http://localhost:1234,http://127.0.0.1:1234` | Browser WebSocket origin allow-list. `*` is for temporary debugging only. |
|
||||
| `TELESRV_MTPROTO_MAX_CONNECTIONS` | int / `200000` | Global physical connection admission limit. Negative disables this gate. |
|
||||
|
|
@ -32,20 +34,24 @@ This document describes every setting loaded by `internal/config`. Defaults and
|
|||
| `TELESRV_MTPROTO_RPC_TIMEOUT` | duration / `30s` | End-to-end handler timeout for scheduled RPC work. |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_WORKERS` | int / `256` | Shared fair-scheduler worker count. |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS` | int / `8192` | Process-wide scheduled/in-flight RPC task cap. |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | Process-wide queued/in-flight RPC request-body budget. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_ENTRIES` | int / `262144` | Global ownership entries for pending owners, completed results, and tombstones during the in-process 331-second replay window. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_BYTES` | int64 bytes / `67108864` | Global retained-byte budget. Owner admission reserves one byte; Put transfers it to a body or tombstone. Must be at least `16775168`. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_ENTRIES` | int / `32768` | Per raw-auth-key ownership entries; charged together with global and session scopes. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_BYTES` | int64 bytes / `33554432` | Per raw-auth-key retained bytes. Limits must satisfy `global >= auth >= session`. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_ENTRIES` | int / `16384` | Per `raw auth key + session_id` ownership entries. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_BYTES` | int64 bytes / `16777216` | Per `raw auth key + session_id` retained bytes; large enough for one legal outbound body. |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_PENDING_PER_AUTH` | int / `2048` | Additional active-owner cap per raw auth key; no greater than global pending tasks or auth entries. |
|
||||
| `TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | Process-wide reservation for transport wire bytes plus maximum decrypted plaintext, acquired before payload allocation. |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES` | int64 charge bytes / `536870912` | Process-wide reserved/queued/in-flight RPC memory charge. Exact admission reserves a conservative typed-materialization charge from wire size and grows it atomically before a nested-gzip-expanded graph is decoded; grow failure rejects the complete candidate batch. This is not an equal amount of concurrently receivable wire bytes. |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES` | int / `262144` | Global cap for pending owners and compact unacknowledged execution receipts. Receipts retain request identity, execution outcome, and Layer admission metadata only—never TL bodies. `msgs_ack` removes them immediately; 331 seconds is only the no-ACK safety horizon. |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES` | int / `32768` | Per-raw-auth owner/receipt cap; limits satisfy `global >= auth >= session`. |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES` | int / `16384` | Per `raw auth key + session_id` owner/receipt cap. |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH` | int / `2048` | Additional active-owner cap per raw auth key; no greater than global pending tasks or auth entries. |
|
||||
| `TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | Process-wide reservation for transport wire bytes, maximum decrypted plaintext, and every live outer/nested gzip expansion, acquired before the corresponding payload allocation. |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE` | int / `128` | Per-connection normal outbound mailbox capacity. |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_CONTROL_QUEUE_SIZE` | int / `32` | Per-connection control-message mailbox capacity. |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | Global budget for tracked resend-pending message bodies. |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | Sole global budget for unacknowledged logical-session bodies. Reconnects reuse the same `msg_id/seq_no/body`; ACK, destroy, or six minutes offline releases it, with no second RPC cache/spool copy. |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_WRITE_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | Global budget for concurrent encrypted wire/codec/obfuscation scratch. |
|
||||
|
||||
Nested gzip admission adds no environment setting. Code-enforced ceilings are
|
||||
10 MiB of output per `gzip_packed` envelope and 32 MiB of cumulative expansion
|
||||
work per transport frame across outer, nested, sibling, failed, and
|
||||
authoritative-profile re-decode attempts. Releasing a live expanded buffer
|
||||
returns process memory but does not refund that frame's CPU/work counter.
|
||||
Retained typed graphs remain charged to the RPC scheduler budget above.
|
||||
|
||||
## 3. HTTP endpoints, public links, and administration
|
||||
|
||||
| Setting | Type / code default | Description and constraints |
|
||||
|
|
@ -58,14 +64,14 @@ This document describes every setting loaded by `internal/config`. Defaults and
|
|||
| `TELESRV_ADMIN_UI_PASSWORD` | secret string / empty | Admin UI login password. Configure this or `TELESRV_ADMIN_UI_TOKEN`. |
|
||||
| `TELESRV_ADMIN_UI_TOKEN` | secret string / empty | Alternative Admin UI login credential. Admin write calls still use the separate `TELESRV_ADMIN_API_TOKEN`. |
|
||||
| `TELESRV_ADMIN_SESSION_KEY` | secret string / empty | Encrypts/signs Admin UI session cookies. Production should use at least 32 random bytes; changing it invalidates sessions. |
|
||||
| `TELESRV_ADMIN_UI_PERMISSIONS` | comma-separated list / `*` | Permissions granted to an Admin UI session authenticated with `TELESRV_ADMIN_UI_PASSWORD` / `_TOKEN`. `*` grants every permission and is the default, so enabling RBAC never locks an operator out of a panel that worked before. Names use letters, digits and `._:-`, at most 64 characters, and may end in `namespace.*` to grant a whole namespace. An empty list or an unparsable name fails startup. |
|
||||
| `TELESRV_ADMIN_SCOPED_TOKENS` | `name:token:perm1,perm2` entries separated by `;` / empty | Additional Admin API bearer tokens carrying a bounded permission set each, so an integration gets exactly the rights it needs instead of the unrestricted `TELESRV_ADMIN_API_TOKEN`. A token may contain neither `:` nor whitespace, every entry must list at least one permission, names and tokens must be unique, and reusing `TELESRV_ADMIN_API_TOKEN` as a scoped token is refused because it would silently widen it to every permission. Any malformed entry fails startup rather than silently granting or dropping rights. |
|
||||
| `TELESRV_PUBLIC_BASE_URL` | HTTP(S) URL / `https://telesrv.net` | Client-visible canonical public-link root. Paths are allowed; credentials, query, and fragment are rejected. Local example: `http://127.0.0.1:2401`. |
|
||||
| `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | Automatic app-open scheme on landing pages. Must match patched client registration. `tg`, `http`, and `https` are rejected. |
|
||||
| `TELESRV_PUBLIC_APP_LINK_BASE` | nullable custom URL base / empty | Optional host-based root for multi-server clients, for example `owpg://example.com`. When set, links use `owpg://example.com/oauth`, `owpg://example.com/<username>`, and equivalent route paths. Only exact `<custom-scheme>://<host>` values are accepted; ports, paths, queries, and fragments are rejected. `TELESRV_PUBLIC_APP_SCHEME` remains an accepted legacy input. |
|
||||
| `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | Web-client root used by public username pages. Same URL validation as `TELESRV_PUBLIC_BASE_URL`. |
|
||||
| `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://weba.telesrv.net` | Web-client root used by public username pages. Same URL validation as `TELESRV_PUBLIC_BASE_URL`. |
|
||||
| `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | Public landing-page product name; trimmed, non-empty, no control characters, maximum 64 Unicode characters. |
|
||||
| `TELESRV_SCAM_WARNING` | string / empty | Overrides the profile warning injected into `getFullUser`/`getFullChannel` About for SCAM-flagged peers. Empty keeps the built-in per-peer-type English default. Non-destructive: the stored bio/description is never overwritten and the warning is re-applied from the flag on every read. Clients cannot localize server-provided text. |
|
||||
| `TELESRV_FAKE_WARNING` | string / empty | Same as `TELESRV_SCAM_WARNING`, for FAKE-flagged peers. |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Read-only username/avatar/sticker/emoji/chatlist/collectible-gift landing-page listener. Empty disables it. Production should bind loopback behind exact nginx routes. `.env.example` enables `127.0.0.1:2401` for development. |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Username/avatar/sticker/emoji/chatlist/collectible-gift landing pages plus the hash-only moderation appeal form. Empty disables it. Production should bind loopback behind exact nginx routes. Moderation `freeze_account` actions fail closed when this listener is disabled because telesrv cannot issue a reachable appeal URL. `.env.example` enables `127.0.0.1:2401` for development. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ENABLE` | bool / `false` | Mount the self-hosted Telegram Login/OIDC provider on `TELESRV_PUBLIC_LINK_WEB_ADDR`. Enabling it requires that listener and all key files below. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ISSUER` | absolute origin URL / `TELESRV_PUBLIC_BASE_URL` | Exact public issuer used in discovery and tokens. HTTPS is required by default; paths, credentials, query, and fragment are rejected. The next setting permits any HTTP host/IP. |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP` | bool / `false` | When enabled, permits any valid HTTP issuer, BotFather Web origin, redirect URI, and native HTTP callback, without loopback, subnet, or port restrictions. When disabled, those Web URLs still require HTTPS. |
|
||||
|
|
@ -390,6 +396,7 @@ key rings independently on different instances.
|
|||
| `TELESRV_BLOB_DIR` | path / `data/blobs` | Local development blob-backend root for media bytes. |
|
||||
| `TELESRV_STICKER_SEED_DIR` | path / `data/sticker-seed` | Sticker/reaction seed packages imported into documents, sticker sets, and blobs. |
|
||||
| `TELESRV_STICKER_SEED_MAX_SETS` | int / `300` | Maximum regular sticker sets imported at startup; `<=0` means unlimited. |
|
||||
| `TELESRV_PREMIUM_PROMO_SEED_DIR` | path / `data/premium-promo` | Exported `help.getPremiumPromo` manifest, MP4 videos, and JPEG thumbnails. A missing directory keeps the no-video fallback; an existing invalid/incomplete directory fails startup. |
|
||||
|
||||
The language-pack file manifest is authoritative. To add a language, place `data/langpack/<pack>/<pack>_<lang>_v<version>.strings` and restart `telesrv`. The `pack` must match its first-level directory and may use the letters, digits, `-`, and `_` already used by Telegram (for example, `android_x`); `lang` is canonicalized to lowercase with hyphens (`pt_BR` becomes `pt-br`). Only the highest file version for each language is loaded. Effective content changes require a version bump; same-version effective mutations and version rollbacks stop startup. Removing a language file or an entire pack subdirectory atomically removes its database catalog and strings on the next restart. Startup streams a source-file SHA-256 first: unchanged files reuse the last atomic manifest without parsing strings or writing the database, while only new or changed files are parsed and replaced through PostgreSQL `COPY`.
|
||||
|
||||
|
|
@ -510,6 +517,13 @@ The following fallback keys are accepted from the **process environment only**.
|
|||
| `TELESRV_RETENTION_INTERVAL` | duration / `1h` | General retention worker interval. |
|
||||
| `TELESRV_RETENTION_BATCH` | int / `10000` | Maximum rows deleted by one general retention batch. |
|
||||
|
||||
Moderation report/evidence/case/decision/action/appeal rows are durable audit facts and are not removed by the
|
||||
general retention worker. The same worker deletes expired sponsored impressions and appeal links in bounded seek
|
||||
batches, and deletes auth-delivery diagnostics and client telemetry after their fixed 30-day privacy retention.
|
||||
The raw appeal token is never persisted. Production reverse proxies must expose only `/appeal/<token>` to the
|
||||
public-link listener, preserve HTTPS in `TELESRV_PUBLIC_BASE_URL`, cap request bodies, and must not log the tokenized
|
||||
path. `TELESRV_PUBLIC_BASE_URL` must resolve to that proxy for moderation freeze actions.
|
||||
|
||||
## 10. Premium and Stars development grants
|
||||
|
||||
| Setting | Type / code default | Description and constraints |
|
||||
|
|
@ -532,6 +546,118 @@ The following fallback keys are accepted from the **process environment only**.
|
|||
| `TELESRV_STARGIFT_CRAFT_DELAY` | duration / `0s` | Delay snapshotted into `can_craft_at`. |
|
||||
| `TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE` | int / `250` | Per-input local craft success contribution, capped at 1000 permille. |
|
||||
|
||||
### Composite account rating and collectible usernames
|
||||
|
||||
The account rating is gramsrv's own local score combining Stars received and spent, bounded account activity, and
|
||||
moderation penalties; it does not claim to reproduce Telegram's private algorithm 1:1. The stored level is projected
|
||||
through `userFull.stars_rating`, while `stars_my_pending_rating` and its activation date are exposed only to the
|
||||
account itself so official clients can render the result without a patch. Profile reads only fetch a projection
|
||||
already persisted by the background worker and reuse the existing 30-minute `userFull` projection cache; they never
|
||||
recompute or write a rating synchronously. Every component remains separately explainable. Collectible (NFT)
|
||||
usernames are minted by the operator; no external marketplace, wallet or chain node is configured or contacted.
|
||||
|
||||
| Setting | Type / code default | Description and constraints |
|
||||
|---|---|---|
|
||||
| `TELESRV_RATING_ENABLED` | bool / `true` | Enables the local composite rating and client level projection. Disabled refuses rating writes and leaves client rating flags unset. |
|
||||
| `TELESRV_RATING_PENDING_DELAY` | duration / `24h` | How long a local rating increase stays pending before it becomes the visible admin level. A decrease is always applied immediately, so a penalty is never delayed. `0` applies every change at once; must be `0..720h`. |
|
||||
| `TELESRV_RATING_RECOMPUTE_INTERVAL` | duration / `15m` | Background recompute worker interval; must be positive. |
|
||||
| `TELESRV_RATING_RECOMPUTE_BATCH` | int / `500` | Stale projections recomputed per cycle; must be `1..10000`. |
|
||||
| `TELESRV_RATING_STALE_AFTER` | duration / `6h` | Projection age after which the worker recomputes a user; must be positive. |
|
||||
| `TELESRV_RATING_WEIGHT_STARS_RECEIVED_PERMILLE` | int64 / `1000` | Weight of Stars credited to the account (gifts, reactions, paid messages received), in permille of the raw amount. |
|
||||
| `TELESRV_RATING_WEIGHT_STARS_SPENT_PERMILLE` | int64 / `250` | Weight of Stars the account spent, in permille. Spending is a weaker signal than receiving. |
|
||||
| `TELESRV_RATING_WEIGHT_MESSAGE_SENT` | int64 / `1` | Score per sent message. |
|
||||
| `TELESRV_RATING_WEIGHT_ACCOUNT_AGE_DAY` | int64 / `2` | Score per day of account age. |
|
||||
| `TELESRV_RATING_WEIGHT_GIFT_RECEIVED` | int64 / `25` | Score per collectible gift held. |
|
||||
| `TELESRV_RATING_WEIGHT_MODERATION_CASE` | int64 / `150` | Penalty magnitude per upheld moderation case; the formula subtracts it. |
|
||||
| `TELESRV_RATING_WEIGHT_SCAM_PENALTY` | int64 / `5000` | Flat penalty magnitude for the scam flag. |
|
||||
| `TELESRV_RATING_WEIGHT_FAKE_PENALTY` | int64 / `5000` | Flat penalty magnitude for the fake flag. |
|
||||
| `TELESRV_RATING_ACTIVITY_CAP` | int64 / `5000` | Upper bound of the activity component so activity alone cannot outweigh Stars and moderation; `0` leaves it uncapped. |
|
||||
| `TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE` | URL template / empty | Landing URL recorded on a minted collectible username when the mint command carries no explicit URL. Empty derives `<TELESRV_PUBLIC_BASE_URL>/nft/username/<username>`. A configured template must be an absolute http(s) URL without userinfo; it may carry the `{username}` placeholder, and without it the name is appended as the last path segment. |
|
||||
|
||||
All rating weights are non-negative magnitudes and are validated even when the feature is disabled, so enabling it
|
||||
later is not the moment a typo is discovered. The defaults above are exactly the shipped domain formula, so behaviour
|
||||
is identical whether or not these keys are set. The final score is clamped at zero: penalties can erase a rating but
|
||||
never invert it.
|
||||
|
||||
**Collectible username prices are stored in the smallest units of their currency**, because that is what
|
||||
`fragment.collectibleInfo` carries: `amount` is "the total price in the smallest units of the currency (integer, not
|
||||
float/double)" and `crypto_amount` likewise. So `USD 1000` is ten dollars, `TON 900` is 900 nanotons, and `XTR 1000` is
|
||||
a thousand Stars, since Stars have no subunit. Clients divide by that exponent before drawing the price. The admin panel
|
||||
is the conversion boundary: prices are typed and displayed there in whole currency units and converted on the way to the
|
||||
API, so an operator never has to count zeros. An integration writing to `/api/actions/mint-collectible-username`
|
||||
directly is talking to the API, not the panel, and must send smallest units itself.
|
||||
|
||||
### Official platform verification
|
||||
|
||||
Official verification is the platform badge (`user.verified` / `channel.verified`): an application is filed through the
|
||||
built-in `@verifybot`, decided in the admin panel, and an approval flips that one flag on that one peer record. It is
|
||||
deliberately not the third-party `botVerification` icon, where an outside organisation attaches its own mark. The
|
||||
application row is the durable audit subject and is never deleted, only moved through its status machine.
|
||||
|
||||
Every eligibility check runs twice: once when the application is filed and again, against a freshly loaded snapshot, at
|
||||
the moment of approval. A target must exist, carry a public username, be controlled by the applicant (bot owner, or
|
||||
channel creator/administrator), not already be verified, not be deleted/frozen/scam/fake, and not be a built-in system
|
||||
entity. A target that changed between submission and review is refused at the second gate, so the review queue cannot
|
||||
be used to grant a state the submission path forbids. The flag itself is written inside the decision transaction, so
|
||||
"approved" and "target verified" commit together.
|
||||
|
||||
Submitted links (website, social, press) are validated as plain http(s) URLs to public hosts: credentials, non-web
|
||||
schemes, non-standard ports, loopback, link-local, private and other reserved address space are all refused. **The
|
||||
server never fetches a submitted link** — not at submission, not during review, not from the admin panel. That is a
|
||||
deliberate anti-SSRF decision, and validation is the only thing ever done to an applicant-controlled URL.
|
||||
|
||||
| Setting | Type / code default | Description and constraints |
|
||||
|---|---|---|
|
||||
| `TELESRV_VERIFICATION_ENABLED` | bool / `true` | Enables official verification. Disabled refuses every verification use case explicitly; peers already carrying the badge keep it, because the flag lives on the peer record. |
|
||||
| `TELESRV_VERIFICATION_ALLOW_USER_TARGETS` | bool / `false` | Accepts plain user accounts as verification subjects. Off by default: the official process verifies a public presence (bot, public channel, public supergroup), and a private account has nothing to check. |
|
||||
| `TELESRV_VERIFICATION_REJECT_COOLDOWN` | duration / `720h` | Wait imposed on an applicant/target pair after a rejection, measured from the decision so a slow review never shortens it. `0` disables it; must be `0..8760h`. |
|
||||
| `TELESRV_VERIFICATION_APPLY_RATE_LIMIT` | int / `3` | Applications one applicant may create per window. `0` disables the budget; must be non-negative. |
|
||||
| `TELESRV_VERIFICATION_APPLY_RATE_WINDOW` | duration / `24h` | Window for the creation budget. Must be positive whenever the limit is set: a positive limit with a zero window is a limiter that never refills. |
|
||||
| `TELESRV_VERIFICATION_BOT_RATE_LIMIT` | int / `30` | `@verifybot` dialog rate per applicant, independent of how many applications are actually created. `0` disables it. |
|
||||
| `TELESRV_VERIFICATION_BOT_RATE_WINDOW` | duration / `1m` | Window for the bot dialog rate; must be positive whenever that limit is set. |
|
||||
| `TELESRV_VERIFICATION_NOTIFY_INTERVAL` | duration / `15s` | Applicant-notification worker interval; must be positive. A decision commits with its outbox row, never with a message send, so delivery is a separate retrying cycle over durable rows. |
|
||||
| `TELESRV_VERIFICATION_NOTIFY_BATCH` | int / `50` | Outbox rows delivered per cycle; must be `1..500`. |
|
||||
| `TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER` | int / `3` | Applications one applicant may keep open (draft, submitted, in review) at once. `0` disables the cap; must be `0..50`. |
|
||||
|
||||
The defaults ship the feature on with the official bar in place, so no existing deployment changes behaviour: user
|
||||
accounts are not accepted, a rejection costs a month, and an applicant can neither flood the queue nor keep an
|
||||
unbounded number of applications open. Every value is validated even when the feature is disabled, so enabling it
|
||||
later is not the moment a typo is discovered.
|
||||
|
||||
### Third-party bot verification
|
||||
|
||||
Third-party verification is the other badge (`botVerification`, projected onto `user.bot_verification_icon`,
|
||||
`channel.bot_verification_icon`, `userFull.bot_verification`, `channelFull.bot_verification`,
|
||||
`chatInvite.bot_verification`, and advertised by `botInfo.verifier_settings`): an outside organisation running a
|
||||
**verifier bot** marks peers with its own icon and description. The operator grants verifier status to a bot; the bot
|
||||
then applies its mark through `bots.setCustomVerification`, or through the application queue its own dialog drives. It is
|
||||
never a second route to the platform checkmark above, and the two mechanisms never read or write each other's state.
|
||||
|
||||
The icon is a custom emoji **document id**, and clients resolve it through `messages.getCustomEmojiDocuments`. An id
|
||||
that names no fetchable custom emoji document therefore renders as *nothing at all*: the badge is invisible, the peer
|
||||
looks unverified, and the only place the mark exists is the database. That is why the icon catalogue is operator-curated
|
||||
and validated against real documents before anything is written — both when an entry is added and when a verifier is
|
||||
granted an icon.
|
||||
|
||||
Every mutation re-derives "may this bot verify right now?" from the stored verifier row, so the per-verifier kill switch
|
||||
takes effect immediately on the RPC path *and* on the review path: an application approved after the switch was flipped
|
||||
is refused rather than granted. A verifier bot may only be driven by itself or by its owner, and an applicant may only
|
||||
file for a peer it controls (bot owner, channel creator, or an administrator carrying `change_info`). Approvals and
|
||||
revocations move the mark inside the store's decision transaction, so an approved application can never exist without
|
||||
its mark.
|
||||
|
||||
| Setting | Type / code default | Description and constraints |
|
||||
|---|---|---|
|
||||
| `TELESRV_BOT_VERIFICATION_ENABLED` | bool / `true` | Enables third-party bot verification. Disabled refuses every mutation explicitly (grants, revocations, applications, catalogue edits) while marks already granted keep projecting: blanking one verifier's badges is what its per-verifier kill switch is for. A deployment that wants the pre-feature wire shape leaves the service unwired instead. |
|
||||
| `TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER` | int / `10000` | Peers one verifier bot may mark. Verifier status is granted per deployment rather than earned per peer, so an unbounded verifier would be an unbounded badge printer. Spent only on a *new* mark — an existing one stays re-describable at the bound. `0` disables the service bound and leaves only the storage bound, which is also the maximum accepted (`10000`). |
|
||||
| `TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT` | int / `5` | Verification applications one applicant may file per window, across all verifier bots. Spent last among the creation checks, so a refused application costs no budget. `0` disables it; must be non-negative. |
|
||||
| `TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW` | duration / `24h` | Window for the application budget. Must be positive whenever the limit is set: a positive limit with a zero window is a limiter that never refills. |
|
||||
|
||||
The application budget is deliberately looser than the official one (`TELESRV_VERIFICATION_APPLY_RATE_LIMIT=3`): a
|
||||
deployment can run several verifier companies, and filing with a second one is not a retry of the first. Both keys are
|
||||
validated even when the feature is disabled, and the per-verifier bound is checked against the storage bound, so a key
|
||||
that cannot do what it says fails startup instead of being silently unreachable.
|
||||
|
||||
## 11. Private calls, group calls, TURN, SFU, and livestream
|
||||
|
||||
| Setting | Type / code default | Description and constraints |
|
||||
|
|
@ -539,6 +665,7 @@ The following fallback keys are accepted from the **process environment only**.
|
|||
| `TELESRV_CALL_RING_TIMEOUT` | duration / `90s` | Server fallback timeout for ringing/accepted private calls; should remain aligned with the client `callRingTimeoutMs`. |
|
||||
| `TELESRV_CALL_TOMBSTONE_TTL` | duration / `60s` | Terminal-call tombstone window for idempotency and late RPC absorption. |
|
||||
| `TELESRV_CALL_MAX_ACTIVE_PER_USER` | int / `4` | Maximum non-terminal private calls per user. Non-positive values are normalized by the phone service. |
|
||||
| `TELESRV_CALL_REGISTRY_MAX_ENTRIES` | int / `10000` | Process-wide private-call registry hard limit. At capacity, new calls fail with `CALL_OCCUPY_FAILED`; established calls are never evicted by age. |
|
||||
| `TELESRV_CALL_SIGNALING_MAX_BYTES` | int bytes / `65536` | Maximum payload for one `phone.sendSignalingData`. |
|
||||
| `TELESRV_CALL_SIGNALING_RATE` | int / `50` | Signaling forwards per call per second; excess is silently dropped. |
|
||||
| `TELESRV_CALL_EXPIRY_INTERVAL` | duration / `1s` | Call-expiry dispatcher polling interval. |
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@
|
|||
| `TELESRV_LISTEN` | string / `0.0.0.0:2398` | MTProto TCP 监听地址,必须与 patched 客户端可达地址/端口一致。 |
|
||||
| `TELESRV_ADVERTISE_IP` | string / `127.0.0.1` | 媒体、通话等回退路径使用的客户端可达 IP;当前 TDesktop 静态 DC patch 不从这里获取 MTProto 地址。 |
|
||||
| `TELESRV_RSA_KEY` | path / `data/server_rsa.pem` | MTProto RSA 私钥;缺失时自动生成。属于敏感文件,重启和升级间必须稳定保存。 |
|
||||
| `TELESRV_DC` | int / `2` | 服务端 DC ID,必须与客户端 patch 及媒体/DC 元数据一致。 |
|
||||
| `TELESRV_DC` | int / `2` | 服务端输出配置及媒体/DC 元数据使用的规范 DC ID;当前单后端不会按它分区密钥交换状态。 |
|
||||
| `TELESRV_DEFAULT_COUNTRY_CODE` | ISO alpha-2 / `CN` | `help.getNearestDc` 返回的登录页默认国家。客户端把 `CN` 映射为国际区号 `+86`、`US` 映射为 `+1`。输入会 trim、转大写并校验为国家或自治地区;格式错误或未知值会让启动失败。 |
|
||||
| `TELESRV_STRICT_DC_CHECK` | bool / `false` | 默认 `false`,永久与临时密钥交换接受任意 wire int32 DC 标签。设为 `true` 时永久标签必须等于 `TELESRV_DC`、临时标签绝对值必须等于 `TELESRV_DC`;它仅是诊断开关,不提供多 DC 隔离。 |
|
||||
| `TELESRV_WEBSOCKET_ENABLE` | bool / `true` | 在 MTProto 监听端口启用 MTProto-over-WebSocket 分流。 |
|
||||
| `TELESRV_WEBSOCKET_ALLOWED_ORIGINS` | list / `http://localhost:1234,http://127.0.0.1:1234` | 浏览器 WebSocket origin 白名单;`*` 只用于临时调试。 |
|
||||
| `TELESRV_MTPROTO_MAX_CONNECTIONS` | int / `200000` | 全局物理连接 admission 上限;负数关闭该门禁。 |
|
||||
|
|
@ -32,20 +34,23 @@
|
|||
| `TELESRV_MTPROTO_RPC_TIMEOUT` | duration / `30s` | 调度后 RPC handler 的端到端超时。 |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_WORKERS` | int / `256` | 共享公平调度器 worker 数。 |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS` | int / `8192` | 进程级排队与执行中的 RPC task 上限。 |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES` | int64 charge bytes / `536870912` | 进程级已预留/排队/执行中 RPC 内存 charge 预算;legacy 等于 copied body,exact 是 typed decode 前按 wire 与生成对象放大计算的保守 materialization charge,不代表可并发接收同等大小的 wire body。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_ENTRIES` | int / `262144` | 331 秒进程内重放窗口中,pending owner、completed `rpc_result` 与容量 tombstone 的全局 ownership 条目上限。owner 执行前先占 1 条,转 completed 时不重复计数。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_BYTES` | int64 bytes / `67108864` | 上述 ownership 的全局 retained-byte 上限;owner 先占 1 byte,Put 转移为真实 body 或 1-byte identity tombstone。不得低于 `16775168`(单条合法 outbound body 上限)。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_ENTRIES` | int / `32768` | 单 raw auth key 的 ownership 条目上限;与全局、session 层同时计费,防一个 auth key 吃满进程缓存。必须 `global >= auth >= session`。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_BYTES` | int64 bytes / `33554432` | 单 raw auth key retained-byte 上限;必须不低于单条合法 outbound body,且满足 byte 层级关系。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_ENTRIES` | int / `16384` | 单 `raw auth key + session_id` ownership 条目上限;不同 session 不共享该局部额度。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_BYTES` | int64 bytes / `16777216` | 单 `raw auth key + session_id` retained-byte 上限;默认略高于单条合法 outbound body,确保空预算时任一合法结果可完整进入。 |
|
||||
| `TELESRV_MTPROTO_RPC_RESULT_PENDING_PER_AUTH` | int / `2048` | 单 raw auth key 的 active pending owner 附加上限;必须不大于 `RPC_GLOBAL_MAX_TASKS` 和 auth entry 上限。Put/Abort 都立即归还此 active 额度。 |
|
||||
| `TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | transport wire 与最大解密明文的进程级在途预算,在分配 payload 前预留。 |
|
||||
| `TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES` | int64 charge bytes / `536870912` | 进程级已预留/排队/执行中 RPC 内存 charge 预算;legacy 等于 copied body,exact 是 typed decode 前按 wire 与生成对象放大计算的保守 materialization charge。nested gzip 展开后会在 decoder 分配 typed graph 前原子增长该 charge,grow 失败原子拒绝整批候选 RPC;该值不代表可并发接收同等大小的 wire body。 |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES` | int / `262144` | pending owner 与未 ACK execution receipt 的全局上限。receipt 只存请求身份、执行结果和 Layer admission 元数据,不存 TL body;收到 `msgs_ack` 立即删除,331 秒仅是无 ACK 时的安全上限。 |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES` | int / `32768` | 单 raw auth key 的 owner/receipt 条目上限;必须满足 `global >= auth >= session`。 |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES` | int / `16384` | 单 `raw auth key + session_id` 的 owner/receipt 条目上限。 |
|
||||
| `TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH` | int / `2048` | 单 raw auth key 的 active pending owner 附加上限;必须不大于 `RPC_GLOBAL_MAX_TASKS` 和 auth entry 上限。 |
|
||||
| `TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | transport wire、最大解密明文以及每个 live outer/nested gzip 输出的进程级在途预算,均在对应 payload 分配前预留。 |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE` | int / `128` | 单连接普通 outbound mailbox 容量。 |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_CONTROL_QUEUE_SIZE` | int / `32` | 单连接控制消息 mailbox 容量。 |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | resend pending message body 的全局预算。 |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | 所有逻辑 session 未 ACK 出站 body 的唯一全局预算。物理连接重连复用同一份 `msg_id/seq_no/body`;ACK、destroy 或离线 6 分钟回收时释放,不再另建 RPC cache/spool 副本。 |
|
||||
| `TELESRV_MTPROTO_OUTBOUND_WRITE_GLOBAL_MAX_BYTES` | int64 bytes / `536870912` | 并发加密 wire/codec/obfuscation scratch 的全局预算。 |
|
||||
|
||||
nested gzip admission 不新增环境变量。代码硬限制为:每个
|
||||
`gzip_packed` envelope 输出最多 10 MiB;同一 transport frame 内 outer、nested、
|
||||
sibling、失败尝试和 authoritative-profile re-decode 的累计解压工作最多 32 MiB。
|
||||
live expanded buffer 释放后会归还进程内存,但不会返还该 frame 的 CPU/work counter;
|
||||
保留的 typed graph 继续计入上面的 RPC scheduler 预算。
|
||||
|
||||
## 3. HTTP 端点、公开链接与管理后台
|
||||
|
||||
| 参数 | 类型 / 代码默认值 | 说明与约束 |
|
||||
|
|
@ -61,7 +66,7 @@
|
|||
| `TELESRV_PUBLIC_BASE_URL` | HTTP(S) URL / `https://telesrv.net` | 客户端可见的公开链接根地址;允许 path,禁止 credentials、query、fragment。本地例:`http://127.0.0.1:2401`。 |
|
||||
| `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | 落地页自动唤起客户端的 scheme,必须与 patched 客户端注册值一致;禁止 `tg`、`http`、`https`。 |
|
||||
| `TELESRV_PUBLIC_APP_LINK_BASE` | nullable custom URL base / 空 | 多服务客户端可选的 host-based 根,例如 `owpg://example.com`。配置后生成 `owpg://example.com/oauth`、`owpg://example.com/<username>` 等;只允许精确 `<custom-scheme>://<host>`,禁止端口、path、query、fragment。`TELESRV_PUBLIC_APP_SCHEME` 仍作为旧链接输入兼容。 |
|
||||
| `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | username 页面 Web 客户端入口,校验规则同 `TELESRV_PUBLIC_BASE_URL`。 |
|
||||
| `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://weba.telesrv.net` | username 页面 Web 客户端入口,校验规则同 `TELESRV_PUBLIC_BASE_URL`。 |
|
||||
| `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | 公开落地页产品名;trim 后非空、无控制字符、最多 64 个 Unicode 字符。 |
|
||||
| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / 空 | 只读 username/avatar/sticker/emoji/chatlist/collectible gift 落地页监听;空值关闭。生产应 loopback + nginx 精确反代;`.env.example` 为开发启用 `127.0.0.1:2401`。 |
|
||||
| `TELESRV_TELEGRAM_LOGIN_ENABLE` | bool / `false` | 在 `TELESRV_PUBLIC_LINK_WEB_ADDR` 上挂载自建 Telegram Login/OIDC Provider;启用时必须同时配置该 listener 与下列全部密钥文件。 |
|
||||
|
|
@ -372,6 +377,7 @@ active key。不要手工编辑 manifest 或 PEM,不要在各实例上分别
|
|||
| `TELESRV_BLOB_DIR` | path / `data/blobs` | 本地开发 blob backend 的媒体字节根目录。 |
|
||||
| `TELESRV_STICKER_SEED_DIR` | path / `data/sticker-seed` | 导入 documents、sticker sets、blob 的贴纸/reaction seed 目录。 |
|
||||
| `TELESRV_STICKER_SEED_MAX_SETS` | int / `300` | 启动时导入的常规贴纸集上限;`<=0` 表示不限。 |
|
||||
| `TELESRV_PREMIUM_PROMO_SEED_DIR` | path / `data/premium-promo` | `help.getPremiumPromo` 导出的 manifest、MP4 视频与 JPEG 缩略图目录。目录缺失时保留无视频兼容响应;目录存在但内容非法或不完整时启动失败。 |
|
||||
|
||||
语言包 seed 以文件 manifest 为事实源。新增语言时放入 `data/langpack/<pack>/<pack>_<lang>_v<version>.strings` 并重启 `telesrv`;`pack` 必须与所在一级目录一致,允许 Telegram 已使用的字母、数字、`-` 与 `_`(例如 `android_x`),`lang` 会统一为小写、连字符形式(例如 `pt_BR` 归一为 `pt-br`)。同一语言存在多个文件时只读取最高版本。修改已有语言的有效内容必须提高版本;同版本有效内容变化或版本倒退会阻止启动。删除语言文件或整个 pack 子目录后,下次重启会原子移除对应数据库目录和字符串。启动先流式计算源文件 SHA-256;未变化文件复用上次原子 manifest,不解析字符串也不写库,只有新增或变化文件才解析并通过 PostgreSQL `COPY` 整包替换。
|
||||
|
||||
|
|
@ -514,6 +520,58 @@ active key。不要手工编辑 manifest 或 PEM,不要在各实例上分别
|
|||
| `TELESRV_STARGIFT_CRAFT_DELAY` | duration / `0s` | 签发时固化到 `can_craft_at` 的等待期;可 Craft 礼物即使为 `0s` 也写升级时间这一正数能力边界,0 只表示不具备 Craft 能力或已终结。 |
|
||||
| `TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE` | int / `250` | 每份输入礼物贡献的本地合成成功概率,累计上限 1000‰。 |
|
||||
|
||||
### 本地账号评分与 collectible username
|
||||
|
||||
账号评分是 gramsrv 自己的本地风控/信誉复合分,组合 Stars 收支、账号活跃和管理处罚;它不承诺 1:1 复刻 Telegram 的私有评分算法。已计算的本地等级会投影到 `userFull.stars_rating`,本人还会收到 `stars_my_pending_rating` 与生效时间,让官方客户端直接显示;他人的 pending 永不下发。资料页只读取后台 worker 已持久化的评分并复用现有 30 分钟 `userFull` 投影缓存,不会同步重算或写库。Collectible username 由管理员签发,本功能不访问外部市场、钱包或区块链节点。
|
||||
|
||||
| 参数 | 类型 / 代码默认值 | 说明与约束 |
|
||||
|---|---|---|
|
||||
| `TELESRV_RATING_ENABLED` | bool / `true` | 启用本地复合评分及客户端等级投影;关闭时拒绝评分写入且客户端 rating flags 保持未设置。 |
|
||||
| `TELESRV_RATING_PENDING_DELAY` | duration / `24h` | 本地评分上涨进入可见后台等级前的等待期;下降立即生效。允许 `0..720h`,`0` 表示立即应用。 |
|
||||
| `TELESRV_RATING_RECOMPUTE_INTERVAL` | duration / `15m` | 后台重算周期,必须为正数。 |
|
||||
| `TELESRV_RATING_RECOMPUTE_BATCH` | int / `500` | 每轮重算的 stale projection 数,必须为 `1..10000`。 |
|
||||
| `TELESRV_RATING_STALE_AFTER` | duration / `6h` | 超过该年龄的评分进入重算,必须为正数。 |
|
||||
| `TELESRV_RATING_WEIGHT_STARS_RECEIVED_PERMILLE` | int64 / `1000` | Stars 收入权重(千分比)。 |
|
||||
| `TELESRV_RATING_WEIGHT_STARS_SPENT_PERMILLE` | int64 / `250` | Stars 支出权重(千分比)。 |
|
||||
| `TELESRV_RATING_WEIGHT_MESSAGE_SENT` | int64 / `1` | 每条已发送消息贡献分。 |
|
||||
| `TELESRV_RATING_WEIGHT_ACCOUNT_AGE_DAY` | int64 / `2` | 每个账号存续日贡献分。 |
|
||||
| `TELESRV_RATING_WEIGHT_GIFT_RECEIVED` | int64 / `25` | 每份持有 collectible gift 贡献分。 |
|
||||
| `TELESRV_RATING_WEIGHT_MODERATION_CASE` | int64 / `150` | 每个成立管理案件的扣分幅度。 |
|
||||
| `TELESRV_RATING_WEIGHT_SCAM_PENALTY` | int64 / `5000` | scam 标记固定扣分。 |
|
||||
| `TELESRV_RATING_WEIGHT_FAKE_PENALTY` | int64 / `5000` | fake 标记固定扣分。 |
|
||||
| `TELESRV_RATING_ACTIVITY_CAP` | int64 / `5000` | 活跃分上限;`0` 表示不封顶。 |
|
||||
| `TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE` | URL template / 空 | 管理员未显式给 URL 时写入资产的落地页模板;空值派生 `<TELESRV_PUBLIC_BASE_URL>/nft/username/<username>`。配置值须为无 userinfo 的绝对 http(s) URL,可含 `{username}`;无占位符时把 username 追加为最后路径段。 |
|
||||
|
||||
`fragment.collectibleInfo.amount` 与 `crypto_amount` 使用币种最小单位:例如 USD 1000 表示 10 美元,TON 900 表示 900 nanotons,XTR 无子单位。后台 UI 负责整币单位与最小单位转换;直接调用 Admin API 的集成必须自行传最小单位。
|
||||
|
||||
### 官方平台认证
|
||||
|
||||
官方认证对应 `user.verified` / `channel.verified`。申请由内置 `@verifybot` 收集、管理后台审核;提交和批准时都会重新校验目标存在、公开 username、申请者控制权、账号状态与系统账号禁入,badge 写入和决定状态在同一事务提交。申请 URL 只做 public http(s) 形状校验,服务端不会抓取,避免 SSRF。
|
||||
|
||||
| 参数 | 类型 / 代码默认值 | 说明与约束 |
|
||||
|---|---|---|
|
||||
| `TELESRV_VERIFICATION_ENABLED` | bool / `true` | 启用官方认证;关闭时拒绝新操作,既有 badge 保留。 |
|
||||
| `TELESRV_VERIFICATION_ALLOW_USER_TARGETS` | bool / `false` | 是否允许普通用户账号成为认证目标。 |
|
||||
| `TELESRV_VERIFICATION_REJECT_COOLDOWN` | duration / `720h` | 同申请者/目标被拒后的等待期;允许 `0..8760h`。 |
|
||||
| `TELESRV_VERIFICATION_APPLY_RATE_LIMIT` | int / `3` | 每个申请者在窗口内可新建的申请数;`0` 关闭。 |
|
||||
| `TELESRV_VERIFICATION_APPLY_RATE_WINDOW` | duration / `24h` | 申请预算窗口;limit>0 时必须为正数。 |
|
||||
| `TELESRV_VERIFICATION_BOT_RATE_LIMIT` | int / `30` | `@verifybot` 每个申请者的对话限流;`0` 关闭。 |
|
||||
| `TELESRV_VERIFICATION_BOT_RATE_WINDOW` | duration / `1m` | bot 对话限流窗口;limit>0 时必须为正数。 |
|
||||
| `TELESRV_VERIFICATION_NOTIFY_INTERVAL` | duration / `15s` | durable 通知 outbox worker 周期,必须为正数。 |
|
||||
| `TELESRV_VERIFICATION_NOTIFY_BATCH` | int / `50` | 每轮投递通知数,必须为 `1..500`。 |
|
||||
| `TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER` | int / `3` | 每个申请者可保持的 active 申请数;允许 `0..50`。 |
|
||||
|
||||
### 第三方 bot 认证
|
||||
|
||||
第三方认证对应 `botVerification`,由管理员授权的 verifier bot 使用自己的 custom emoji document 与描述标记 user/channel,不会授予官方 checkmark。Icon 必须是客户端可通过 `messages.getCustomEmojiDocuments` 读取的真实 document。每个 peer 只保留一个 wire-visible mark;新的 verifier 替换旧 mark,禁止旧 badge 在撤销或 kill switch 后意外复活。客户端自定义描述上限通过 appConfig `bot_verification_description_length_limit=70` 发布。
|
||||
|
||||
| 参数 | 类型 / 代码默认值 | 说明与约束 |
|
||||
|---|---|---|
|
||||
| `TELESRV_BOT_VERIFICATION_ENABLED` | bool / `true` | 启用第三方认证;关闭时拒绝新 mutation,既有 mark 仍投影。 |
|
||||
| `TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER` | int / `10000` | 单 verifier 可标记的 peer 数;`0` 仅关闭 service cap,storage 硬上限仍为 10000。 |
|
||||
| `TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT` | int / `5` | 每个申请者跨 verifier 的申请预算;`0` 关闭。 |
|
||||
| `TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW` | duration / `24h` | 第三方认证申请预算窗口;limit>0 时必须为正数。 |
|
||||
|
||||
## 11. 私聊通话、群通话、TURN、SFU 与直播
|
||||
|
||||
| 参数 | 类型 / 代码默认值 | 说明与约束 |
|
||||
|
|
@ -521,6 +579,7 @@ active key。不要手工编辑 manifest 或 PEM,不要在各实例上分别
|
|||
| `TELESRV_CALL_RING_TIMEOUT` | duration / `90s` | 私聊通话 ringing/accepted 服务端兜底超时,应与客户端 `callRingTimeoutMs` 保持一致。 |
|
||||
| `TELESRV_CALL_TOMBSTONE_TTL` | duration / `60s` | 终态通话 tombstone 的幂等/晚到 RPC 吸收窗口。 |
|
||||
| `TELESRV_CALL_MAX_ACTIVE_PER_USER` | int / `4` | 单用户非终态私聊通话上限;非正值由 phone service 归一。 |
|
||||
| `TELESRV_CALL_REGISTRY_MAX_ENTRIES` | int / `10000` | 进程级私聊通话 registry 硬上限;满载返回 `CALL_OCCUPY_FAILED`,不按年龄驱逐已建立通话。 |
|
||||
| `TELESRV_CALL_SIGNALING_MAX_BYTES` | int bytes / `65536` | 单条 `phone.sendSignalingData` 载荷上限。 |
|
||||
| `TELESRV_CALL_SIGNALING_RATE` | int / `50` | 单通话每秒信令转发上限,超限静默丢弃。 |
|
||||
| `TELESRV_CALL_EXPIRY_INTERVAL` | duration / `1s` | 通话 expiry dispatcher 轮询间隔。 |
|
||||
|
|
|
|||
291
docs/verification.md
Normal file
291
docs/verification.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
# Official platform verification
|
||||
|
||||
Official verification is the platform badge shown next to the name of a bot,
|
||||
public channel or public supergroup whose identity a human reviewer has
|
||||
confirmed. In this server it is exactly one boolean on one peer record:
|
||||
`users.verified` or `channels.verified`. Nothing else about the peer changes.
|
||||
|
||||
Applications are filed through the built-in `@verifybot`, decided in the admin
|
||||
panel, and the decision commits together with the flag write. The protocol edge
|
||||
then makes the new flag observable: it drops the cached peer projections and
|
||||
pushes the ordinary peer-refresh update to online clients.
|
||||
|
||||
## What this is not
|
||||
|
||||
The badge here is the **platform** flag: `user#b1b8cc83 verified:flags.17` and
|
||||
`channel#d49f34c6 verified:flags.7`.
|
||||
|
||||
Telegram also has a second, unrelated mechanism — **third-party bot
|
||||
verification** — where an ordinary bot that a platform operator has appointed as
|
||||
a "verifier" attaches its own custom icon and description to arbitrary peers.
|
||||
That is `botVerification#f93cd45c`, `botVerifierSettings#b0cd6617`,
|
||||
`bots.setCustomVerification#8b89dfbd`, `user.bot_verification_icon:flags2.14?long`,
|
||||
`channel.bot_verification_icon:flags2.13?long` and
|
||||
`channelFull.bot_verification:flags2.17?BotVerification`.
|
||||
|
||||
The two are deliberately kept apart:
|
||||
|
||||
- an approval in this flow never writes `bot_verification*`, and never issues
|
||||
`botVerifierSettings` to anybody;
|
||||
- `bots.setCustomVerification` is routed and argument-checked at the RPC edge and
|
||||
then refused with `403 BOT_VERIFIER_FORBIDDEN`
|
||||
(`internal/rpc/bots_longtail.go`), because no bot on this deployment is a
|
||||
verifier;
|
||||
- a client that renders `bot_verification_icon` renders it from data this flow
|
||||
never produces, so a third-party icon can neither stand in for the platform
|
||||
badge nor be shadowed by it.
|
||||
|
||||
## TL constructors and flags (Layer 228)
|
||||
|
||||
Checked against the schema snapshot the server is built for,
|
||||
`/tmp/td/_schema/layers/layer-228.tl`.
|
||||
|
||||
Platform verification:
|
||||
|
||||
| Constructor | Field |
|
||||
| --- | --- |
|
||||
| `user#b1b8cc83` | `verified:flags.17?true` |
|
||||
| `channel#d49f34c6` | `verified:flags.7?true` |
|
||||
| `chatInvite#5c9d3702` | `verified:flags.7?true`, `scam:flags.8?true`, `fake:flags.9?true` |
|
||||
|
||||
`chatInviteAlready#5a686d7c` carries a whole `Chat`, so a member's preview gets
|
||||
the badge through `channel#d49f34c6` rather than through invite-level flags.
|
||||
|
||||
Third-party bot verification, for contrast — none of these are written by this
|
||||
flow:
|
||||
|
||||
| Constructor / method | Field |
|
||||
| --- | --- |
|
||||
| `botVerification#f93cd45c` | `bot_id:long icon:long description:string` |
|
||||
| `botVerifierSettings#b0cd6617` | `icon:long company:string custom_description:flags.0?string` |
|
||||
| `bots.setCustomVerification#8b89dfbd` | `enabled:flags.1?true bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?string` |
|
||||
| `user#b1b8cc83` | `bot_verification_icon:flags2.14?long` |
|
||||
| `channel#d49f34c6` | `bot_verification_icon:flags2.13?long` |
|
||||
| `channelFull#a04e8d3a` | `bot_verification:flags2.17?BotVerification` |
|
||||
|
||||
`chatInvite#5c9d3702` also has `bot_verification:flags.13?BotVerification`; it is
|
||||
never populated, for the same reason.
|
||||
|
||||
## End-to-end path
|
||||
|
||||
1. **`@verifybot`** (user id `1250000011`, seeded by migration `0152`) collects
|
||||
the application in a step-by-step dialog: subject, category, description,
|
||||
official website, optional social links, independent press links, optional
|
||||
note. Commands are `/new`, `/status`, `/cancel`, `/help`.
|
||||
2. **Eligibility, first gate.** The subject must be a bot, public channel or
|
||||
public supergroup with a public `@username`, created or administered by the
|
||||
applicant, not a built-in system entity, not already verified, and not
|
||||
scam/fake/frozen/deleted. Per-applicant rate limits, an open-application cap
|
||||
and a post-rejection cooldown also apply.
|
||||
3. **Submission** writes `verification_applications` (status `submitted`) and an
|
||||
immutable `verification_application_events` row.
|
||||
4. **Admin panel** lists the queue and lets a reviewer claim, approve or reject.
|
||||
Panel BFF routes are under `/api/verification/...`; the Admin API routes are
|
||||
`GET /v1/verification/applications`, `.../{id}`, `.../counts`,
|
||||
`POST .../{id}/claim|approve|reject` and `POST /v1/verification/revoke`.
|
||||
Every decision also goes through the shared admin command journal, so it lands
|
||||
in `admin_commands` / `admin_audit_logs`.
|
||||
5. **Eligibility, second gate.** At approval time the target is re-loaded and
|
||||
re-evaluated against a fresh snapshot, so a target that turned scam, lost its
|
||||
username, was frozen or got verified by another route between filing and
|
||||
review is refused. The review queue cannot launder a state the submission path
|
||||
forbids.
|
||||
6. **Decision transaction.** The status transition, the audit event, the
|
||||
applicant-notification outbox row and the `verified` flag write on the peer
|
||||
commit in one transaction (`verification.PeerVerifier` is invoked with the
|
||||
store transaction taken from the context). "Approved" and "the peer carries
|
||||
the badge" can never disagree.
|
||||
7. **Protocol edge.** After the commit the service calls
|
||||
`rpc.Router.NotifyPeerVerified(ctx, domain.Peer)`
|
||||
(`internal/rpc/verification_notify.go`), which:
|
||||
- drops the cached peer projections for the target
|
||||
(`invalidateRPCProjectionForUser` / `invalidateRPCProjectionForChannel`);
|
||||
- for a user or bot, reuses `NotifyUserModerationFlagsChanged` — the same
|
||||
audience-wide, non-PTS `updateUser` fan-out the scam/fake flags use. The
|
||||
audience is `ModerationFlagAudience` (accounts that already see the peer),
|
||||
filtered to the ones currently online; each recipient gets the peer
|
||||
re-projected for itself;
|
||||
- for a channel, reuses `NotifyChannelChanged` →
|
||||
`channelStateMutationUpdates` → `pushChannelStateToMembersWithLinkedMonoforum`,
|
||||
i.e. `updateChannel` plus the refreshed `channel#d49f34c6` object to the
|
||||
channel's members (and the linked monoforum when there is one);
|
||||
- reports a clear error instead of panicking when the peer cannot be resolved,
|
||||
and is a no-op on a nil receiver. A push failure never invalidates the
|
||||
committed decision; the caller logs it and moves on.
|
||||
8. **Applicant notification.** `@verifybot` messages the applicant from a durable
|
||||
outbox drained by a retrying worker, never from inside the decision
|
||||
transaction. Kinds are `approved`, `rejected`, `revoked`.
|
||||
9. **What the client sees.** An online client applies the flag from the pushed
|
||||
`User`/`Channel` object immediately: the badge appears in the dialog list,
|
||||
profile, search results and message headers without a restart. An offline
|
||||
client converges on reconnect — see below.
|
||||
|
||||
## Offline convergence
|
||||
|
||||
`updateUser` and `updateChannel` carry no `pts`, so they are not stored as
|
||||
message-box events and are not replayed by `updates.getDifference`. Offline
|
||||
sessions converge because `verified` is part of the peer's **base read model**,
|
||||
whose version is bumped by the triggers shipped in `0001_init`:
|
||||
|
||||
- `users.verified` is listed in `telesrv_notify_user_base_read_model` (trigger
|
||||
`users_read_model_changed`), which bumps `user_base`, `contact_account` and the
|
||||
private dialog-light models, and in
|
||||
`telesrv_notify_user_channel_participants_read_model` (trigger
|
||||
`users_channel_participants_read_model_changed`), which bumps
|
||||
`channel_participants`;
|
||||
- `channels` bumps `channel_base` on every row change (trigger
|
||||
`channels_read_model_changed`) and additionally fires
|
||||
`pg_notify('telesrv_channel_changed')`.
|
||||
|
||||
The `user_base` notification is consumed by the read-model listener
|
||||
(`internal/store/postgres/read_model_listener.go`), which invalidates the RPC
|
||||
projections **and** the shared Redis `user:base` row across instances. So any
|
||||
later authoritative read — `users.getUsers`, `users.getFullUser`,
|
||||
`channels.getChannels`, `channels.getFullChannel`, `messages.getDialogs`, or the
|
||||
`users`/`chats` vectors attached to a `getDifference` answer — already carries the
|
||||
new flag. No migration is needed for this, and none was added.
|
||||
|
||||
## Where the flag surfaces
|
||||
|
||||
`verified` is projected wherever a `User` or `Channel` object is projected, which
|
||||
is every one of these:
|
||||
|
||||
| Surface | Method | Constructor |
|
||||
| --- | --- | --- |
|
||||
| Dialog list | `messages.getDialogs`, `messages.getPeerDialogs` | `user`, `channel` in `users`/`chats` |
|
||||
| Search | `contacts.search`, `contacts.resolveUsername`, `messages.searchGlobal`, `channels.getAdminedPublicChannels` | `user`, `channel` |
|
||||
| Profile | `users.getUsers`, `users.getFullUser` | `user` in `users.userFull.users` |
|
||||
| Channel info | `channels.getChannels`, `channels.getFullChannel` | `channel` in `chats` |
|
||||
| Message history | `messages.getHistory`, `messages.getMessages`, channel history | `user`, `channel` in `users`/`chats` |
|
||||
| Invite preview | `messages.checkChatInvite` | `chatInvite` (`verified:flags.7`) or `chatInviteAlready.chat` |
|
||||
| Live updates | pushed `updates` envelopes | `updateUser` / `updateChannel` plus the peer object |
|
||||
| Difference | `updates.getDifference`, `updates.getChannelDifference` | `user`, `channel` in `users`/`chats` |
|
||||
|
||||
The invite preview is the one that used to be missing: before, a non-member saw
|
||||
an unbadged preview and the badge only appeared after joining. It is now set from
|
||||
the persistent channel record in `internal/rpc/channels_invites.go`, through the
|
||||
generated `Set*` helpers so the `flags` word and the struct field stay in step,
|
||||
and left entirely unset for an unflagged peer.
|
||||
|
||||
## Migrations
|
||||
|
||||
- **`0153_verify_service_bot`** seeds `@verifybot` (id `1250000011`, fixed
|
||||
`access_hash` double-written with `domain.VerifyBotAccessHash`), its `bots` row
|
||||
and command list, and its `peer_usernames` registry entry. The handle is
|
||||
occupied from the moment the schema is current, so an ordinary user cannot claim
|
||||
`@verifybot` in the window before first use.
|
||||
- **`0154_verification_applications`** creates
|
||||
`verification_applications` (the durable audit subject, never deleted, moved
|
||||
through `draft → submitted → in_review → approved|rejected|cancelled` under an
|
||||
optimistic-locking `version`), the append-only
|
||||
`verification_application_events` history, and
|
||||
`verification_notification_outbox` with
|
||||
`UNIQUE (application_id, kind)` so a repeated approve delivers one message.
|
||||
Partial unique indexes enforce one live application per target and one draft per
|
||||
applicant.
|
||||
|
||||
Neither migration touches the `verified` columns or the read-model triggers:
|
||||
`users.verified` and `channels.verified` already existed and were already covered.
|
||||
|
||||
## Configuration
|
||||
|
||||
Verification (`internal/config/config.go`, `.env.example`):
|
||||
|
||||
| Key | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `TELESRV_VERIFICATION_ENABLED` | `true` | Master switch. When off every use case refuses explicitly; peers already badged keep the badge. |
|
||||
| `TELESRV_VERIFICATION_ALLOW_USER_TARGETS` | `false` | Whether plain user accounts may be subjects. |
|
||||
| `TELESRV_VERIFICATION_REJECT_COOLDOWN` | `720h` | Wait before re-filing the same target after a rejection, measured from the decision. `0` disables; max `8760h`. |
|
||||
| `TELESRV_VERIFICATION_APPLY_RATE_LIMIT` | `3` | Applications one applicant may create per window. `0` disables. |
|
||||
| `TELESRV_VERIFICATION_APPLY_RATE_WINDOW` | `24h` | That window. |
|
||||
| `TELESRV_VERIFICATION_BOT_RATE_LIMIT` | `30` | `@verifybot` dialog rate per applicant, independent of applications created. `0` disables. |
|
||||
| `TELESRV_VERIFICATION_BOT_RATE_WINDOW` | `1m` | That window. |
|
||||
| `TELESRV_VERIFICATION_NOTIFY_INTERVAL` | `15s` | Applicant-notification worker cadence. Must be positive. |
|
||||
| `TELESRV_VERIFICATION_NOTIFY_BATCH` | `50` | Rows per cycle, `1..500`. |
|
||||
| `TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER` | `3` | Open applications per applicant. `0` disables, max `50`. |
|
||||
|
||||
Reviewer access:
|
||||
|
||||
| Key | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `TELESRV_ADMIN_UI_PERMISSIONS` | `*` | Permissions of an Admin UI session. Reviewing needs `verification.review`; clearing an existing badge needs `verification.revoke` on top of it. |
|
||||
| `TELESRV_ADMIN_SCOPED_TOKENS` | *(empty)* | `name:token:perm1,perm2` entries separated by `;`, for Admin API integrations that should get `verification.review` and nothing else. |
|
||||
|
||||
## Manual check with official Telegram Desktop
|
||||
|
||||
1. Log in to the deployment with official Telegram Desktop.
|
||||
2. Open `@verifybot` — it resolves by username and its own profile already shows
|
||||
the badge (the seed row is `verified`).
|
||||
3. Send `/new`, pick the subject from the inline picker, and answer the steps:
|
||||
category, description, official website, social links (or *Skip*), at least the
|
||||
required number of independent press links, optional note. Press
|
||||
*Submit application*.
|
||||
4. Send `/status`; the application is listed as submitted.
|
||||
5. In the admin panel open the verification queue, claim the application and
|
||||
approve it. Confirm the audit entry appeared.
|
||||
6. Within one notification-worker interval `@verifybot` messages the applicant
|
||||
with the decision.
|
||||
7. Without restarting the client, check the badge on the approved subject:
|
||||
- **Profile** — open the peer's profile; the badge sits next to the title.
|
||||
- **Search** — type the `@username` in global search; the result row is badged.
|
||||
- **Dialog list** — the chat row in the main list is badged.
|
||||
- **Message header** — open the chat; the header title is badged.
|
||||
- **Invite preview** — from a *second* account that is **not** a member, open
|
||||
an invite link to the approved channel. The join-confirmation box is badged
|
||||
before joining. (This is the `chatInvite#5c9d3702 verified:flags.7` path.)
|
||||
8. To check offline convergence, quit the client before approving, approve, then
|
||||
start it again: the badge is present on the first read, delivered by
|
||||
`getDifference` and the peer reads it triggers rather than by a live push.
|
||||
9. Revoking from the panel takes the badge away by the same route.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Plain user accounts are off by default.** With
|
||||
`TELESRV_VERIFICATION_ALLOW_USER_TARGETS=false` (the shipped default) an
|
||||
application whose subject is an ordinary account is refused
|
||||
(`ErrVerificationUserTargetsDisabled`). Turning it on does not add any extra
|
||||
identity checks; it only stops refusing the target type.
|
||||
- **Third-party bot verification is not part of this mechanism and is not
|
||||
implemented.** `bots.setCustomVerification` never succeeds: it validates its
|
||||
arguments and then refuses with `403 BOT_VERIFIER_FORBIDDEN`. No
|
||||
`botVerifierSettings` are issued, and
|
||||
`user.bot_verification_icon` / `channel.bot_verification_icon` /
|
||||
`channelFull.bot_verification` / `chatInvite.bot_verification` are never
|
||||
populated. A client asking for a custom verifier icon gets nothing.
|
||||
- **The badge has no attributes.** It is a single boolean: no verifier company, no
|
||||
custom description, no per-peer icon, no expiry, no verification tier. There is
|
||||
nothing in the TL surface to carry them for the platform flag.
|
||||
- **Built-in system entities cannot be applied for.** Service accounts are refused
|
||||
with `ErrVerificationTargetSystem`; their badge is set by the seed migrations.
|
||||
- **A subject with no public `@username` is refused**
|
||||
(`ErrVerificationTargetNotPublic`), so private channels and usernameless bots
|
||||
cannot be verified at all — not even by an operator using the panel's revoke
|
||||
route in reverse.
|
||||
- **Legacy basic groups (`chat#…`) have no `verified` field** in Layer 228, so a
|
||||
non-migrated basic group can never show a badge regardless of what is stored.
|
||||
- **The live push only reaches online sessions.** `NotifyPeerVerified` filters the
|
||||
audience by the online index; everybody else converges on their next
|
||||
authoritative read. The user fan-out is additionally bounded (the moderation
|
||||
audience is capped, currently at 4096 accounts) and the channel fan-out is
|
||||
bounded by the online-member index, so on a very large peer some sessions get
|
||||
the flag from their next read rather than from a push.
|
||||
- **`updateUser` / `updateChannel` carry no `pts`.** They are not persisted as
|
||||
message-box events, so a session that was offline during the decision never
|
||||
replays the push itself; it depends on the read-model bump. That is by design
|
||||
(a badge change is not a message), but it means the badge is not guaranteed to
|
||||
arrive as an *event* — only as *state*.
|
||||
- **The live push can be one beat behind the shared base-user cache.** The
|
||||
decision writes the user row inside the verification transaction, bypassing the
|
||||
`users` service and therefore its Redis `user:base` refresh; that cache is
|
||||
dropped cross-instance by the asynchronous `user_base` read-model
|
||||
notification. `NotifyPeerVerified` runs synchronously right after commit, so in
|
||||
the small window before the listener processes the event the pushed `user`
|
||||
object can still carry the pre-decision flag. The persisted state is always
|
||||
correct, the projections are always invalidated, and the client repairs itself
|
||||
on the next read, so this shows up at worst as a badge that appears a moment
|
||||
late rather than instantly. The channel path is not affected: the
|
||||
transaction-scoped channel store is handed the channel row cache and drops it
|
||||
on the flag write.
|
||||
- **Nothing re-checks a verified peer over time.** Once badged, a peer keeps the
|
||||
badge until an operator revokes it. There is no periodic re-validation, and
|
||||
losing the username or picking up a scam flag later does not clear it.
|
||||
Loading…
Add table
Add a link
Reference in a new issue