diff --git a/docs/deep-links.md b/docs/deep-links.md new file mode 100644 index 00000000..3464d01a --- /dev/null +++ b/docs/deep-links.md @@ -0,0 +1,119 @@ +# Client deep links + +The desktop client (`owpengram-desktop-client`, a Telegram Desktop fork) resolves +`tg://` URIs and `https://t.me/` links through the same handler table: +`Telegram/SourceFiles/core/local_url_handlers.cpp`. Both schemes are normalized to the +same `?` form before dispatch, so every entry below works as either +`tg://join?invite=...` or `t.me/join/...` (the client also accepts the shorter +`t.me/+...` / `t.me/joinchat/...` aliases, which get rewritten to `join?invite=` +before reaching this table). + +Patterns are matched in order top to bottom; the first regex match wins. + +## Path handlers + +| Path | Handler | Purpose | +|---|---|---| +| `join?invite=` | `JoinGroupByHash` | Join a chat/channel via invite link hash. | +| `addlist?slug=` | `JoinFilterBySlug` | Import a shareable chat folder. | +| `addstickers?set=` / `addemoji?set=` | `ShowStickerSet` | Open a sticker or custom-emoji set for install. | +| `addtheme?slug=` | `ShowTheme` | Open a shared theme for install. | +| `addstyle?slug=` | `ShowAiStyle` | Open a shared AI compose style. | +| `setlanguage?lang=` | `SetLanguage` | Switch/install the app's interface language. | +| `msg_url?` | `ShareUrl` | Open the "share to chat" composer for an external URL. | +| `confirmphone?` | `ConfirmPhone` | Confirm a phone number transfer/login code sent by SMS. | +| `socks?` | `ApplySocksProxy` | Add/apply a SOCKS5 proxy config. | +| `proxy?` | `ApplyMtprotoProxy` | Add/apply an MTProto proxy config. | +| `webproxy?` | `ApplyWebProxy` | Add/apply a web proxy config. | +| `passport?` | `ShowPassport` | Telegram Passport authorization request. | +| `bg?` | `ShowWallPaper` | Open a shared chat wallpaper for install. | +| `resolve?` | `ResolveUsernameOrPhone` | Resolve `@username`/phone to a peer; see [resolve parameters](#resolve-parameters) below. This is the most common deep link kind. | +| `privatepost?` | `ResolvePrivatePost` | Open a specific message inside a private channel by internal channel id + message id. | +| `test_chat_theme?` | `ResolveTestChatTheme` | Preview a chat theme without installing it. | +| `invoice?` | `ResolveInvoice` | Open a payment invoice (Telegram Payments / Stars). | +| `premium_offer[?]` | `ResolvePremiumOffer` | Open the Premium subscription offer screen. | +| `premium_multigift?` | `ResolvePremiumMultigift` | Open the bulk/multi-recipient Premium gifting screen. | +| `login?code=` | `ResolveLoginCode` | Apply a login confirmation code (QR/code login flow). | +| `boost?` | `ResolveBoost` | Open the "boost this channel" flow. | +| `message?slug=` | `ResolveChatLink` | Open a pre-composed shareable chat/business link. | +| `stars_topup?` | `ResolveTopUp` | Open the Telegram Stars top-up screen. | +| `nft?slug=` | `ResolveUniqueGift` | Open a collectible/unique gift by slug. | +| `stargift_auction?slug=` | `ResolveGiftAuction` | Open a Star Gift auction listing. | +| `call?slug=` | `ResolveConferenceCall` | Join a group/conference call by invite slug. | +| `stars[?]` | `ResolveStarsSettings` | Open Telegram Stars balance/settings. | +| `ton[?]` / `grams[?]` | `ResolveTonSettings` | Open TON wallet settings. | +| `oauth?` | `ResolveOAuth` | Third-party OAuth authorization via Telegram. | +| *(anything else)* | `HandleUnknown` | Fallback — shown as an unrecognized-link message. | + +## Resolve parameters + +`resolve?domain=` (or the bare `t.me/` shorthand, which the +client rewrites to this form) accepts additional query parameters that change what +opens once the peer is resolved: + +| Parameter | Effect | +|---|---| +| `domain` | The `@username` to resolve (required unless using the bare `t.me/` form). | +| `phone` | Resolve by phone number instead of username. | +| `appname` | Open a Telegram Mini App by name instead of the peer's chat. | +| `startapp` | Start-parameter passed into a Mini App launch. | +| `attach` | Open the target's attach-menu bot alongside the chat. | +| `start` | Start-parameter for a bot (`/start `). | +| `startgroup` | Add a bot to a group with this start token (bot-in-group flow). | +| `startchannel` | Add a bot to a channel with this start token. | +| `post` | Jump to a specific message id in the resolved peer. | +| `story` | Open a specific story id from the resolved peer. | +| `comment` | Jump to a specific comment/reply message id (discussion groups). | +| `topic` | Open a specific forum topic id. | +| `thread` | Open a specific reply thread/topic id (older alias of `topic`). | +| `game` | Launch a specific inline game short name from a bot. | +| `text` | Pre-fill the message composer with this text. | +| `voicechat` / `videochat` / `livestream` | Join the peer's active voice chat / video chat / live stream by hash. | + +## Internal-only links + +`InternalUrlHandlers()` in the same file (`internal:`) covers links the app +generates for its own in-app UI (e.g. `internal:edit_peer/`, +`internal:common_groups/`, `internal:collectible_username/@`). These +are not meant to be shared externally and are not part of the public deep-link +surface — they only work as links the client itself produces inside its own UI. + +## OwpenGram (`owpg://`) links + +This fork adds a second scheme, handled in `Telegram/SourceFiles/core/application.cpp` +(`Application::openLocalUrl` / `Application::openOwpengramUrl`), for operating across +multiple OwpenGram-compatible backends rather than only the official Telegram DCs. + +| Form | Purpose | +|---|---| +| `owpg://addserver?host=&port=` | Opens `AddServerBox` pre-filled with just the address. **Deliberately carries nothing else** — no name, description, RSA key, or DC id. | +| `owpg://oauth?` / `owpg://resolve?` | Rewritten to the equivalent `tg://oauth` / `tg://resolve` command and dispatched on the current session — lets those two flows work on an OwpenGram account, not just official Telegram. | +| `owpg:///` | Routes a normal `t.me`-style path (`` = `+`, ``, etc.) to whichever logged-in account is on that server's ``, bypassing the official-Telegram-only guard. If no account on the device is logged into ``, the client shows an inform box naming the server instead of silently failing. | + +### Why `addserver` only takes host+port + +The RSA public key, main DC id, display name, and description are **never** accepted +from the link itself — see the comment above `kOwpgAddServerRe` in `application.cpp`. +Letting a link author supply the key/identity directly would be a MITM vector: whoever +crafts the link would control what the victim ends up trusting as that server's +identity, regardless of who actually operates the host. Only host+port travel in the +link; everything else is fetched live and is exactly as trustworthy as the TCP +connection to that host:port already is. + +Once `AddServerBox` has host+port, it calls `Owpengram::FetchServerInfo(host, port, …)`, +which does a plain HTTP `GET` against the server's own well-known endpoints on the +same port: + +| Endpoint | Returns | +|---|---| +| `GET :/owpengram/server-info` | RSA public key (PEM), main DC id, and optional operator-set name/description/icon-presence flag. | +| `GET :/owpengram/server-icon` | Raw icon bytes, only fetched when `server-info` reported `hasIcon: true`. | + +`RefreshServersInfo()` re-polls `server-info`/`server-icon` for every already-added +custom server to keep a cosmetic name/icon change in sync — it never touches host, +port, RSA key, DC id, or the server's local id, for the same MITM reason as above (see +the doc comment on `RefreshServersInfo` in `owpengram_servers.h`). + +This also means the RSA-key-in-a-URL scheme discussed earlier in this doc's history +is intentionally **not** how this client adds a server — that was a plausible-looking +design that this project already considered and rejected in favor of fetch-after-add. diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 00000000..949123b5 --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,3628 @@ +openapi: 3.1.0 +info: + title: owpengram-server Admin APIs + version: "1.1.0" + description: | + Two independently authenticated HTTP admin surfaces, run as **separate + listeners in the same process** (`cmd/telesrv-admin`): + + 1. **Admin API** (`internal/adminapi`) — a bearer-token JSON API under + `/v1/*`, listening on `TELESRV_ADMIN_API_ADDR` + (default `127.0.0.1:2599`). Closed unless configured + (`TELESRV_ADMIN_API_ADDR` empty ⇒ off). This is the raw write surface. + 2. **Panel API** (`cmd/telesrv-admin`) — the cookie/CSRF-session JSON API + under `/api/*` behind the built admin SPA, listening on + `TELESRV_ADMIN_UI_ADDR` (default `127.0.0.1:2600`). Most reads go + straight to Postgres; every `/api/actions/*` mutation (plus + moderation and verification/bot-verification decisions) is relayed + through to the Admin API on your behalf, bearer-authenticated with + `TELESRV_ADMIN_API_TOKEN` — the panel itself only writes local state + directly for admin-operator accounts and server settings/identity. + + Both APIs' `POST` routes return a **command result** + (`CommandResult` — shared by both surfaces, same Go type). Panel API + action routes wrap a relayed Admin API call, so most of them can also + surface a `502` if that upstream call itself fails — except the + verification/bot-verification decision routes, which preserve the + upstream's real status (404/409/429/400) instead of flattening to 502. + + Panel API permissions are granular (`accounts.manage`, + `channels.read`, `content.manage`, …) — see each operation's + description for the exact permission it requires; `*` (only ever held + by the master/break-glass "owpengram" account or a token explicitly + granted it) satisfies every check. + + Built entirely from reading source directly (no prose doc was used or + trusted for either surface — an earlier version of this spec was + generated from `docs/admin-panel-api.en.md` and found to document + several routes, like `grant-stars`, that don't exist, while missing + ~40 real ones and understating the permission model; that doc should + be considered unreliable going forward): + `internal/adminapi/{server,rbac,verification,botverification}.go` and + `cmd/telesrv-admin/{server,security,session,adminauth,adminusers_api, + verification,botverification,serversettings}.go`. + contact: + name: owpengram-server + url: https://git.zio.sh/astra/owpengram + +servers: + - url: http://127.0.0.1:2599 + description: Admin API (internal/adminapi) — bearer token, TELESRV_ADMIN_API_ADDR + - url: http://127.0.0.1:2600 + description: Panel API (cmd/telesrv-admin) — cookie session + CSRF, TELESRV_ADMIN_UI_ADDR + +security: [] + +tags: + - { name: "Panel: Auth", description: "Panel API — login/logout/session/public branding" } + - { name: "Panel: Admin Operators", description: "Panel API — named admin_console_users accounts (requires admins.manage)" } + - { name: "Panel: Dashboard", description: "Panel API — dashboard summary (requires dashboard.read)" } + - { name: "Panel: Accounts", description: "Panel API — account browsing + actions (requires accounts.read / accounts.manage)" } + - { name: "Panel: Broadcasts", description: "Panel API — broadcast browsing + actions (requires broadcasts.read / broadcasts.send)" } + - { name: "Panel: Channels", description: "Panel API — channel/supergroup browsing + actions (requires channels.read / channels.manage)" } + - { name: "Panel: Bots", description: "Panel API — bot browsing + actions (requires bots.read / bots.manage / bots.token.read)" } + - { name: "Panel: Messages", description: "Panel API — message audit + deletion actions (requires messages.read / messages.manage)" } + - { name: "Panel: Content", description: "Panel API — custom emoji, sticker sets, GIF catalog (requires content.read / content.manage)" } + - { name: "Panel: Storage", description: "Panel API — storage stats + manual purge (requires storage.read / storage.manage)" } + - { name: "Panel: Moderation", description: "Panel API — moderation case queue, proxied to the Admin API (requires moderation.review)" } + - { name: "Panel: Usernames", description: "Panel API — reserved + collectible username lifecycle (requires usernames.read / usernames.manage)" } + - { name: "Panel: Official Verification", description: "Panel API — official verification queue (requires verification.review, revoke also needs verification.revoke)" } + - { name: "Panel: Bot Verification", description: "Panel API — third-party bot verification queue, hidden by default (requires botverification.review / botverification.manage)" } + - { name: "Panel: Server Settings", description: "Panel API — server identity, env, updates, restart (requires server.manage)" } + - { name: "AdminAPI: Accounts", description: "Admin API — account write actions" } + - { name: "AdminAPI: Channels", description: "Admin API — channel write actions" } + - { name: "AdminAPI: Bots & Broadcasts", description: "Admin API — bot/broadcast write actions" } + - { name: "AdminAPI: Messages", description: "Admin API — message deletion actions" } + - { name: "AdminAPI: Stickers", description: "Admin API — sticker set write actions" } + - { name: "AdminAPI: GIF Catalog", description: "Admin API — GIF catalog write actions" } + - { name: "AdminAPI: Storage", description: "Admin API — storage purge" } + - { name: "AdminAPI: Moderation", description: "Admin API — moderation case queue (raw)" } + - { name: "AdminAPI: Collectible Usernames", description: "Admin API — collectible username lifecycle" } + - { name: "AdminAPI: Reserved Usernames", description: "Admin API — operator username blocklist" } + - { name: "AdminAPI: Official Verification", description: "Admin API — official verification queue (raw)" } + - { name: "AdminAPI: Bot Verification", description: "Admin API — third-party bot verification queue (raw)" } + - { name: "AdminAPI: Health", description: "Admin API — unauthenticated health check" } + +paths: + # ============================================================ + # PANEL API (cmd/telesrv-admin) — cookie session + CSRF + # ============================================================ + + # --- Auth / session --- + + /api/login: + post: + tags: ["Panel: Auth"] + summary: Log in + security: [] + description: >- + Issues the signed telesrv_admin_session cookie and the + telesrv_admin_csrf cookie. The only mutating panel route without + CSRF (no session exists yet) — only the Origin header is checked. + `username` is always required: either the literal break-glass + username `"owpengram"` (checked against TELESRV_ADMIN_UI_PASSWORD/ + _TOKEN — reserved, no DB account may take it), or a named account + in `admin_console_users`. Every failure mode (unknown user, wrong + password, disabled account) returns the same 401 with the same + generic message, and an unknown-user attempt still burns a dummy + bcrypt compare, so failure modes cost the same time. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [username, secret] + properties: + username: { type: string, description: "Break-glass username (\"owpengram\"), or a named admin_console_users account." } + secret: { type: string } + example: { username: "owpengram", secret: "letmein" } + responses: + "200": { description: OK, headers: { Set-Cookie: { schema: { type: string } } } } + "401": { $ref: "#/components/responses/PanelUnauthorized" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/logout: + post: + tags: ["Panel: Auth"] + summary: Log out + description: Requires an authenticated session; no specific permission. + security: [{ panelSession: [], panelCsrf: [] }] + responses: + "200": { description: OK } + + /api/session: + get: + tags: ["Panel: Auth"] + summary: Current session info + description: Requires an authenticated session; no specific permission. + security: [{ panelSession: [] }] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + actor: { type: string } + permissions: { type: array, items: { type: string } } + csrf_token: { type: string } + "401": { $ref: "#/components/responses/PanelUnauthorized" } + + /api/public/branding: + get: + tags: ["Panel: Auth"] + summary: Public server branding (name/description), unauthenticated + description: Public route — shown on the login screen before signing in. + security: [] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + + /api/public/icon: + get: + tags: ["Panel: Auth"] + summary: Public server icon, unauthenticated + security: [] + responses: + "200": { description: Image bytes, content: { image/*: { schema: { type: string, format: binary } } } } + "404": { description: No icon set } + + # --- Admin operator accounts (admins.manage) --- + + /api/admin-users: + get: + tags: ["Panel: Admin Operators"] + summary: List admin operator accounts + description: Requires `admins.manage`. + security: [{ panelSession: [] }] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + system: { type: object, description: "the break-glass account", properties: { username: { type: string, const: owpengram }, permissions: { type: array, items: { type: string } }, enabled: { type: boolean, const: true }, system: { type: boolean, const: true } } } + rows: { type: array, items: { type: object, additionalProperties: true } } + available_permissions: { type: array, items: { type: string } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/create-admin-operator: + post: + tags: ["Panel: Admin Operators"] + summary: Create a named admin operator account + description: Requires `admins.manage`. `reason` is mandatory (stricter than the generic action envelope). + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: { $ref: "#/components/requestBodies/PanelAdminUserAction" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/set-admin-operator-access: + post: + tags: ["Panel: Admin Operators"] + summary: Edit a named admin operator's permissions/enabled state + description: Requires `admins.manage`. `reason` is mandatory. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: { $ref: "#/components/requestBodies/PanelAdminUserAction" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/set-admin-operator-password: + post: + tags: ["Panel: Admin Operators"] + summary: Set a named admin operator's password + description: Requires `admins.manage`. `reason` is mandatory. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: { $ref: "#/components/requestBodies/PanelAdminUserAction" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + # --- Dashboard --- + + /api/dashboard: + get: + tags: ["Panel: Dashboard"] + summary: Dashboard summary + description: Requires `dashboard.read`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + # --- Accounts --- + + /api/accounts: + get: + tags: ["Panel: Accounts"] + summary: List/search accounts + description: Requires `accounts.read`. + security: [{ panelSession: [] }] + parameters: + - { name: q, in: query, schema: { type: string } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_active_us, in: query, description: microseconds, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + query: { type: string } + limit: { type: integer } + rows: { type: array, items: { $ref: "#/components/schemas/AccountRow" } } + has_more: { type: boolean } + next_before_id: { type: integer, format: int64 } + next_before_active_us: { type: integer, format: int64 } + listing: { type: boolean } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/accounts/stats: + get: + tags: ["Panel: Accounts"] + summary: Account statistics + description: Requires `accounts.read`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/accounts/shared-devices: + get: + tags: ["Panel: Accounts"] + summary: Accounts grouped by shared device fingerprint + description: Requires `accounts.read`. + security: [{ panelSession: [] }] + parameters: + - { name: offset, in: query, schema: { type: integer } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + limit: { type: integer } + offset: { type: integer } + rows: { type: array, items: { type: object, additionalProperties: true } } + has_more: { type: boolean } + next_offset: { type: integer } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/accounts/{id}: + get: + tags: ["Panel: Accounts"] + summary: Account card (profile, flags, statistics) + description: Requires `accounts.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + "404": { $ref: "#/components/responses/PanelNotFound" } + + /api/accounts/{id}/avatar: + get: + tags: ["Panel: Accounts"] + summary: Account avatar (binary, proxied from the Admin API) + description: Requires `accounts.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: Image bytes, content: { image/*: { schema: { type: string, format: binary } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + "404": { description: Not found } + + /api/actions/set-frozen: + post: + tags: ["Panel: Accounts"] + summary: Freeze/unfreeze an account + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: { $ref: "#/components/requestBodies/PanelSetFrozen" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/grant-premium: + post: + tags: ["Panel: Accounts"] + summary: Grant Premium months + description: Requires `premium.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id, months], properties: { user_id: { type: integer, format: int64 }, months: { type: integer } } }] + example: { command_id: "grant-premium-001", reason: "Incident compensation", confirm: true, user_id: 123456789, months: 12 } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-verified: + post: + tags: ["Panel: Accounts"] + summary: Set/clear the legacy verified badge on a user + description: >- + Requires `verification.review` (not `accounts.manage`) — this sets + the boolean verified flag directly, separate from the official + verification-applications workflow, but shares its permission. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id, verified], properties: { user_id: { type: integer, format: int64 }, verified: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-flags: + post: + tags: ["Panel: Accounts"] + summary: Set scam/fake flags on a user + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, scam: { type: boolean }, fake: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-support: + post: + tags: ["Panel: Accounts"] + summary: Set/clear the support flag on a user + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id, support], properties: { user_id: { type: integer, format: int64 }, support: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-username: + post: + tags: ["Panel: Accounts"] + summary: Set a user's username + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id, username], properties: { user_id: { type: integer, format: int64 }, username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-profile: + post: + tags: ["Panel: Accounts"] + summary: Set a user's first/last name + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, first_name: { type: string }, last_name: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-phone: + post: + tags: ["Panel: Accounts"] + summary: Set a user's phone number + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id, phone], properties: { user_id: { type: integer, format: int64 }, phone: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-login-email: + post: + tags: ["Panel: Accounts"] + summary: Set/clear a user's login email + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, email: { type: string, description: "empty clears it" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-avatar: + post: + tags: ["Panel: Accounts"] + summary: Set a user's avatar (static image) + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: { $ref: "#/components/requestBodies/PanelUserAvatarUpload" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-avatar-video: + post: + tags: ["Panel: Accounts"] + summary: Set a user's avatar (animated video) + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: { description: "JSON — common fields + user_id (int64), video_start_ts (float64)", type: string } + file: { type: string, format: binary } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-color: + post: + tags: ["Panel: Accounts"] + summary: Set a user's profile/name color + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/PeerColorInput" }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-account-emoji-status: + post: + tags: ["Panel: Accounts"] + summary: Set a user's emoji status + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/EmojiStatusInput" }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/revoke-sessions: + post: + tags: ["Panel: Accounts"] + summary: Revoke a user's authorized sessions + description: Requires `accounts.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, hash: { type: integer, format: int64 }, keep_hash: { type: integer, format: int64 }, revoke_all: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Broadcasts --- + + /api/broadcasts: + get: + tags: ["Panel: Broadcasts"] + summary: List broadcasts + description: Requires `broadcasts.read`. + security: [{ panelSession: [] }] + parameters: + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: { limit: { type: integer }, rows: { type: array, items: { type: object, additionalProperties: true } }, has_more: { type: boolean }, next_before_id: { type: integer, format: int64 } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/create-broadcast: + post: + tags: ["Panel: Broadcasts"] + summary: Create a broadcast + description: Requires `broadcasts.send`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [message, target_mode], properties: { message: { type: string }, target_mode: { type: string }, user_ids: { type: array, items: { type: integer, format: int64 }, description: "only for target_mode=selected" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Channels --- + + /api/channels: + get: + tags: ["Panel: Channels"] + summary: List/search channels + description: Requires `channels.read`. + security: [{ panelSession: [] }] + parameters: + - { name: q, in: query, schema: { type: string } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_updated_us, in: query, description: microseconds, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": { $ref: "#/components/responses/PanelCursorList" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/channels/{id}: + get: + tags: ["Panel: Channels"] + summary: Channel card + description: Requires `channels.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/channels/{id}/avatar: + get: + tags: ["Panel: Channels"] + summary: Channel avatar (binary, proxied from the Admin API) + description: Requires `channels.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: Image bytes, content: { image/*: { schema: { type: string, format: binary } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/set-channel-flags: + post: + tags: ["Panel: Channels"] + summary: Set scam/fake flags on a channel + description: Requires `channels.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [channel_id], properties: { channel_id: { type: integer, format: int64 }, scam: { type: boolean }, fake: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-channel-avatar: + post: + tags: ["Panel: Channels"] + summary: Set a channel's avatar + description: Requires `channels.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: { description: "JSON — common fields + channel_id (int64)", type: string } + file: { type: string, format: binary } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-channel-settings: + post: + tags: ["Panel: Channels"] + summary: Partially update channel settings + description: >- + Requires `channels.manage`. Every field besides `channel_id` is an + optional pointer — a partial PATCH: only fields present in the JSON + body are changed. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [channel_id] + properties: + channel_id: { type: integer, format: int64 } + gigagroup: { type: [boolean, "null"] } + antispam: { type: [boolean, "null"] } + participants_hidden: { type: [boolean, "null"] } + noforwards: { type: [boolean, "null"] } + join_to_send: { type: [boolean, "null"] } + join_request: { type: [boolean, "null"] } + slowmode_seconds: { type: [integer, "null"] } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-channel-username: + post: + tags: ["Panel: Channels"] + summary: Set a channel's username + description: Requires `channels.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [channel_id, username], properties: { channel_id: { type: integer, format: int64 }, username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-channel-color: + post: + tags: ["Panel: Channels"] + summary: Set a channel's profile/name color + description: Requires `channels.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [channel_id], properties: { channel_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/PeerColorInput" }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-channel-emoji-status: + post: + tags: ["Panel: Channels"] + summary: Set a channel's emoji status + description: Requires `channels.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [channel_id], properties: { channel_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/EmojiStatusInput" }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-channel-verified: + post: + tags: ["Panel: Channels"] + summary: Set/clear the verified badge on a channel + description: Requires `verification.review` (not `channels.manage`). + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [channel_id, verified], properties: { channel_id: { type: integer, format: int64 }, verified: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Bots --- + + /api/bots: + get: + tags: ["Panel: Bots"] + summary: List/search bots + description: Requires `bots.read`. + security: [{ panelSession: [] }] + parameters: + - { name: q, in: query, schema: { type: string } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": { $ref: "#/components/responses/PanelCursorList" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/bots/{id}: + get: + tags: ["Panel: Bots"] + summary: Bot card + description: Requires `bots.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/create-bot: + post: + tags: ["Panel: Bots"] + summary: Create a bot account + description: Requires `bots.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [owner_user_id, name, username], properties: { owner_user_id: { type: integer, format: int64 }, name: { type: string }, username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/delete-bot: + post: + tags: ["Panel: Bots"] + summary: Delete a bot account + description: Requires `bots.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [bot_user_id], properties: { bot_user_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/export-bot-token: + post: + tags: ["Panel: Bots"] + summary: Export a bot's auth token + description: Requires `bots.token.read`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [bot_user_id], properties: { bot_user_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Messages --- + + /api/messages: + get: + tags: ["Panel: Messages"] + summary: Private message audit + description: Requires `messages.read`. Only queries when both `owner_user_id` and `peer_id` are > 0. + security: [{ panelSession: [] }] + parameters: + - { name: owner_user_id, in: query, schema: { type: integer, format: int64 } } + - { name: peer_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_date, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: { owner_user_id: { type: integer, format: int64 }, peer_id: { type: integer, format: int64 }, before_date: { type: integer, format: int64 }, before_id: { type: integer }, limit: { type: integer }, rows: { type: array, items: { type: object, additionalProperties: true } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/messages/detail: + get: + tags: ["Panel: Messages"] + summary: Private message detail + description: Requires `messages.read`. Both query params required. + security: [{ panelSession: [] }] + parameters: + - { name: owner_user_id, in: query, required: true, schema: { type: integer, format: int64 } } + - { name: msg_id, in: query, required: true, schema: { type: integer } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/messages/groups: + get: + tags: ["Panel: Messages"] + summary: Channel/group message audit + description: Requires `messages.read`. + security: [{ panelSession: [] }] + parameters: + - { name: channel_id, in: query, required: true, schema: { type: integer, format: int64 } } + - { name: before_date, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: { channel_id: { type: integer, format: int64 }, before_date: { type: integer, format: int64 }, before_id: { type: integer }, limit: { type: integer }, rows: { type: array, items: { type: object, additionalProperties: true } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/messages/groups/detail: + get: + tags: ["Panel: Messages"] + summary: Channel/group message detail + description: Requires `messages.read`. + security: [{ panelSession: [] }] + parameters: + - { name: channel_id, in: query, required: true, schema: { type: integer, format: int64 } } + - { name: msg_id, in: query, required: true, schema: { type: integer } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/delete-messages: + post: + tags: ["Panel: Messages"] + summary: Delete specific private messages + description: Requires `messages.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [owner_user_id, peer_id, ids], properties: { owner_user_id: { type: integer, format: int64 }, peer_id: { type: integer, format: int64 }, ids: { type: array, items: { type: integer } }, revoke: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/delete-history: + post: + tags: ["Panel: Messages"] + summary: Delete a private chat's history + description: Requires `messages.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [owner_user_id, peer_id] + properties: + owner_user_id: { type: integer, format: int64 } + peer_id: { type: integer, format: int64 } + max_id: { type: integer } + min_date: { type: integer } + max_date: { type: integer } + max_batches: { type: integer } + just_clear: { type: boolean } + revoke: { type: boolean } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Content: emoji --- + + /api/emoji: + get: + tags: ["Panel: Content"] + summary: List/search custom emoji + description: Requires `content.read`. + security: [{ panelSession: [] }] + parameters: + - { name: q, in: query, schema: { type: string } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": { $ref: "#/components/responses/PanelCursorList" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/emoji/{id}/animation: + get: + tags: ["Panel: Content"] + summary: Emoji Lottie animation + description: Requires `content.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: Animation JSON bytes } + "403": { $ref: "#/components/responses/PanelForbidden" } + + # --- Content: stickers --- + + /api/stickers: + get: + tags: ["Panel: Content"] + summary: List sticker sets + description: Requires `content.read`. + security: [{ panelSession: [] }] + parameters: + - { name: kind, in: query, schema: { type: string } } + responses: + "200": + description: OK + content: + application/json: + schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } }, max_items: { type: integer } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/stickers/{id}/documents: + get: + tags: ["Panel: Content"] + summary: Document ids in a sticker set + description: Requires `content.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { document_ids: { type: array, items: { type: integer, format: int64 } } } } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/stickers/documents/{id}/animation: + get: + tags: ["Panel: Content"] + summary: Sticker animation + description: Requires `content.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: Animation bytes } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/set-sticker-set-archived: + post: + tags: ["Panel: Content"] + summary: Archive/unarchive a sticker set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [set_id, archived], properties: { set_id: { type: string, description: "int64, json:\",string\"" }, archived: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-sticker-set-sort-order: + post: + tags: ["Panel: Content"] + summary: Reorder a sticker set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [set_id, sort_order], properties: { set_id: { type: string, description: "int64, json:\",string\"" }, sort_order: { type: integer } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/rename-sticker-set: + post: + tags: ["Panel: Content"] + summary: Rename a sticker set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [set_id, title], properties: { set_id: { type: string, description: "int64, json:\",string\"" }, title: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/delete-sticker-set: + post: + tags: ["Panel: Content"] + summary: Delete a sticker set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [set_id], properties: { set_id: { type: string, description: "int64, json:\",string\"" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/create-sticker-set: + post: + tags: ["Panel: Content"] + summary: Create a sticker set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: { description: "JSON — common fields + title, short_name, kind, emoji, keywords (opt.)", type: string } + file: { type: string, format: binary } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/add-sticker-to-set: + post: + tags: ["Panel: Content"] + summary: Add a sticker to a set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: { description: "JSON — common fields + set_id (int64, json:\",string\"), emoji, keywords (opt.)", type: string } + file: { type: string, format: binary } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/remove-sticker-from-set: + post: + tags: ["Panel: Content"] + summary: Remove a sticker from a set + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [set_id, document_id], properties: { set_id: { type: string, description: "int64, json:\",string\"" }, document_id: { type: string, description: "int64, json:\",string\"" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Content: GIF catalog --- + + /api/gif-catalog: + get: + tags: ["Panel: Content"] + summary: GIF catalog + description: Requires `content.read`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/gif-catalog/documents/{id}/preview: + get: + tags: ["Panel: Content"] + summary: GIF preview + description: Requires `content.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: Preview bytes } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/create-gif-catalog-entry: + post: + tags: ["Panel: Content"] + summary: Add a GIF catalog entry + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: { description: "JSON — common fields + title", type: string } + file: { type: string, format: binary } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-gif-catalog-enabled: + post: + tags: ["Panel: Content"] + summary: Enable/disable a GIF catalog entry + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [id, enabled], properties: { id: { type: string, description: "int64, json:\",string\"" }, enabled: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-gif-catalog-sort-order: + post: + tags: ["Panel: Content"] + summary: Reorder a GIF catalog entry + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [id, sort_order], properties: { id: { type: string, description: "int64, json:\",string\"" }, sort_order: { type: integer } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-gif-catalog-category: + post: + tags: ["Panel: Content"] + summary: Set a GIF catalog entry's category + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [id, category], properties: { id: { type: string, description: "int64, json:\",string\"" }, category: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/auto-categorize-gif-catalog: + post: + tags: ["Panel: Content"] + summary: Auto-categorize uncategorized GIFs + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PanelCommandFields" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/delete-uncategorized-gifs: + post: + tags: ["Panel: Content"] + summary: Delete every uncategorized GIF catalog entry + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PanelCommandFields" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/delete-gif-catalog-entry: + post: + tags: ["Panel: Content"] + summary: Delete a GIF catalog entry + description: Requires `content.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [id], properties: { id: { type: string, description: "int64, json:\",string\"" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Storage --- + + /api/storage/stats: + get: + tags: ["Panel: Storage"] + summary: Object storage statistics + description: Requires `storage.read`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/storage/accounts: + get: + tags: ["Panel: Storage"] + summary: Storage usage broken down by account + description: Requires `storage.read`. + security: [{ panelSession: [] }] + parameters: + - { name: offset, in: query, schema: { type: integer } } + - { $ref: "#/components/parameters/Limit" } + - { name: order, in: query, schema: { type: string, enum: [asc, desc] } } + - { name: q, in: query, schema: { type: string } } + - { name: sort, in: query, schema: { type: string } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/storage-manual-purge: + post: + tags: ["Panel: Storage"] + summary: Manually purge storage by category + description: Requires `storage.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [categories] + properties: + categories: { type: array, items: { type: string, enum: [photo, video, round_video, gif, music, voice, file] } } + include_avatars: { type: boolean } + created_before: { type: [string, "null"], format: date-time, description: "RFC3339; null = no age filter" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Moderation (proxied to the Admin API) --- + + /api/moderation/cases: + get: + tags: ["Panel: Moderation"] + summary: List moderation cases + description: Requires `moderation.review`. Entire query string forwarded to the Admin API's `GET /v1/moderation/cases` unchanged. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/moderation/cases/{id}: + get: + tags: ["Panel: Moderation"] + summary: Moderation case detail + description: Requires `moderation.review`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/moderation/reports/{id}: + get: + tags: ["Panel: Moderation"] + summary: Moderation report detail + description: Requires `moderation.review`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/moderation/cases/{id}/claim: + post: + tags: ["Panel: Moderation"] + summary: Claim a moderation case + description: Requires `moderation.review`. Relayed to the Admin API — `actor` is injected from the session, `command_id` minted if absent. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/moderation/cases/{id}/decide: + post: + tags: ["Panel: Moderation"] + summary: Decide a moderation case + description: Requires `moderation.review`. Relayed to the Admin API's `POST /v1/moderation/cases/{id}/decide`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/moderation/cases/{id}/appeals/{appeal_id}/review: + post: + tags: ["Panel: Moderation"] + summary: Review an appeal on a moderation case + description: Requires `moderation.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: + - { $ref: "#/components/parameters/PathId" } + - { name: appeal_id, in: path, required: true, schema: { type: integer, format: int64 } } + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + # --- Usernames: reserved + collectible --- + + /api/collectible-usernames: + get: + tags: ["Panel: Usernames"] + summary: List collectible usernames + description: Requires `usernames.read`. + security: [{ panelSession: [] }] + parameters: + - { name: status, in: query, schema: { type: string, enum: ["", vault, owned, burned] } } + - { name: owner_user_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: q, in: query, schema: { type: string } } + responses: + "200": + description: "OK — `next_before_id` is a string (int64 precision)." + content: + application/json: + schema: + type: object + properties: + rows: { type: array, items: { type: object, additionalProperties: true } } + has_more: { type: boolean } + next_before_id: { type: string } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/collectible-usernames/{id}: + get: + tags: ["Panel: Usernames"] + summary: Collectible username detail + transfer log + description: Requires `usernames.read`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + asset: { type: object, additionalProperties: true } + transfers: { type: array, items: { type: object, additionalProperties: true } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/reserved-usernames: + get: + tags: ["Panel: Usernames"] + summary: List reserved usernames + description: Requires `usernames.read`. Proxied to the Admin API's `GET /v1/reserved-usernames`. + security: [{ panelSession: [] }] + parameters: + - { name: q, in: query, schema: { type: string } } + - { $ref: "#/components/parameters/Limit" } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/reserve-username: + post: + tags: ["Panel: Usernames"] + summary: Add a username to the operator reserved-username blocklist + description: Requires `usernames.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [username], properties: { username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/unreserve-username: + post: + tags: ["Panel: Usernames"] + summary: Remove a username from the operator reserved-username blocklist + description: Requires `usernames.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [username], properties: { username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/mint-collectible-username: + post: + tags: ["Panel: Usernames"] + summary: Mint a collectible username + description: Requires `usernames.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [username, currency, amount] + properties: + username: { type: string } + owner_user_id: { $ref: "#/components/schemas/FlexInt64" } + owner_channel_id: { $ref: "#/components/schemas/FlexInt64" } + currency: { type: string } + amount: { $ref: "#/components/schemas/FlexInt64" } + crypto_currency: { type: string } + crypto_amount: { $ref: "#/components/schemas/FlexInt64" } + url: { type: string } + purchase_date: { $ref: "#/components/schemas/FlexUnix" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/transfer-collectible-username: + post: + tags: ["Panel: Usernames"] + summary: Transfer a collectible username + description: Requires `usernames.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [username] + properties: + username: { type: string } + to_user_id: { $ref: "#/components/schemas/FlexInt64" } + to_channel_id: { $ref: "#/components/schemas/FlexInt64" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/revoke-collectible-username: + post: + tags: ["Panel: Usernames"] + summary: Revoke (or burn) a collectible username + description: >- + Requires `usernames.manage`. When `burn: true`, the effective + action name in the command envelope flips to + `burn-collectible-username` (same route/struct). + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [username], properties: { username: { type: string }, burn: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/delete-collectible-username: + post: + tags: ["Panel: Usernames"] + summary: Delete a collectible username + description: Requires `usernames.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [username], properties: { username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Official verification --- + + /api/verification/applications: + get: + tags: ["Panel: Official Verification"] + summary: List official-verification applications + description: Requires `verification.review`. + security: [{ panelSession: [] }] + parameters: + - { name: status, in: query, schema: { type: string } } + - { name: target_type, in: query, schema: { type: string } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: reviewer, in: query, schema: { type: string } } + - { name: q, in: query, schema: { type: string } } + responses: + "200": { $ref: "#/components/responses/PanelCursorList" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/verification/applications/{id}: + get: + tags: ["Panel: Official Verification"] + summary: Application detail + description: Requires `verification.review`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/verification/counts: + get: + tags: ["Panel: Official Verification"] + summary: Application counts by status + description: Requires `verification.review`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/verification/applications/{id}/claim: + post: + tags: ["Panel: Official Verification"] + summary: Claim an application + description: Requires `verification.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/verification/applications/{id}/approve: + post: + tags: ["Panel: Official Verification"] + summary: Approve an application (grants the badge) + description: Requires `verification.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/verification/applications/{id}/reject: + post: + tags: ["Panel: Official Verification"] + summary: Reject an application + description: Requires `verification.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/actions/revoke-verification: + post: + tags: ["Panel: Official Verification"] + summary: Strip an official-verification badge + description: Requires BOTH `verification.review` and `verification.revoke`. Addresses the target directly, not an application id. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [target_type, target_id] + properties: + target_type: { type: string, description: "domain.VerificationTargetType, validated" } + target_id: { $ref: "#/components/schemas/FlexInt64" } + internal_note: { type: string } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + # --- Bot verification (feature-flagged: 404s while TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true, the default) --- + + /api/botverification/verifiers: + get: + tags: ["Panel: Bot Verification"] + summary: List third-party verifier bots + description: Requires `botverification.review`. + security: [{ panelSession: [] }] + parameters: + - { $ref: "#/components/parameters/Limit" } + - { name: enabled_only, in: query, schema: { type: string, description: "1/true/yes/on" } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } } } } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + "404": { description: "Third-party verification is disabled (TELESRV_HIDE_THIRD_PARTY_VERIFICATION)" } + + /api/botverification/icons: + get: + tags: ["Panel: Bot Verification"] + summary: List verification icon catalog + description: Requires `botverification.review`. + security: [{ panelSession: [] }] + parameters: + - { $ref: "#/components/parameters/Limit" } + - { name: active_only, in: query, schema: { type: string } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } } } } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/botverification/marks: + get: + tags: ["Panel: Bot Verification"] + summary: List active third-party marks + description: Requires `botverification.review`. + security: [{ panelSession: [] }] + parameters: + - { name: peer_type, in: query, schema: { type: string, enum: ["", user, channel] } } + - { name: verifier_bot_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: q, in: query, schema: { type: string } } + responses: + "200": { $ref: "#/components/responses/PanelCursorList" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/botverification/requests: + get: + tags: ["Panel: Bot Verification"] + summary: List third-party verification requests + description: Requires `botverification.review`. + security: [{ panelSession: [] }] + parameters: + - { name: status, in: query, schema: { type: string } } + - { name: peer_type, in: query, schema: { type: string } } + - { name: verifier_bot_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: q, in: query, schema: { type: string } } + responses: + "200": { $ref: "#/components/responses/PanelCursorList" } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/botverification/requests/{id}: + get: + tags: ["Panel: Bot Verification"] + summary: Request detail + description: Requires `botverification.review`. + security: [{ panelSession: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + request: { type: object, additionalProperties: true } + verifier: { type: object, additionalProperties: true } + mark_active: { type: boolean } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/botverification/counts: + get: + tags: ["Panel: Bot Verification"] + summary: Request counts by status + description: Requires `botverification.review`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/botverification/requests/{id}/approve: + post: + tags: ["Panel: Bot Verification"] + summary: Approve a third-party verification request + description: Requires `botverification.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/botverification/requests/{id}/reject: + post: + tags: ["Panel: Bot Verification"] + summary: Reject a third-party verification request + description: Requires `botverification.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/botverification/requests/{id}/revoke: + post: + tags: ["Panel: Bot Verification"] + summary: Revoke a granted third-party verification + description: Requires `botverification.review`. + security: [{ panelSession: [], panelCsrf: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/PanelVersionedDecision" } + responses: + "200": { $ref: "#/components/responses/PanelActionOK" } + "403": { $ref: "#/components/responses/PanelForbidden" } + "409": { $ref: "#/components/responses/PanelConflict" } + + /api/actions/grant-bot-verifier: + post: + tags: ["Panel: Bot Verification"] + summary: Appoint a bot as a third-party verifier + description: Requires `botverification.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [bot_id, company_name] + properties: + bot_id: { $ref: "#/components/schemas/FlexInt64" } + icon_document_id: { $ref: "#/components/schemas/FlexInt64" } + company_name: { type: string } + default_description: { type: string } + can_modify_custom_description: { type: boolean } + version: { $ref: "#/components/schemas/FlexInt64" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-bot-verifier-enabled: + post: + tags: ["Panel: Bot Verification"] + summary: Enable/disable a verifier bot + description: Requires `botverification.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [bot_id, enabled], properties: { bot_id: { $ref: "#/components/schemas/FlexInt64" }, enabled: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/revoke-bot-verifier: + post: + tags: ["Panel: Bot Verification"] + summary: Strip a bot's verifier status + description: Requires `botverification.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [bot_id], properties: { bot_id: { $ref: "#/components/schemas/FlexInt64" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/upsert-verification-icon: + post: + tags: ["Panel: Bot Verification"] + summary: Add/edit a verification-icon catalog entry + description: Requires `botverification.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [document_id, name], properties: { document_id: { $ref: "#/components/schemas/FlexInt64" }, name: { type: string }, owner_bot_id: { $ref: "#/components/schemas/FlexInt64" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/set-verification-icon-active: + post: + tags: ["Panel: Bot Verification"] + summary: Enable/disable a verification icon + description: Requires `botverification.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [icon_id, active], properties: { icon_id: { $ref: "#/components/schemas/FlexInt64" }, active: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + /api/actions/revoke-custom-verification: + post: + tags: ["Panel: Bot Verification"] + summary: Strip a third-party mark from a peer + description: Requires `botverification.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [verifier_bot_id, peer_type, peer_id], properties: { verifier_bot_id: { $ref: "#/components/schemas/FlexInt64" }, peer_type: { type: string, enum: [user, channel] }, peer_id: { $ref: "#/components/schemas/FlexInt64" } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "502": { $ref: "#/components/responses/PanelActionUpstreamError" } } + + # --- Server settings --- + + /api/server/identity: + get: + tags: ["Panel: Server Settings"] + summary: Server identity (name/description/icon presence) + description: Requires `server.manage`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/server/add-server-link: + get: + tags: ["Panel: Server Settings"] + summary: The ready-made owpg://addserver link for this server + description: Requires `server.manage`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/server/icon: + get: + tags: ["Panel: Server Settings"] + summary: Current server icon (binary) + description: Requires `server.manage`. + security: [{ panelSession: [] }] + responses: + "200": { description: Image bytes, content: { image/*: { schema: { type: string, format: binary } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + "404": { description: No icon set } + + /api/actions/set-server-identity: + post: + tags: ["Panel: Server Settings"] + summary: Set the server's name/description + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, properties: { name: { type: string }, description: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/actions/set-welcome-message-templates: + post: + tags: ["Panel: Server Settings"] + summary: Set the welcome (phone/email) message templates + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, properties: { phone_template: { type: string }, email_template: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/actions/set-login-code-message-template: + post: + tags: ["Panel: Server Settings"] + summary: Set the login-code SMS/message template + description: >- + Requires `server.manage`. The template must contain exactly one + `{{code}}` placeholder if non-empty, else 422 + (`domain.ValidateLoginCodeMessageTemplate`). + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, properties: { template: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" }, "422": { description: "template must contain exactly one {{code}}", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } } } + + /api/actions/upload-server-icon: + post: + tags: ["Panel: Server Settings"] + summary: Upload a new server icon + description: "Requires `server.manage`. File ≤ 2MiB, extension in {.png,.jpg,.jpeg,.webp,.gif}." + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: { description: "JSON — common fields only", type: string } + file: { type: string, format: binary } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/actions/remove-server-icon: + post: + tags: ["Panel: Server Settings"] + summary: Remove the server icon + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PanelCommandFields" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/actions/complete-setup: + post: + tags: ["Panel: Server Settings"] + summary: Mark first-run setup as complete + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PanelCommandFields" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/server/env: + get: + tags: ["Panel: Server Settings"] + summary: Current .env values + description: Requires `server.manage`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/update-server-env: + post: + tags: ["Panel: Server Settings"] + summary: Update .env values + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/PanelCommandFields" }, { type: object, required: [values], properties: { values: { type: object, additionalProperties: { type: string } } } }] + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/server/status: + get: + tags: ["Panel: Server Settings"] + summary: Server process status + description: Requires `server.manage`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/server/docker-status: + get: + tags: ["Panel: Server Settings"] + summary: Live container/process status (Services tab) + description: >- + Requires `server.manage`. Backed by `procctl.Manager.DockerStatus`, + which shells out to `docker compose ps`. If the deployment actually + runs under podman (or `docker` isn't installed/running), this exec + fails and the tab renders as "nothing running" rather than a clear + error — the handler doesn't currently distinguish "no services + configured" from "couldn't ask Docker". + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/server/check-updates: + get: + tags: ["Panel: Server Settings"] + summary: Check for available server updates + description: Requires `server.manage`. + security: [{ panelSession: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "403": { $ref: "#/components/responses/PanelForbidden" } + + /api/actions/restart-server: + post: + tags: ["Panel: Server Settings"] + summary: Restart the server process + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PanelCommandFields" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + /api/actions/update-server: + post: + tags: ["Panel: Server Settings"] + summary: Update the server to the latest build + description: Requires `server.manage`. + security: [{ panelSession: [], panelCsrf: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PanelCommandFields" } + responses: { "200": { $ref: "#/components/responses/PanelActionOK" }, "403": { $ref: "#/components/responses/PanelForbidden" } } + + # ============================================================ + # ADMIN API (internal/adminapi) — bearer token + # ============================================================ + + /healthz: + get: + tags: ["AdminAPI: Health"] + summary: Health check + security: [] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { status: { type: string, const: ok } } } } } } + + /v1/accounts/set-frozen: + post: + tags: ["AdminAPI: Accounts"] + summary: Freeze/unfreeze an account + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [user_id, frozen] + properties: + user_id: { type: integer, format: int64 } + frozen: { type: boolean } + freeze_until: { type: string, format: date-time } + freeze_appeal_url: { type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "400": { $ref: "#/components/responses/AdminApiCommandError" } } + + /v1/accounts/set-restricted: + post: + tags: ["AdminAPI: Accounts"] + summary: Restrict/unrestrict an account (spam sanction — join/message gate) + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [user_id, restricted] + properties: + user_id: { type: integer, format: int64 } + restricted: { type: boolean } + restricted_until: { type: string, format: date-time } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "400": { $ref: "#/components/responses/AdminApiCommandError" } } + + /v1/accounts/{id}/avatar: + get: + tags: ["AdminAPI: Accounts"] + summary: Account avatar (binary) + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: Image bytes, content: { image/*: { schema: { type: string, format: binary } } } } + "404": { description: Not found } + + /v1/accounts/grant-premium: + post: + tags: ["AdminAPI: Accounts"] + summary: Grant Premium months + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id, months], properties: { user_id: { type: integer, format: int64 }, months: { type: integer } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-verified: + post: + tags: ["AdminAPI: Accounts"] + summary: Set/clear verified badge + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id, verified], properties: { user_id: { type: integer, format: int64 }, verified: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-flags: + post: + tags: ["AdminAPI: Accounts"] + summary: Set scam/fake flags + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, scam: { type: boolean }, fake: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-support: + post: + tags: ["AdminAPI: Accounts"] + summary: Set/clear support flag + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id, support], properties: { user_id: { type: integer, format: int64 }, support: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-username: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's username + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id, username], properties: { user_id: { type: integer, format: int64 }, username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-profile: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's first/last name + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, first_name: { type: string }, last_name: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-phone: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's phone + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id, phone], properties: { user_id: { type: integer, format: int64 }, phone: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-login-email: + post: + tags: ["AdminAPI: Accounts"] + summary: Set/clear a user's login email + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, email: { type: string, description: "empty clears it" } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-avatar: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's avatar (static image) + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, metadata] + properties: + file: { type: string, format: binary } + metadata: { description: "JSON — CommandMeta + user_id (int64)", type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-avatar-video: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's avatar (animated video) + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, metadata] + properties: + file: { type: string, format: binary } + metadata: { description: "JSON — CommandMeta + user_id (int64), video_start_ts (float64)", type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-color: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's profile/name color + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/PeerColorInput" }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/set-emoji-status: + post: + tags: ["AdminAPI: Accounts"] + summary: Set a user's emoji status + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/EmojiStatusInput" }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/accounts/revoke-sessions: + post: + tags: ["AdminAPI: Accounts"] + summary: Revoke a user's authorized sessions + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [user_id], properties: { user_id: { type: integer, format: int64 }, hash: { type: integer, format: int64 }, keep_hash: { type: integer, format: int64 }, revoke_all: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/{id}/avatar: + get: + tags: ["AdminAPI: Channels"] + summary: Channel avatar (binary) + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: { "200": { description: Image bytes, content: { image/*: { schema: { type: string, format: binary } } } }, "404": { description: Not found } } + + /v1/channels/set-avatar: + post: + tags: ["AdminAPI: Channels"] + summary: Set a channel's avatar + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, metadata] + properties: + file: { type: string, format: binary } + metadata: { description: "JSON — CommandMeta + channel_id (int64)", type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/set-verified: + post: + tags: ["AdminAPI: Channels"] + summary: Set/clear verified badge + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [channel_id, verified], properties: { channel_id: { type: integer, format: int64 }, verified: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/set-flags: + post: + tags: ["AdminAPI: Channels"] + summary: Set scam/fake flags + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [channel_id], properties: { channel_id: { type: integer, format: int64 }, scam: { type: boolean }, fake: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/set-settings: + post: + tags: ["AdminAPI: Channels"] + summary: Partially update channel settings + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [channel_id] + properties: + channel_id: { type: integer, format: int64 } + gigagroup: { type: [boolean, "null"] } + antispam: { type: [boolean, "null"] } + participants_hidden: { type: [boolean, "null"] } + noforwards: { type: [boolean, "null"] } + join_to_send: { type: [boolean, "null"] } + join_request: { type: [boolean, "null"] } + slowmode_seconds: { type: [integer, "null"] } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/set-username: + post: + tags: ["AdminAPI: Channels"] + summary: Set a channel's username + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [channel_id, username], properties: { channel_id: { type: integer, format: int64 }, username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/set-color: + post: + tags: ["AdminAPI: Channels"] + summary: Set a channel's profile/name color + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [channel_id], properties: { channel_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/PeerColorInput" }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/channels/set-emoji-status: + post: + tags: ["AdminAPI: Channels"] + summary: Set a channel's emoji status + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [channel_id], properties: { channel_id: { type: integer, format: int64 } } }, { $ref: "#/components/schemas/EmojiStatusInput" }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/bots/create: + post: + tags: ["AdminAPI: Bots & Broadcasts"] + summary: Create a bot account + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [owner_user_id, name, username], properties: { owner_user_id: { type: integer, format: int64 }, name: { type: string }, username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/broadcasts/create: + post: + tags: ["AdminAPI: Bots & Broadcasts"] + summary: Create a broadcast + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [message, target_mode], properties: { message: { type: string }, target_mode: { type: string }, user_ids: { type: array, items: { type: integer, format: int64 } } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/bots/delete: + post: + tags: ["AdminAPI: Bots & Broadcasts"] + summary: Delete a bot account + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [bot_user_id], properties: { bot_user_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/bots/export-token: + post: + tags: ["AdminAPI: Bots & Broadcasts"] + summary: Export a bot's auth token + description: Requires scoped permission `bots.token.read` (or master token). + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [bot_user_id], properties: { bot_user_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/messages/delete: + post: + tags: ["AdminAPI: Messages"] + summary: Delete specific private messages + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [owner_user_id, peer, ids] + properties: + owner_user_id: { type: integer, format: int64 } + peer: { $ref: "#/components/schemas/Peer" } + ids: { type: array, items: { type: integer } } + revoke: { type: boolean } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/messages/delete-history: + post: + tags: ["AdminAPI: Messages"] + summary: Delete a private chat's history + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [owner_user_id, peer] + properties: + owner_user_id: { type: integer, format: int64 } + peer: { $ref: "#/components/schemas/Peer" } + max_id: { type: integer } + min_date: { type: integer } + max_date: { type: integer } + just_clear: { type: boolean } + revoke: { type: boolean } + max_batches: { type: integer } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/set-archived: + post: + tags: ["AdminAPI: Stickers"] + summary: Archive/unarchive a sticker set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [set_id, archived], properties: { set_id: { type: integer, format: int64 }, archived: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/set-sort-order: + post: + tags: ["AdminAPI: Stickers"] + summary: Reorder a sticker set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [set_id, sort_order], properties: { set_id: { type: integer, format: int64 }, sort_order: { type: integer } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/rename: + post: + tags: ["AdminAPI: Stickers"] + summary: Rename a sticker set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [set_id, title], properties: { set_id: { type: integer, format: int64 }, title: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/delete: + post: + tags: ["AdminAPI: Stickers"] + summary: Delete a sticker set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [set_id], properties: { set_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/create: + post: + tags: ["AdminAPI: Stickers"] + summary: Create a sticker set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, metadata] + properties: + file: { type: string, format: binary } + metadata: { description: "JSON — CommandMeta + title, short_name, kind, emoji, keywords (opt.)", type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/add: + post: + tags: ["AdminAPI: Stickers"] + summary: Add a sticker to a set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, metadata] + properties: + file: { type: string, format: binary } + metadata: { description: "JSON — CommandMeta + set_id (int64), emoji, keywords (opt.)", type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/remove: + post: + tags: ["AdminAPI: Stickers"] + summary: Remove a sticker from a set + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [set_id, document_id], properties: { set_id: { type: integer, format: int64 }, document_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/stickers/documents/{id}/animation: + get: + tags: ["AdminAPI: Stickers"] + summary: Sticker animation + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: { "200": { description: Animation bytes }, "400": { $ref: "#/components/responses/AdminApiCodedError" }, "404": { $ref: "#/components/responses/AdminApiCodedError" } } + + /v1/gif-catalog/create: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Add a GIF catalog entry + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, metadata] + properties: + file: { type: string, format: binary } + metadata: { description: "JSON — CommandMeta + title, content_sha256 (opt.)", type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/set-enabled: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Enable/disable a GIF catalog entry + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [id, enabled], properties: { id: { type: integer, format: int64 }, enabled: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/set-sort-order: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Reorder a GIF catalog entry + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [id, sort_order], properties: { id: { type: integer, format: int64 }, sort_order: { type: integer } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/set-category: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Set a GIF catalog entry's category + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [id, category], properties: { id: { type: integer, format: int64 }, category: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/auto-categorize: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Auto-categorize uncategorized GIFs + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CommandMeta" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/delete-uncategorized: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Delete every uncategorized GIF catalog entry + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CommandMeta" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/delete: + post: + tags: ["AdminAPI: GIF Catalog"] + summary: Delete a GIF catalog entry + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [id], properties: { id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/gif-catalog/documents/{id}/preview: + get: + tags: ["AdminAPI: GIF Catalog"] + summary: GIF preview + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: { "200": { description: Preview bytes }, "400": { $ref: "#/components/responses/AdminApiCodedError" }, "404": { $ref: "#/components/responses/AdminApiCodedError" } } + + /v1/emoji/{id}/animation: + get: + tags: ["AdminAPI: Health"] + summary: Custom emoji Lottie animation + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: { "200": { description: "Animation JSON bytes (Content-Type forced to application/json)" }, "400": { $ref: "#/components/responses/AdminApiCodedError" }, "404": { $ref: "#/components/responses/AdminApiCodedError" } } + + /v1/storage/manual-purge: + post: + tags: ["AdminAPI: Storage"] + summary: Manually purge storage by category + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [categories] + properties: + categories: + type: array + items: { type: string, enum: [photo, video, round_video, gif, music, voice, file] } + include_avatars: { type: boolean } + created_before: { type: [string, "null"], format: date-time, description: "RFC3339; null = no age filter" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/moderation/cases: + get: + tags: ["AdminAPI: Moderation"] + summary: List moderation cases + security: [{ adminApiBearer: [] }] + parameters: + - { name: limit, in: query, schema: { type: integer, default: 50 } } + - { name: assigned_to, in: query, schema: { type: string } } + - { name: statuses, in: query, description: comma-separated, schema: { type: string } } + - { name: target_type, in: query, schema: { type: string } } + - { name: target_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_updated_at, in: query, schema: { type: string, format: date-time } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { cases: { type: array, items: { type: object, additionalProperties: true } } } } } } } + + /v1/moderation/cases/{id}: + get: + tags: ["AdminAPI: Moderation"] + summary: Moderation case detail + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: "domain.ModerationCaseDetail (Decisions/Actions/Appeals nil-normalized to [])", content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "404": { $ref: "#/components/responses/AdminApiModerationError" } + + /v1/moderation/reports/{id}: + get: + tags: ["AdminAPI: Moderation"] + summary: Moderation report detail + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": { description: "domain.ModerationReport", content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "404": { $ref: "#/components/responses/AdminApiModerationError" } + + /v1/moderation/cases/{id}/claim: + post: + tags: ["AdminAPI: Moderation"] + summary: Claim a moderation case + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [expected_version, actor] + properties: { expected_version: { type: integer, format: int64 }, actor: { type: string } } + responses: + "200": { description: "domain.ModerationCase", content: { application/json: { schema: { type: object, additionalProperties: true } } } } + "409": { $ref: "#/components/responses/AdminApiModerationError" } + + /v1/moderation/cases/{id}/decide: + post: + tags: ["AdminAPI: Moderation"] + summary: Decide a moderation case + description: >- + `kind` = domain.ModerationDecisionKind. `actions[].kind` includes + `restrict_account`/`unrestrict_account` (the account spam-restriction + sanction), `freeze_account`/`unfreeze_account`, `mark_scam`, + `mark_fake`, `clear_peer_flags`, `delete_private_message`, + `delete_channel_message`, `delete_account`. + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [expected_version, actor, reason, kind] + properties: + expected_version: { type: integer, format: int64 } + actor: { type: string } + reason: { type: string } + command_id: { type: string } + kind: { type: string, description: "domain.ModerationDecisionKind" } + actions: + type: array + items: + type: object + properties: + kind: { type: string, description: "domain.ModerationActionKind, e.g. restrict_account" } + payload: { type: object, additionalProperties: true } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { created: { type: boolean }, case: { type: object, additionalProperties: true } } } } } } + "400": { $ref: "#/components/responses/AdminApiModerationError" } + "409": { $ref: "#/components/responses/AdminApiModerationError" } + + /v1/moderation/cases/{id}/appeals: + post: + tags: ["AdminAPI: Moderation"] + summary: Submit an appeal on a moderation case + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [appellant_user_id, text] + properties: { appellant_user_id: { type: integer, format: int64 }, text: { type: string } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { created: { type: boolean }, appeal: { type: object, additionalProperties: true } } } } } } + + /v1/moderation/cases/{id}/appeals/{appeal_id}/review: + post: + tags: ["AdminAPI: Moderation"] + summary: Review an appeal + security: [{ adminApiBearer: [] }] + parameters: + - { $ref: "#/components/parameters/PathId" } + - { name: appeal_id, in: path, required: true, schema: { type: integer, format: int64 } } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [expected_version, actor, reason, granted] + properties: + expected_version: { type: integer, format: int64 } + actor: { type: string } + reason: { type: string } + command_id: { type: string } + granted: { type: boolean, description: "maps to ModerationDecisionAppealGrant/Deny" } + actions: + type: array + items: { type: object, properties: { kind: { type: string }, payload: { type: object, additionalProperties: true } } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { created: { type: boolean }, case: { type: object, additionalProperties: true } } } } } } + "409": { $ref: "#/components/responses/AdminApiModerationError" } + + /v1/collectible-usernames/mint: + post: + tags: ["AdminAPI: Collectible Usernames"] + summary: Mint a collectible username + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [username, currency, amount] + properties: + username: { type: string } + owner_user_id: { type: string, description: "int64 as decimal string; at most one owner_*" } + owner_channel_id: { type: string, description: "int64 as decimal string" } + currency: { type: string } + amount: { type: string, description: "int64 as decimal string" } + crypto_currency: { type: string } + crypto_amount: { type: string, description: "int64 as decimal string" } + url: { type: string } + purchase_date: { type: integer, format: int64, description: "unix seconds" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/collectible-usernames/transfer: + post: + tags: ["AdminAPI: Collectible Usernames"] + summary: Transfer a collectible username + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [username], properties: { username: { type: string }, to_user_id: { type: string, description: "int64 as decimal string" }, to_channel_id: { type: string, description: "int64 as decimal string" } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/collectible-usernames/revoke: + post: + tags: ["AdminAPI: Collectible Usernames"] + summary: Revoke (or burn) a collectible username + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [username], properties: { username: { type: string }, expected_owner_user_id: { type: string, description: "int64 as decimal string" }, burn: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/collectible-usernames/delete: + post: + tags: ["AdminAPI: Collectible Usernames"] + summary: Delete a collectible username + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [username], properties: { username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/collectible-usernames: + get: + tags: ["AdminAPI: Collectible Usernames"] + summary: List collectible usernames + description: All int64 ids/amounts in the response are decimal strings, to avoid float64 precision loss. + security: [{ adminApiBearer: [] }] + parameters: + - { name: status, in: query, schema: { type: string } } + - { name: q, in: query, schema: { type: string } } + - { name: owner_user_id, in: query, schema: { type: integer, format: int64 } } + - { name: owner_channel_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { assets: { type: array, items: { type: object, additionalProperties: true } } } } } } } + "400": { $ref: "#/components/responses/AdminApiCollectibleError" } + + /v1/collectible-usernames/{id}: + get: + tags: ["AdminAPI: Collectible Usernames"] + summary: Collectible username detail + transfer log + security: [{ adminApiBearer: [] }] + parameters: + - { $ref: "#/components/parameters/PathId" } + - { $ref: "#/components/parameters/Limit" } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + asset: { type: object, additionalProperties: true } + transfers: { type: array, items: { type: object, additionalProperties: true } } + "404": { $ref: "#/components/responses/AdminApiCollectibleError" } + + /v1/reserved-usernames/reserve: + post: + tags: ["AdminAPI: Reserved Usernames"] + summary: Add a username to the operator reserved-username blocklist + description: >- + `Username` is sent with a **capitalized, untagged** JSON key + (`{"Username": "..."}`, not `username`) — a source-level quirk + (missing `json:` struct tag), not a spec error. Every other request + body in this document uses lowercase `snake_case` keys. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [Username], properties: { Username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/reserved-usernames/unreserve: + post: + tags: ["AdminAPI: Reserved Usernames"] + summary: Remove a username from the operator reserved-username blocklist + description: Same `Username` (capitalized, untagged) quirk as `/v1/reserved-usernames/reserve`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [Username], properties: { Username: { type: string } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" } } + + /v1/reserved-usernames: + get: + tags: ["AdminAPI: Reserved Usernames"] + summary: List reserved usernames + security: [{ adminApiBearer: [] }] + parameters: + - { name: q, in: query, schema: { type: string } } + - { $ref: "#/components/parameters/Limit" } + - { name: offset, in: query, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + reserved: + type: array + items: + type: object + properties: + username: { type: string } + reason: { type: string } + actor: { type: string } + created_at: { type: integer, format: int64, description: unix seconds } + "500": { description: "list failed", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } } + + /v1/verification/applications: + get: + tags: ["AdminAPI: Official Verification"] + summary: List official-verification applications + security: [{ adminApiBearer: [] }] + parameters: + - { name: target_type, in: query, schema: { type: string } } + - { name: reviewer, in: query, schema: { type: string } } + - { name: q, in: query, schema: { type: string } } + - { name: status, in: query, description: comma-separated, schema: { type: string } } + - { $ref: "#/components/parameters/Limit" } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { applications: { type: array, items: { type: object, additionalProperties: true } } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/verification/applications/{id}: + get: + tags: ["AdminAPI: Official Verification"] + summary: Application detail + security: [{ adminApiBearer: [] }] + parameters: + - { $ref: "#/components/parameters/PathId" } + - { name: limit, in: query, description: events, schema: { type: integer } } + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + application: { type: object, additionalProperties: true } + events: { type: array, items: { type: object, additionalProperties: true } } + target: { type: object, additionalProperties: true } + target_error: { type: string } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/verification/counts: + get: + tags: ["AdminAPI: Official Verification"] + summary: Application counts by status + security: [{ adminApiBearer: [] }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: object, properties: { counts: { type: object, additionalProperties: { type: string } } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/verification/applications/{id}/claim: + post: + tags: ["AdminAPI: Official Verification"] + summary: Claim an application + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/AdminApiVersionedNote" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" }, "409": { $ref: "#/components/responses/AdminApiVerificationError" } } + + /v1/verification/applications/{id}/approve: + post: + tags: ["AdminAPI: Official Verification"] + summary: Approve an application (grants the badge) + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/AdminApiVersionedNote" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" }, "409": { $ref: "#/components/responses/AdminApiVerificationError" } } + + /v1/verification/applications/{id}/reject: + post: + tags: ["AdminAPI: Official Verification"] + summary: Reject an application + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/AdminApiVersionedNote" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" }, "409": { $ref: "#/components/responses/AdminApiVerificationError" } } + + /v1/verification/revoke: + post: + tags: ["AdminAPI: Official Verification"] + summary: Strip an official-verification badge + description: Requires BOTH `verification.review` and `verification.revoke` (or master token). Addresses the target directly, not an application id. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [target_type, target_id] + properties: + target_type: { type: string, description: "domain.VerificationTargetType" } + target_id: { type: integer, format: int64 } + internal_note: { type: string } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/botverification/verifiers: + get: + tags: ["AdminAPI: Bot Verification"] + summary: List third-party verifier bots + security: [{ adminApiBearer: [] }] + parameters: + - { $ref: "#/components/parameters/Limit" } + - { name: enabled_only, in: query, schema: { type: string, description: "1/true/yes/on" } } + responses: + "200": { description: "PascalCase fields: BotID, IconDocumentID, CompanyName, ... (id/version fields as decimal strings)", content: { application/json: { schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/botverification/icons: + get: + tags: ["AdminAPI: Bot Verification"] + summary: List verification icon catalog + security: [{ adminApiBearer: [] }] + parameters: + - { $ref: "#/components/parameters/Limit" } + - { name: active_only, in: query, schema: { type: string } } + responses: + "200": { description: "PascalCase fields: ID, DocumentID, OwnerBotID, Name, Active, ...", content: { application/json: { schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/botverification/marks: + get: + tags: ["AdminAPI: Bot Verification"] + summary: List active third-party marks + security: [{ adminApiBearer: [] }] + parameters: + - { name: peer_type, in: query, schema: { type: string, enum: [user, channel] } } + - { name: verifier_bot_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: q, in: query, schema: { type: string } } + responses: + "200": { description: "PascalCase fields: ID, VerifierBotID, PeerType, PeerID, ...", content: { application/json: { schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } }, has_more: { type: boolean }, next_before_id: { type: string } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/botverification/requests: + get: + tags: ["AdminAPI: Bot Verification"] + summary: List third-party verification requests + security: [{ adminApiBearer: [] }] + parameters: + - { name: peer_type, in: query, schema: { type: string } } + - { name: status, in: query, description: comma-separated, schema: { type: string } } + - { name: verifier_bot_id, in: query, schema: { type: integer, format: int64 } } + - { name: before_id, in: query, schema: { type: integer, format: int64 } } + - { $ref: "#/components/parameters/Limit" } + - { name: q, in: query, schema: { type: string } } + responses: + "200": { description: "PascalCase fields incl. ApplicantUserID, PeerTitle/Username, Reason, Status, InternalNote, ...", content: { application/json: { schema: { type: object, properties: { rows: { type: array, items: { type: object, additionalProperties: true } }, has_more: { type: boolean }, next_before_id: { type: string } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/botverification/requests/{id}: + get: + tags: ["AdminAPI: Bot Verification"] + summary: Request detail + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + request: { type: object, additionalProperties: true } + verifier: { type: object, additionalProperties: true, description: "present even if verifier is gone" } + verifier_error: { type: string } + mark_active: { type: boolean } + mark_error: { type: string } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + "404": { $ref: "#/components/responses/AdminApiBotVerificationError" } + + /v1/botverification/counts: + get: + tags: ["AdminAPI: Bot Verification"] + summary: Request counts by status + security: [{ adminApiBearer: [] }] + responses: + "200": { description: "counts for pending/approved/rejected/revoked, zero-filled", content: { application/json: { schema: { type: object, properties: { counts: { type: object, additionalProperties: { type: string } } } } } } } + "403": { $ref: "#/components/responses/AdminApiForbidden" } + + /v1/botverification/requests/{id}/approve: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Approve a third-party verification request + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/AdminApiVersionedNote" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" }, "409": { $ref: "#/components/responses/AdminApiBotVerificationError" } } + + /v1/botverification/requests/{id}/reject: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Reject a third-party verification request + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/AdminApiVersionedNote" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" }, "409": { $ref: "#/components/responses/AdminApiBotVerificationError" } } + + /v1/botverification/requests/{id}/revoke: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Revoke a granted third-party verification + security: [{ adminApiBearer: [] }] + parameters: [{ $ref: "#/components/parameters/PathId" }] + requestBody: { $ref: "#/components/requestBodies/AdminApiVersionedNote" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" }, "409": { $ref: "#/components/responses/AdminApiBotVerificationError" } } + + /v1/botverification/verifiers/grant: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Appoint a bot as a third-party verifier + description: Requires `botverification.manage`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [bot_id, company_name, version] + properties: + bot_id: { type: integer, format: int64 } + icon_document_id: { type: integer, format: int64 } + company_name: { type: string } + default_description: { type: string } + can_modify_custom_description: { type: boolean } + version: { type: integer, format: int64, description: "0 = new grant" } + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/botverification/verifiers/set-enabled: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Enable/disable a verifier bot + description: Requires `botverification.manage`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [bot_id, enabled], properties: { bot_id: { type: integer, format: int64 }, enabled: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/botverification/verifiers/revoke: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Strip a bot's verifier status + description: Requires `botverification.manage`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [bot_id], properties: { bot_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/botverification/icons/upsert: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Add/edit a verification-icon catalog entry + description: Requires `botverification.manage`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [document_id, name], properties: { document_id: { type: integer, format: int64 }, name: { type: string }, owner_bot_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/botverification/icons/set-active: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Enable/disable a verification icon + description: Requires `botverification.manage`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [icon_id, active], properties: { icon_id: { type: integer, format: int64 }, active: { type: boolean } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + + /v1/botverification/marks/revoke: + post: + tags: ["AdminAPI: Bot Verification"] + summary: Strip a third-party mark from a peer + description: Requires `botverification.manage`. + security: [{ adminApiBearer: [] }] + requestBody: + required: true + content: + application/json: + schema: + allOf: [{ $ref: "#/components/schemas/CommandMeta" }, { type: object, required: [verifier_bot_id, peer_type, peer_id], properties: { verifier_bot_id: { type: integer, format: int64 }, peer_type: { type: string, enum: [user, channel] }, peer_id: { type: integer, format: int64 } } }] + responses: { "200": { $ref: "#/components/responses/AdminApiCommandOK" }, "403": { $ref: "#/components/responses/AdminApiForbidden" } } + +components: + securitySchemes: + panelSession: + type: apiKey + in: cookie + name: telesrv_admin_session + description: Signed session cookie issued by POST /api/login. 12h default TTL, no server-side store. + panelCsrf: + type: apiKey + in: header + name: X-CSRF-Token + description: >- + Must match the telesrv_admin_csrf cookie's value. Required on every + mutating panel request except POST /api/login. Origin header is also + checked when present. + adminApiBearer: + type: http + scheme: bearer + description: >- + The master token (TELESRV_ADMIN_API_TOKEN, implicit "*" permission) + or a scoped token (TELESRV_ADMIN_SCOPED_TOKENS) carrying specific + permissions. Compared with subtle.ConstantTimeCompare against every + configured token unconditionally, to avoid timing side-channels. + + parameters: + PathId: + name: id + in: path + required: true + schema: { type: integer, format: int64 } + Limit: + name: limit + in: query + schema: { type: integer } + + requestBodies: + PanelVersionedDecision: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [version] + properties: + version: { $ref: "#/components/schemas/FlexInt64" } + internal_note: { type: string } + PanelUserAvatarUpload: + required: true + content: + multipart/form-data: + schema: + type: object + required: [metadata, file] + properties: + metadata: + description: "JSON — common fields + user_id (int64)" + type: string + file: { type: string, format: binary } + PanelAdminUserAction: + description: >- + Shared by create/update-access/set-password for a named admin + operator account. `reason` is mandatory here specifically + (`decodeAdminUserAction` 400s on blank reason), stricter than the + generic action envelope. + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + properties: + id: { type: integer, format: int64 } + username: { type: string } + password: { type: string } + permissions: { type: array, items: { type: string } } + enabled: { type: [boolean, "null"] } + PanelSetFrozen: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PanelCommandFields" + - type: object + required: [user_id, frozen] + properties: + user_id: { type: integer, format: int64 } + frozen: { type: boolean } + freeze_until: { type: string, format: date-time } + freeze_appeal_url: { type: string } + AdminApiVersionedNote: + required: true + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CommandMeta" + - type: object + required: [version] + properties: + version: { type: integer, format: int64 } + internal_note: { type: string } + + responses: + PanelUnauthorized: + description: >- + Missing/expired session (`{"error":"not authenticated"}`), or a + named-account session invalidated server-side since it was issued — + password changed, permissions edited, or account disabled — caught + on every request by re-checking `admin_console_users` against the + session's `epoch` claim (`{"error":"session is no longer valid"}`). + content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } + PanelForbidden: + description: >- + Missing/mismatched CSRF cookie or header, Origin mismatch (plain + `ErrorEnvelope`), or an authenticated session lacking the required + permission (`PermissionErrorEnvelope`, adds a `permission` field — + `{"error":"permission accounts.manage is required","code":"FORBIDDEN","permission":"accounts.manage"}`). + content: { application/json: { schema: { $ref: "#/components/schemas/PermissionErrorEnvelope" } } } + PanelNotFound: + description: Not found + content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } + PanelConflict: + description: >- + Optimistic-locking conflict — another reviewer already decided this + row, or `version` is stale. Only the verification/bot-verification + decision routes preserve this status from the upstream Admin API; + every other action route flattens upstream errors to 502 instead + (see `PanelActionUpstreamError`). + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + PanelActionOK: + description: >- + Command accepted. Response body is the same `CommandResult` shape + the Admin API returns — the panel relays it as-is from + `callAdminAPI`/`callAdminMultipart`/`callAdminCommand`, or builds an + equivalent `CommandResult` locally for the handful of actions that + operate on local state (admin operators, server settings) instead + of calling through to the Admin API. + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + PanelActionUpstreamError: + description: >- + The upstream Admin API call failed (transport error, or the command + itself failed). `writeCommandResultAPI` answers this as HTTP 502 + with `CommandResult{Status:"failed", Error: }` — + NOT the upstream's own status code. Verification and bot-verification + decision routes are the exception: they preserve the real upstream + status (404/409/429/400) instead of flattening to 502. + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + PanelCursorList: + description: Cursor-paginated list + content: + application/json: + schema: + type: object + properties: + query: { type: string } + limit: { type: integer } + rows: { type: array, items: { type: object, additionalProperties: true } } + has_more: { type: boolean } + next_before_id: { type: integer, format: int64 } + listing: { type: object, additionalProperties: true } + AdminApiCommandOK: + description: >- + Command result. Success is HTTP 200; a failed command (err != nil) + is answered as HTTP 400 (not 500) by the shared writeCommandResult + helper, with the same CommandResult body shape (Status:"failed"). + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + AdminApiCommandError: + description: Command failed (validation or execution error) + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + AdminApiForbidden: + description: Missing bearer token, or token lacks the required permission + content: { application/json: { schema: { $ref: "#/components/schemas/PermissionErrorEnvelope" } } } + AdminApiCodedError: + description: Coded error envelope + content: { application/json: { schema: { $ref: "#/components/schemas/CodedErrorEnvelope" } } } + AdminApiModerationError: + description: Moderation-family coded error (404/403/409/429/400/500 depending on admin.Code*) + content: { application/json: { schema: { $ref: "#/components/schemas/CodedErrorEnvelope" } } } + AdminApiCollectibleError: + description: Collectible-username coded error (404/409/400/500 depending on admin.Code*) + content: { application/json: { schema: { $ref: "#/components/schemas/CodedErrorEnvelope" } } } + AdminApiVerificationError: + description: >- + Official-verification coded error. Status derives from + admin.VerificationErrorCode: NotFound→404; + Conflict/TargetOccupied/TargetVerified→409; + {StatusInvalid,ReasonRequired,TargetInvalid,TargetNotPublic,TargetRestricted,TargetSystem,NotOwner,UserTargetsDisabled,Invalid}→400; + default→400 (forced down from 500 on the command path). On conflict, + `message` is overwritten with a "reload and decide again" prompt. + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + AdminApiBotVerificationError: + description: >- + Bot-verification coded error. Status derives from + admin.BotVerificationErrorCode: *NotFound codes→404; + {Conflict,Limit,RequestExists}→409; RateLimited→429; + {Forbidden,DescriptionForbidden,Invalid,IconInactive,IconInvalid,StatusInvalid,ReasonRequired,TargetInvalid,TargetSystem,VerificationInvalid}→400 + (explicitly 400 not 403 — the refusal is about the *subject* bot/peer, + not the caller's authorization); default→400. + content: { application/json: { schema: { $ref: "#/components/schemas/CommandResult" } } } + + schemas: + ErrorEnvelope: + type: object + required: [error] + properties: + error: { type: string } + code: { type: string } + CodedErrorEnvelope: + allOf: + - $ref: "#/components/schemas/ErrorEnvelope" + - type: object + required: [code] + PermissionErrorEnvelope: + allOf: + - $ref: "#/components/schemas/ErrorEnvelope" + - type: object + properties: + permission: { type: string, description: "present on 403s from a missing-permission check" } + + PanelCommandFields: + type: object + description: Common fields on every Panel API POST /api/actions/* (and moderation/verification decision) body. + required: [reason, confirm] + properties: + command_id: { type: string, description: "Idempotency key; server-generated if omitted." } + reason: { type: string, description: "Mandatory audit reason." } + confirm: { type: boolean, description: "Operator confirmation; must be true." } + + FlexInt64: + description: >- + Accepts either a JSON number or a numeric JSON string (custom + UnmarshalJSON on the Go side, `flexInt64`) — used for Telegram-scale + ids where a bare JSON number risks float64 precision loss in JS. + Distinct from a `json:",string"`-tagged field (see field + descriptions), which requires a JSON string specifically. + oneOf: + - { type: integer, format: int64 } + - { type: string } + FlexUnix: + description: >- + Accepts a JSON number (unix seconds), or a string — either a numeric + unix-seconds string or an RFC3339 date (`flexUnix`). + oneOf: + - { type: integer, format: int64, description: unix seconds } + - { type: string, description: "numeric unix-seconds string, or RFC3339" } + + CommandMeta: + type: object + description: Common fields embedded in nearly every Admin API (internal/adminapi) request body. + properties: + command_id: { type: string, description: "Idempotency key." } + actor: { type: string, description: "Auto-filled from the scoped-token principal name if omitted." } + reason: { type: string } + dry_run: { type: boolean } + + CommandResult: + type: object + description: Response shape for nearly every Admin API POST route. + properties: + command_id: { type: string } + action: { type: string } + status: { type: string } + already_executed: { type: boolean } + dry_run: { type: boolean } + target_user_id: { type: integer, format: int64 } + target_peer: { $ref: "#/components/schemas/Peer" } + message: { type: string } + details: { type: object, additionalProperties: true } + error: { type: string } + + Peer: + type: object + properties: + Type: { type: string, description: "domain.PeerType, e.g. user/channel/chat" } + ID: { type: integer, format: int64 } + + PeerColorInput: + type: object + description: Shared shape for every *-set-color request (accounts/channels, both APIs). + properties: + for_profile: { type: boolean } + has_color: { type: boolean } + color: { type: integer } + background_emoji_id: { type: integer, format: int64, description: "sent with json:\",string\" on the Admin API side" } + + EmojiStatusInput: + type: object + description: Shared shape for every *-set-emoji-status request (accounts/channels, both APIs). + properties: + document_id: { type: integer, format: int64, description: "sent with json:\",string\" on the Admin API side" } + until: { type: integer, description: unix seconds } + + AccountRow: + type: object + description: >- + `cmd/telesrv-admin/readstore.go` AccountRow — fields are NOT JSON-tagged, + so they serialize with Go's default PascalCase field names, not + snake_case like the rest of this API. + properties: + ID: { type: integer, format: int64 } + Phone: { type: string } + Username: { type: string } + FirstName: { type: string } + LastName: { type: string } + Collectibles: + type: array + description: Collectible usernames in display order; distinct from the editable Username slot. + items: + type: object + properties: { Username: { type: string }, Active: { type: boolean } } + CreatedAt: { type: string, format: date-time } + UpdatedAt: { type: string, format: date-time } + Frozen: { type: boolean } + Reason: { type: string, description: "The freeze reason given when Frozen was last set true. Empty if never frozen." } + Verified: { type: boolean } + Scam: { type: boolean } + Fake: { type: boolean } + PremiumUntil: { type: integer, format: int64 } + LastActiveAt: { type: string, format: date-time } + DeviceCount: { type: integer } + LoginEmail: { type: string } diff --git a/docs/reserved-usernames.md b/docs/reserved-usernames.md new file mode 100644 index 00000000..8d6db064 --- /dev/null +++ b/docs/reserved-usernames.md @@ -0,0 +1,73 @@ +# Suggested reserved usernames + +A starter blocklist for `TELESRV_RESERVED_USERNAMES` (see +`internal/config/config.go`, `ReservedUsernames`), beyond the shipped +default (`owpengram, admin, administrator, support, staff, moderator, +official, root, owner`). The mechanism blocks these, case-insensitively, +from self-service `account.updateUsername` / `channels.updateUsername` — +an operator can still assign one deliberately from the admin panel, since +that's not the squatting this exists to stop. + +Organized by why each entry is worth blocking. Trim or extend per your own +deployment — none of this is enforced by the server itself, it's just a +starting list to feed into the env var. + +## Official-sounding / staff-impersonation + +Names that would read as "this account speaks for the operator" if a +random user grabbed them. + +```text +telegram, telegramofficial, telegramsupport, telegramteam, telegramstaff, +telegramhq, telegramnews, telegramnotifications, telegramverify, telegrambot, +telegramservice, security, securityteam, verify, verification, verified, +helpdesk, helpcenter, customerservice, customersupport, servicedesk, service, +system, systemmessage, systembot, notifications, notification, alerts, +announcements, announcement, newsroom, press, pressoffice, spambot, antispam, +abuse, abusereport, reports, legal, compliance, billing, payments, finance, +accounts, accountservices, recovery, unlock, restore, backup, headquarters, +staffteam, modteam, moderation, moderators, founder, cofounder, management +``` + +## Founders / executives (real people this server is not affiliated with) + +Blocks an account from presenting as Telegram's actual leadership. + +```text +durov, pavel, paveldurov, pdurov, nikolai, nikolaidurov, ndurov, durovpavel, +telegramceo, telegramfounder +``` + +## Crypto / scam-bait + +The classic "free crypto giveaway" impersonation targets — official-sounding +project/exchange names, plus generic scam-pattern handles. + +```text +bitcoin, ethereum, toncoin, tonwallet, tonfoundation, crypto, cryptowallet, +wallet, airdrop, giveaway, bonus, freecoins, claim, claimnow, binance, +coinbase, kraken, metamask, trustwallet, ledger, opensea, uniswap, elonmusk, +tesla, spacex, cryptosupport, walletsupport, tonsupport, binancesupport, +investment, investmentteam, forextrading, tradingbot, signals, cryptosignals +``` + +## Suggested env var + +Comma-separated, matching `envListOr`'s parsing: + +```bash +TELESRV_RESERVED_USERNAMES=owpengram,admin,administrator,support,staff,moderator,official,root,owner,telegram,telegramofficial,telegramsupport,telegramteam,telegramstaff,telegramhq,telegramnews,telegramnotifications,telegramverify,telegrambot,telegramservice,security,securityteam,verify,verification,verified,helpdesk,helpcenter,customerservice,customersupport,servicedesk,service,system,systemmessage,systembot,notifications,notification,alerts,announcements,announcement,newsroom,press,pressoffice,spambot,antispam,abuse,abusereport,reports,legal,compliance,billing,payments,finance,accounts,accountservices,recovery,unlock,restore,backup,headquarters,staffteam,modteam,moderation,moderators,founder,cofounder,management,durov,paveldurov,pdurov,nikolai,nikolaidurov,ndurov,durovpavel,telegramceo,telegramfounder,bitcoin,ethereum,toncoin,tonwallet,tonfoundation,crypto,cryptowallet,wallet,airdrop,giveaway,bonus,freecoins,claim,claimnow,binance,coinbase,kraken,metamask,trustwallet,ledger,opensea,uniswap,elonmusk,tesla,spacex,cryptosupport,walletsupport,tonsupport,binancesupport,investment,investmentteam,forextrading,tradingbot,signals,cryptosignals +``` + +## What this doesn't cover + +- **Collectible usernames** (`internal/admin/service.go` collectible-username + actions) go through the admin panel/Admin API directly and aren't gated + by this list at all — an operator minting one bypasses it on purpose. +- **Prefix/substring squatting** (`telegram_real`, `xtelegram`, `te1egram`) + — the blocklist is an exact-match (case-insensitive) list, not a pattern + matcher, so lookalike spellings need their own entries if you want them + blocked too. +- This is a *username* blocklist only — it says nothing about display + names, bios, or profile photos, which can impersonate just as easily and + aren't restricted by `TELESRV_RESERVED_USERNAMES` at all. diff --git a/internal/app/channels/service.go b/internal/app/channels/service.go index f6ddba08..7d64d788 100644 --- a/internal/app/channels/service.go +++ b/internal/app/channels/service.go @@ -635,11 +635,19 @@ func (s *Service) AdminSetSettings(ctx context.Context, channelID int64, patch d return s.channels.SetChannelAdminSettings(ctx, channelID, patch) } -// AdminSetUsername force-sets or clears a channel username through the admin path. +// AdminSetUsername force-sets or clears a channel username through the admin +// path. Unlike UpdateUsername it does not consult config.ReservedUsernames -- +// an operator who deliberately reserved a word still needs to be able to hand +// it to a specific channel -- but format/length validity is still enforced, +// just against the lower minChannelUsernameLenAdmin floor. func (s *Service) AdminSetUsername(ctx context.Context, channelID int64, username string) (domain.Channel, error) { if s == nil || s.channels == nil || channelID == 0 { return domain.Channel{}, domain.ErrChannelInvalid } + username = normalizeChannelUsername(username) + if username != "" && !validChannelUsernameMinLen(username, minChannelUsernameLenAdmin) { + return domain.Channel{}, domain.ErrUsernameInvalid + } return s.channels.SetChannelUsernameAdmin(ctx, channelID, username) } @@ -2520,8 +2528,20 @@ func normalizeChannelUsername(username string) string { return strings.TrimSpace(username) } +// minChannelUsernameLenAdmin is the floor for an operator-assigned channel +// username via AdminSetUsername (the admin API/panel) -- shorter than what +// self-service channels.updateUsername allows, mirroring +// users.minUsernameLenAdmin: a deliberate short handle assigned by an +// operator is not the squatting the higher self-service minimum guards +// against. +const minChannelUsernameLenAdmin = 3 + func validChannelUsername(username string) bool { - if len(username) < 5 || len(username) > 32 { + return validChannelUsernameMinLen(username, 5) +} + +func validChannelUsernameMinLen(username string, minLen int) bool { + if len(username) < minLen || len(username) > 32 { return false } for i := 0; i < len(username); i++ { diff --git a/internal/app/users/service.go b/internal/app/users/service.go index b3e7fe87..3da6c874 100644 --- a/internal/app/users/service.go +++ b/internal/app/users/service.go @@ -116,7 +116,13 @@ func WithReservedUsernames(names []string) Option { } const ( - minUsernameLen = 5 + minUsernameLen = 5 + // minUsernameLenAdmin is the floor for an operator-assigned username via + // UpdateUsernameAdmin (the admin API/panel) -- shorter than what self-service + // account.updateUsername allows, since a deliberate short handle assigned by + // an operator (e.g. a 3-4 char official/brand account) is not the squatting + // the higher self-service minimum guards against. + minUsernameLenAdmin = 3 maxUsernameLen = 32 maxProfileNameRunes = 64 // bio 长度双档,对齐 appConfig about_length_limit_default=70 / @@ -383,7 +389,13 @@ func (s *Service) updateUsername(ctx context.Context, userID int64, username str return s.projectOne(ctx, self.ID, self) } if username != "" { - if !validUsername(username) || (enforceReserved && s.reserved.Contains(username)) { + minLen := minUsernameLen + if !enforceReserved { + // enforceReserved=false is exactly the admin-bypass path + // (UpdateUsernameAdmin) -- see minUsernameLenAdmin's doc comment. + minLen = minUsernameLenAdmin + } + if !validUsernameMinLen(username, minLen) || (enforceReserved && s.reserved.Contains(username)) { return domain.User{}, domain.ErrUsernameInvalid } var ( @@ -1040,7 +1052,11 @@ func normalizeUsername(username string) string { } func validUsername(username string) bool { - if len(username) < minUsernameLen || len(username) > maxUsernameLen { + return validUsernameMinLen(username, minUsernameLen) +} + +func validUsernameMinLen(username string, minLen int) bool { + if len(username) < minLen || len(username) > maxUsernameLen { return false } for i := 0; i < len(username); i++ {