{
  "markdown": "# docker-telethon-plus\n\n[![CI](https://github.com/psyb0t/docker-telethon-plus/actions/workflows/pipeline.yml/badge.svg?branch=main)](https://github.com/psyb0t/docker-telethon-plus/actions/workflows/pipeline.yml)\n[![version](https://raw.githubusercontent.com/psyb0t/docker-telethon-plus/badges/version.svg)](https://github.com/psyb0t/docker-telethon-plus/releases)\n[![license](https://raw.githubusercontent.com/psyb0t/docker-telethon-plus/badges/license.svg)](LICENSE)\n[![Docker Pulls](https://img.shields.io/docker/pulls/psyb0t/telethon-plus?style=flat-square)](https://hub.docker.com/r/psyb0t/telethon-plus)\n\nYour Telegram account, but it takes HTTP requests. Wraps [Telethon](https://codeberg.org/Lonami/Telethon) — the real MTProto userbot client, not that neutered Bot API garbage — behind a JSON HTTP API and a Model Context Protocol endpoint.\n\nSame tools, two doors. POST some JSON, or point your AI agent at `/mcp` and let it go nuts. Either way it's talking to Telegram as *you*, with full account access.\n\nOne login. One session string. Never type a code again.\n\n## Table of Contents\n\n- [How it works](#how-it-works)\n- [Quick start](#quick-start)\n- [First-time login](#first-time-login)\n- [Configuration](#configuration)\n- [Tools](#tools)\n- [HTTP API](#http-api)\n- [MCP](#mcp)\n- [Agent integrations](#agent-integrations)\n- [Development](#development)\n- [Tests](#tests)\n- [License](#license)\n\n## How it works\n\n```\n+-------------------+        +-----------------------+\n|  Any HTTP client  | -----> |  REST  /api/...       | --+\n+-------------------+        +-----------------------+   |\n                                                         |   +-----------+        Telegram\n+-------------------+        +-----------------------+   +-> |  Telethon | <----> Servers\n|  MCP-aware agent  | -----> |  /mcp  (Streamable    | --+   +-----------+       (MTProto)\n|  (Claude, etc.)   |        |   HTTP transport)     |\n+-------------------+        +-----------------------+\n```\n\nOne Telethon client. One async lock. Both surfaces share the same tool registry — no duplication, no weird state, no bullshit.\n\n## Quick start\n\n```yaml\nservices:\n  telethon-plus:\n    image: psyb0t/telethon-plus\n    ports:\n      - \"8080:8080\"\n    environment:\n      TELETHON_API_ID: \"123456\"\n      TELETHON_API_HASH: \"your-api-hash\"\n      TELETHON_SESSION: \"1Aa...long-string-from-login-helper...\"\n    restart: unless-stopped\n```\n\nGet `API_ID` / `API_HASH` from <https://my.telegram.org/apps>. Get the session string from the [login helper](#first-time-login) below.\n\n## First-time login\n\nTelegram makes you prove you're a human once — phone number, SMS code, optionally 2FA. Do it once, never again.\n\n```bash\ncp .env.example .env\n$EDITOR .env  # put in TELETHON_API_ID and TELETHON_API_HASH\n\nmake login\n```\n\n`make login` builds the image, runs the interactive flow, and shoves `TELETHON_SESSION` straight into your `.env`. That's it. Run `make run` and you're live.\n\nNo repo? No problem:\n\n```bash\ndocker run --rm -it \\\n  -e TELETHON_API_ID=123456 \\\n  -e TELETHON_API_HASH=your-api-hash \\\n  psyb0t/telethon-plus login\n```\n\nCopy the session string it spits out, set it as `TELETHON_SESSION`, done.\n\n> **The session string is full account access.** Whoever has it is you. Don't commit it, don't paste it in Slack, don't tattoo it anywhere.\n\n## Configuration\n\nAll config via environment variables. Copy `.env.example` to get the full list with comments.\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `TELETHON_API_ID` | yes | — | API ID from my.telegram.org |\n| `TELETHON_API_HASH` | yes | — | API hash from my.telegram.org |\n| `TELETHON_SESSION` | yes | — | StringSession from the login helper |\n| `TELETHON_HTTP_LISTEN_ADDRESS` | no | `0.0.0.0:8080` | `host:port` to bind |\n| `TELETHON_LOG_LEVEL` | no | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |\n| `TELETHON_REQUEST_TIMEOUT` | no | `60` | Per-request timeout in seconds |\n| `TELETHON_FLOOD_SLEEP_THRESHOLD` | no | `60` | Auto-sleep through `FLOOD_WAIT` errors below this many seconds. Telegram will rate-limit you — this is the safety valve. |\n| `TELETHON_DEVICE_MODEL` | no | `docker-telethon-plus` | What Telegram thinks your device is |\n| `TELETHON_SYSTEM_VERSION` | no | `1.0` | Ditto for OS |\n| `TELETHON_APP_VERSION` | no | `1.0` | Ditto for app |\n| `TELETHON_DOWNLOAD_DIR` | no | `/tmp/telethon-plus` | Scratch space for `send_file` uploads |\n| `TELETHON_AUTH_KEY` | no | `\"\"` | When set, all endpoints require `Authorization: Bearer <key>`. `/healthz` stays public. Empty = no auth. |\n\n### Throttling & cache (anti-flood)\n\nTelegram bans accounts that hammer it. Defaults here are conservative — meant to keep you under the server-side limits without you having to think about it. Tune only if you know what you're doing.\n\n| Variable | Default | What it does |\n|---|---|---|\n| `TELETHON_THROTTLE_ENABLED` | `true` | Master switch for all rate-limiting below |\n| `TELETHON_THROTTLE_GLOBAL_INTERVAL_MS` | `50` | Min gap between any two outgoing requests |\n| `TELETHON_THROTTLE_JITTER_MS` | `200` | Random ±jitter added on top (kills metronome traffic patterns) |\n| `TELETHON_THROTTLE_PER_CHAT_INTERVAL_MS` | `1100` | Min gap between sends to the same chat (Telegram's \"1/sec/chat\" ceiling, with margin) |\n| `TELETHON_THROTTLE_PER_CHAT_READ_INTERVAL_MS` | `250` | Min gap between reads from the same chat. Stops single-channel scraping from monopolizing the read bucket. |\n| `TELETHON_THROTTLE_ADAPTIVE` | `true` | On each `FLOOD_WAIT`, multiply all waits ×2 for an hour. Resets after a quiet hour. |\n| `TELETHON_BUCKET_RESOLVE_PER_MIN` | `5` | Cap on `resolveUsername` — **the main thing that gets accounts 22-hour-banned** |\n| `TELETHON_BUCKET_GET_FULL_PER_MIN` | `10` | Cap on `getFullChannel` / `getFullUser` / `get_participants` |\n| `TELETHON_BUCKET_JOIN_PER_HOUR` | `5` | Cap on channel/group joins |\n| `TELETHON_BUCKET_CREATE_PER_HOUR` | `5` | Cap on channel/group creation |\n| `TELETHON_BUCKET_SEND_PER_MIN` | `20` | Cap on sends across all chats |\n| `TELETHON_BUCKET_READ_PER_MIN` | `600` | Cap on read ops. **Counted per server-side API call**, not per tool call: `get_messages(limit=300)` charges 3 slots (Telegram caps GetHistory at 100/page); `get_dialogs(limit=500)` similarly charges 5. |\n| `TELETHON_CACHE_ENABLED` | `true` | Persist resolved entities to disk |\n| `TELETHON_CACHE_PATH` | `/cache/entities.json` | Mount `/cache` as a host volume to keep this across rebuilds |\n| `TELETHON_CACHE_TTL_SECONDS` | `604800` | 7 days. Set `0` for no expiry. |\n| `TELETHON_FLOOD_SLEEP_THRESHOLD` | `60` | Telethon's reactive auto-sleep: if a `FLOOD_WAIT` is shorter than this many seconds, sleep through it. Above this, raise. Set very high (e.g. `86400`) to never raise — but then a hostile FLOOD_WAIT will block your process for the full duration. |\n\n**How the layers stack:**\n\n1. Entity cache short-circuits resolveUsername — first lookup of `@somechannel` calls Telegram; every subsequent lookup is free. Survives container restarts via the `/cache` volume.\n2. Per-method token buckets cap the dangerous methods. If you try to resolve 6 new usernames in one minute, the 6th sleeps until the oldest expires.\n3. Per-chat send interval throttles sends to each chat to ≤1/sec (with margin).\n4. Global gap + jitter humanizes the overall traffic shape.\n5. Adaptive backoff: a single FLOOD_WAIT halves your effective rate for an hour. Three in a row → ÷8. Auto-recovers after an idle hour.\n\n**For bulk scraping work** (the scenario that caused 22-hour bans before): the cache + `bucket_resolve_per_min=5` combination is what saves you. Resolving 200 new channels takes ~40 minutes instead of getting you banned in 5.\n\n## Tools\n\nJSON in, JSON out. All inputs are pydantic-validated — send garbage, get a `400` back with exactly what's wrong.\n\nChat references (`chat`, `from_chat`, `to_chat`) accept whatever Telethon accepts:\n\n| Format | Example |\n|---|---|\n| Username | `@psyb0t` |\n| Phone number | `+1234567890` |\n| t.me link | `https://t.me/psyb0t` |\n| Numeric ID | `123456789` |\n| Supergroup/channel ID | `-1001234567890` |\n| Your own Saved Messages | `me` |\n\n> **Numeric IDs only resolve for entities Telethon has already seen** — i.e. cached in your session via a prior `@username` / `t.me` lookup, dialog list, or incoming message. MTProto needs an `access_hash`, not just an ID, and bare numbers don't carry one. Especially relevant for bots: pass `@botusername` first (or call `GET /api/dialogs` / `GET /api/entities?chat=@bot` once) before referring to it by numeric ID. If you only have the bot's token and no username, hit Telegram's Bot API `getMe` to fetch the username, then use that.\n\n### Response shape\n\nEvery 2xx returns the resource directly. No `{\"result\": ...}` wrapper, no envelope. Lists are JSON arrays, singles are JSON objects.\n\nErrors return `{\"detail\": \"...\"}` (FastAPI standard). Telegram RPC errors arrive as `502` with `detail = {\"telegram_error\": \"...\", \"message\": \"...\"}`.\n\n### Quick reference\n\n| Endpoint | Required params | What it does |\n|---|---|---|\n| `GET /api/me` | — | Account profile. |\n| `GET /api/entities` | `chat` | Resolve a username/ID/link to a profile. |\n| `POST /api/entities/bulk` | `chats` | Bulk resolve, honoring the resolve-username bucket. |\n| `GET /api/dialogs` | — | List dialogs. Optional: `limit`, `archived`, `search` (substring on title/@). |\n| `GET /api/messages` | `chat` | Read recent messages. Optional: `limit`, `offset_id`, `search`. |\n| `GET /api/messages/{id}` | `chat` | Fetch a single message. |\n| `GET /api/messages/{id}/media` | `chat` | Download attachment as **raw bytes** (binary stream). Returns `Content-Type` from Telegram + `Content-Disposition: attachment; filename=...`. Optional: `max_bytes`. MCP clients should use the `download_media` tool which returns base64. |\n| `POST /api/messages` | `chat`, **`text` or `file_url`** | Send a message. If `file_url` is present, fetches it and sends as media (with `text` as caption). Otherwise sends `text`. Optional: `parse_mode`, `reply_to`, `silent`, `link_preview`, `schedule`, `force_document`, `max_bytes`. |\n| `POST /api/messages/forward` | `from_chat`, `to_chat`, `message_ids` | Forward messages. |\n| `POST /api/messages/read` | `chat` | Mark as read. Optional `max_id`. |\n| `PATCH /api/messages/{id}` | `chat`, `text` | Edit. |\n| `DELETE /api/messages` | `chat`, `message_ids` | Bulk delete (body has the list). |\n| `POST /api/messages/{id}/pin` | `chat` | Pin. Optional `silent`, `pm_oneside`. |\n| `POST /api/messages/{id}/unpin` | `chat` | Unpin. |\n| `POST /api/messages/{id}/reactions` | `chat`, `emoji` | React. Optional `big`. |\n| `DELETE /api/messages/{id}/reactions` | `chat` | Remove your reaction. |\n| `GET /api/participants` | `chat` | List members. Optional: `limit`, `search`. |\n| `POST /api/chats` | `title` | Create supergroup or channel. |\n| `DELETE /api/chats` | `chat` | Delete supergroup/channel you own (body). |\n| `POST /api/chats/join` | `chat` | Join public channel/group. |\n| `POST /api/chats/invite` | `invite` | Join via private `t.me/+hash`. |\n| `POST /api/chats/leave` | `chat` | Leave. |\n| `GET /api/chats/{chat}/linked` | — | Resolve a channel's linked discussion group. |\n| `POST /api/chats/{chat}/admin/ban` | `user` | Ban a user. Optional `until_seconds`. |\n| `POST /api/chats/{chat}/admin/unban` | `user` | Lift a ban. |\n| `POST /api/chats/{chat}/admin/kick` | `user` | Kick (ban + immediate unban). |\n| `POST /api/chats/{chat}/admin/promote` | `user` | Grant admin rights + optional `title`. |\n| `POST /api/chats/{chat}/admin/demote` | `user` | Strip admin rights. |\n| `POST /api/polls` | `chat`, `question`, `options` | Create a poll. Optional `quiz`, `correct_option`, `solution`. |\n| `POST /api/polls/{id}/vote` | `chat`, `options` | Vote (0-based indices). |\n| `GET /api/polls/{id}/results` | `chat` | Current vote counts. |\n| `GET /api/throttle/status` | — | Live rate-limit + cache state. |\n| `GET /api/account/health` | — | Flood-risk tier. |\n| `GET /metrics` | — | Prometheus exposition (no auth). |\n| `WS /ws/updates` | `?token=...` | Stream incoming Telegram events. |\n\n**`{chat}` in path** accepts the same formats as elsewhere: `@username`, numeric ID, `me`. Phone (`+...`) and `t.me/...` links need URL-encoding (`%2B` etc.) — usernames and IDs work inline.\n\n## HTTP API\n\nStandard REST API. JSON in, JSON out. 2xx returns the resource directly; errors return `{\"detail\": ...}`.\n\nIf `TELETHON_AUTH_KEY` is set, every request (except `/healthz`) needs:\n\n```http\nAuthorization: Bearer your-secret-key\n```\n\n### GET /api/me\n\nWho am I right now.\n\n```http\nGET /api/me\n```\n\n```json\n{\n    \"id\": 123456789,\n    \"type\": \"User\",\n    \"username\": \"psyb0t\",\n    \"first_name\": \"Ciprian\",\n    \"phone\": \"+40...\"\n  }\n```\n\n### GET /api/entities\n\nResolve any chat reference to a full profile.\n\n```http\nGET /api/entities?chat=@telegram\n```\n\n```json\n{\n    \"id\": 1234567,\n    \"type\": \"Channel\",\n    \"username\": \"telegram\",\n    \"title\": \"Telegram\"\n  }\n```\n\n### GET /api/dialogs\n\n```http\nGET /api/dialogs?limit=10&archived=false\n```\n\n| Param | Type | Default | Description |\n|---|---|---|---|\n| `limit` | int | 20 | How many dialogs (1–200) |\n| `archived` | bool | false | Include archived chats |\n\n```json\n[\n  {\n    \"id\": 123456789,\n    \"type\": \"User\",\n    \"username\": \"someone\",\n    \"first_name\": \"Some\",\n    \"last_name\": \"One\",\n    \"unread_count\": 3,\n    \"pinned\": true,\n    \"last_message\": {\n      \"id\": 999,\n      \"date\": \"2026-04-29T11:00:00+00:00\",\n      \"chat_id\": 123456789,\n      \"sender_id\": 123456789,\n      \"text\": \"hey\",\n      \"out\": false,\n      \"reply_to_msg_id\": null,\n      \"media\": false,\n      \"media_type\": null\n    }\n  }\n]\n```\n\n### GET /api/messages\n\n```http\nGET /api/messages?chat=me&limit=5&search=hello\n```\n\n| Param | Type | Default | Description |\n|---|---|---|---|\n| `chat` | string | required | Chat to read from |\n| `limit` | int | 20 | How many messages (1–200) |\n| `offset_id` | int | 0 | Start from this message ID (pagination) |\n| `search` | string | — | Full-text search filter |\n\n```json\n[\n  {\n    \"id\": 4242,\n    \"date\": \"2026-04-29T12:00:00+00:00\",\n    \"chat_id\": 12345,\n    \"sender_id\": 67890,\n    \"text\": \"hello\",\n    \"out\": false,\n    \"reply_to_msg_id\": null,\n    \"media\": false,\n    \"media_type\": null\n  }\n]\n```\n\nNewest first. Returns `[]` if nothing matches.\n\n### POST /api/messages\n\nSend a message. The same endpoint handles **text** and **files** — if the body contains `file_url`, the URL is fetched and sent as media (with `text` becoming the caption); otherwise `text` is sent as a plain message.\n\n```http\nPOST /api/messages\nContent-Type: application/json\n\n{\n  \"chat\": \"@psyb0t\",\n  \"text\": \"**hello** from a container\",\n  \"parse_mode\": \"md\",\n  \"silent\": true,\n  \"reply_to\": 4241\n}\n```\n\n```http\nPOST /api/messages\nContent-Type: application/json\n\n{\n  \"chat\": \"@psyb0t\",\n  \"file_url\": \"https://example.com/photo.jpg\",\n  \"text\": \"look at this shit\",\n  \"silent\": true\n}\n```\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `chat` | string | required | Target chat |\n| `text` | string | required if no `file_url` | Message text or caption (1–4096 chars) |\n| `file_url` | string | null | HTTPS URL to fetch and send as media. Telegram auto-picks media type from extension/MIME; override with `force_document`. |\n| `parse_mode` | string | null | `md` / `markdown` / `html` / null |\n| `reply_to` | int | null | Message ID to reply to |\n| `silent` | bool | false | Send without notification |\n| `link_preview` | bool | true | Show link previews (text only) |\n| `schedule` | string | null | ISO datetime in the future to schedule the send |\n| `force_document` | bool | false | When sending a file: send as generic doc instead of letting Telegram pick |\n| `max_bytes` | int | 52428800 | When sending a file: reject larger than this (max 2 GB) |\n\n```json\n{\n    \"id\": 4242,\n    \"date\": \"2026-04-29T12:00:00+00:00\",\n    \"chat_id\": 12345,\n    \"sender_id\": 67890,\n    \"text\": \"hello from a container\",\n    \"out\": true,\n    \"reply_to_msg_id\": 4241,\n    \"media\": false,\n    \"media_type\": null\n  }\n```\n\n### PATCH /api/messages/{id}\n\nEdit a message. Message ID goes in the URL, everything else in the body.\n\n```http\nPATCH /api/messages/4242\nContent-Type: application/json\n\n{\n  \"chat\": \"me\",\n  \"text\": \"fixed version\",\n  \"parse_mode\": \"md\"\n}\n```\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `chat` | string | required | Chat containing the message |\n| `text` | string | required | New text (1–4096 chars) |\n| `parse_mode` | string | null | `md` / `html` / null |\n| `link_preview` | bool | true | Show link previews |\n\n```json\n{\n    \"id\": 4242,\n    \"date\": \"2026-04-29T12:00:00+00:00\",\n    \"chat_id\": 99999,\n    \"sender_id\": 123456789,\n    \"text\": \"fixed version\",\n    \"out\": true,\n    \"reply_to_msg_id\": null,\n    \"media\": false,\n    \"media_type\": null\n  }\n```\n\n### DELETE /api/messages\n\nDelete messages by ID.\n\n```http\nDELETE /api/messages\nContent-Type: application/json\n\n{\n  \"chat\": \"@psyb0t\",\n  \"message_ids\": [4242, 4243],\n  \"revoke\": true\n}\n```\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `chat` | string | required | Chat containing the messages |\n| `message_ids` | list[int] | required | IDs to delete (max 100) |\n| `revoke` | bool | true | Delete for everyone, not just yourself |\n\n```json\n{ \"deleted\": 2, \"requested\": 2 }\n```\n\n### POST /api/messages/forward\n\n```http\nPOST /api/messages/forward\nContent-Type: application/json\n\n{\n  \"from_chat\": \"@sourcechannel\",\n  \"to_chat\": \"me\",\n  \"message_ids\": [101, 102, 103],\n  \"silent\": true\n}\n```\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `from_chat` | string | required | Source chat |\n| `to_chat` | string | required | Destination chat |\n| `message_ids` | list[int] | required | IDs to forward (max 100) |\n| `silent` | bool | false | Forward without notification |\n\n```json\n[\n  {\n    \"id\": 5001,\n    \"date\": \"2026-04-29T12:01:00+00:00\",\n    \"chat_id\": 99999,\n    \"sender_id\": 123456789,\n    \"text\": \"forwarded content here\",\n    \"out\": true,\n    \"reply_to_msg_id\": null,\n    \"media\": false,\n    \"media_type\": null\n  }\n]\n```\n\n### POST /api/messages/read\n\nMark messages as read.\n\n```http\nPOST /api/messages/read\nContent-Type: application/json\n\n{ \"chat\": \"@psyb0t\", \"max_id\": 0 }\n```\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `chat` | string | required | Chat to mark as read |\n| `max_id` | int | 0 | Mark up to this message ID. `0` = mark all. |\n\n```json\n{ \"ok\": true }\n```\n\n### GET /api/participants\n\nList members of a group or channel.\n\n```http\nGET /api/participants?chat=-1001234567890&limit=50&search=john\n```\n\n| Param | Type | Default | Description |\n|---|---|---|---|\n| `chat` | string | required | Group or channel |\n| `limit` | int | 100 | Max members to return (1–1000) |\n| `search` | string | — | Filter by name |\n\n```json\n[\n  { \"id\": 123456789, \"type\": \"User\", \"username\": \"johndoe\", \"first_name\": \"John\" }\n]\n```\n\nLarge public channels may return a limited set or require admin rights.\n\n### POST /api/chats\n\nCreate a supergroup or broadcast channel.\n\n```http\nPOST /api/chats\nContent-Type: application/json\n\n{ \"title\": \"my-group\", \"megagroup\": true }\n```\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `title` | string | required | Group name (1–255 chars) |\n| `megagroup` | bool | true | `true` = supergroup, `false` = broadcast channel |\n\n```json\n{ \"id\": 1234567890, \"type\": \"Channel\", \"title\": \"my-group\" }\n```\n\n### DELETE /api/chats\n\nDelete a supergroup or channel you own. **Irreversible.**\n\n```http\nDELETE /api/chats\nContent-Type: application/json\n\n{ \"chat\": \"-1001234567890\" }\n```\n\n```json\n{ \"ok\": true }\n```\n\n### POST /api/chats/join\n\nJoin a public channel or supergroup.\n\n```http\nPOST /api/chats/join\nContent-Type: application/json\n\n{ \"chat\": \"@somegroup\" }\n```\n\n```json\n{ \"ok\": true }\n```\n\n### POST /api/chats/leave\n\nLeave a channel or supergroup.\n\n```http\nPOST /api/chats/leave\nContent-Type: application/json\n\n{ \"chat\": \"@somegroup\" }\n```\n\n```json\n{ \"ok\": true }\n```\n\n### Errors\n\n| Status | When |\n|---|---|\n| `400` | Bad JSON, validation failure, or Telethon said the input is nonsense. Body has details. |\n| `401` | Missing or wrong Bearer token (only when `TELETHON_AUTH_KEY` is set). |\n| `404` | Unknown endpoint. |\n| `502` | Telegram threw an RPC error (`FloodWaitError`, `ChatWriteForbiddenError`, etc.). Body has the error class and message. |\n\n### Health\n\n```http\nGET /healthz\n```\n\n```json\n{ \"status\": \"ok\", \"authorized\": true }\n```\n\n`authorized: false` means the container started but the session is fucked — bad string, revoked, or Telegram unreachable. Always public, no auth required.\n\n## Observability\n\n### `GET /metrics`\n\nPrometheus exposition. Scrape it. No auth (publicly readable inside your cluster — if you need it locked down, put it behind your usual scrape-network policy).\n\nSeries exported:\n- `telethon_plus_tool_calls_total{tool=...}`\n- `telethon_plus_tool_errors_total{tool=...}`\n- `telethon_plus_flood_events_total{bucket=...}`\n- `telethon_plus_cache_hits_total` / `_misses_total` / `_entries`\n- `telethon_plus_throttle_multiplier`\n- `telethon_plus_bucket_used{bucket=...}` / `_bucket_limit{bucket=...}`\n- `telethon_plus_uptime_seconds`\n\n### `GET /api/throttle/status`\n\nLive state of every bucket, multiplier, recent flood events, cache stats:\n\n```json\n{\n  \"throttle\": {\n    \"enabled\": true,\n    \"adaptive\": true,\n    \"multiplier\": 1.0,\n    \"flood_events_1h\": 0,\n    \"buckets\": {\n      \"resolve_username\": {\"used\": 0, \"limit\": 5, \"window_seconds\": 60},\n      \"send\": {\"used\": 0, \"limit\": 20, \"window_seconds\": 60}\n    },\n    \"tracked_chats_send\": 12,\n    \"tracked_chats_read\": 30,\n    \"global_interval_ms\": 50,\n    \"per_chat_interval_ms\": 1100,\n    \"per_chat_read_interval_ms\": 250,\n    \"jitter_ms\": 200\n  },\n  \"cache\": {\"entries\": 247},\n  \"read_only\": false,\n  \"dry_run\": false\n}\n```\n\n### `GET /api/account/health`\n\n```json\n{\n  \"authorized\": true,\n  \"risk\": \"ok\",\n  \"multiplier\": 1.0,\n  \"flood_events_1h\": 0,\n  \"read_only\": false,\n  \"dry_run\": false\n}\n```\n\n`risk` is `ok` (multiplier 1×), `warning` (≥2×), or `high` (≥8×).\n\n### Throttle response headers\n\nEvery `/api/...` response carries:\n- `X-Throttle-Multiplier`\n- `X-Throttle-Flood-Events-1h`\n- `X-RateLimit-Remaining-<bucket>` for each bucket\n\nLets clients self-throttle without polling `/api/throttle/status`.\n\n## Updates / webhooks\n\nTwo ways to receive incoming Telegram events (new messages, edits, deletes, chat actions):\n\n### WebSocket — `/ws/updates`\n\n```javascript\nconst ws = new WebSocket('ws://your-host:8080/ws/updates?token=YOUR_AUTH_KEY');\nws.onmessage = (e) => console.log(JSON.parse(e.data));\n```\n\nIf `TELETHON_AUTH_KEY` is set, pass it as `?token=`. Browser WS clients can't set Authorization headers, hence query-string.\n\nMultiple subscribers are supported — each gets its own queue. Slow consumers drop events at `TELETHON_UPDATES_BUFFER_SIZE`.\n\n### Outbound webhook — `TELETHON_POST_TO_URL`\n\nSet the env var to your endpoint and every event gets `POST`ed there as JSON. Fire-and-forget — a slow or failing webhook never blocks Telethon's loop. Use for distributed workers, separate processes, anything that can't hold a WS open.\n\n### Event payload shape\n\n```json\n{\n  \"type\": \"NewMessage\",\n  \"message\": {\n    \"id\": 4242,\n    \"date\": \"2026-04-29T12:00:00+00:00\",\n    \"text\": \"hi\",\n    \"out\": false,\n    \"sender_id\": 12345,\n    \"chat_id\": -1001234567890,\n    \"reply_to_msg_id\": null,\n    \"media\": false,\n    \"media_type\": null\n  },\n  \"chat_id\": -1001234567890\n}\n```\n\nFor `MessageEdited`, `MessageDeleted`, `ChatAction` — same shape, fewer fields.\n\n## Safety switches\n\n### `TELETHON_READ_ONLY=true`\n\nEvery write endpoint returns `403`. Reads, status, metrics still work. Killswitch for panic mode or for keeping a test deployment harmless.\n\n### `TELETHON_DRY_RUN=true`\n\nWrite endpoints accept the request, validate it, **don't** call Telegram, return `{\"dry_run\": true, \"would_...\": {...}}`. Useful for verifying scripts before pointing them at production.\n\n## MCP\n\nMounted at `/mcp/` using the [streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http). Every tool from the table above shows up automatically as an MCP tool with the same name and schema.\n\nPoint your agent at:\n\n```\nhttp://your-host:8080/mcp/\n```\n\nStateless — every request is independent, no session juggling. Drop it into Claude Desktop, a custom agent, anything that speaks MCP over HTTP. Works out of the box.\n\n## Agent integrations\n\nThe [skill](.agents/skills/telethon-plus) works in any agent that reads `.agents/skills/`, and installs natively in the clients below.\n\n### Claude Code\n\n```bash\nclaude plugin marketplace add psyb0t/agents\nclaude plugin install telethon-plus@psyb0t\n```\n\nClaude Code prompts for the telethon-plus server URL and, if auth is enabled, the bearer token — the token is stored in your OS keychain.\n\n### Codex\n\n```bash\ncodex plugin marketplace add psyb0t/agents\ncodex plugin add telethon-plus@psyb0t\n```\n\nInstalled via the marketplace, the skill invokes as `$telethon-plus:telethon-plus`. Codex also picks the skill up automatically with no install in any repo containing `.agents/skills/`, where it invokes as plain `$telethon-plus`.\n\n### OpenClaw\n\nThe skill is published to ClawHub on every release:\n\n```bash\nopenclaw skills install @psyb0t/telethon-plus\n```\n\nFor MCP clients that speak local stdio, the [`@psyb0t/telethon-plus`](.agents/plugins/telethon-plus) plugin bridges to the service's `/mcp/` endpoint:\n\n```bash\nopenclaw plugins install clawhub:@psyb0t/telethon-plus\n```\n\nThen set `TELETHON_PLUS_URL` (and `TELETHON_AUTH_KEY` if the server requires auth).\n\n## Development\n\n```bash\nmake build        # build psyb0t/telethon-plus:latest\nmake build-test   # build psyb0t/telethon-plus:latest-test\nmake run          # run locally on :8080 (reads .env)\nmake login        # interactive login — writes TELETHON_SESSION to .env automatically\nmake lint         # flake8 + pyright\nmake format       # isort + black\nmake test         # run integration tests in Docker\nmake clean        # remove built images\n```\n\n## Tests\n\nReal tests. Real Telegram. No mocking bullshit.\n\n`tests/` spins up the container and hammers both REST and MCP with your actual account. Messages get sent and deleted. If anything breaks, you'll know.\n\nSetup:\n\n```bash\ncp .env.example .env\n$EDITOR .env  # needs TELETHON_API_ID, TELETHON_API_HASH, TELETHON_SESSION, TEST_CHAT\n\nmake test\n```\n\n`TEST_CHAT` is where test messages land. Use `me` for Saved Messages — private, yours, no one else sees it. All chat reference formats from the [Tools](#tools) section work here.\n\n`make test` builds both images and runs pytest inside Docker with the socket mounted. No setup beyond `.env`. If credentials are missing, the suite skips cleanly.\n\n| Test file | What it beats on |\n|---|---|\n| `test_health.py` | Container boots, auth succeeds, OpenAPI spec has all the routes. |\n| `test_rest.py` | Validation errors, extra fields rejected, send → edit → fetch → delete roundtrip, dialogs, entity resolution, public channel read, group create/delete, participants. |\n| `test_mcp.py` | MCP streamable HTTP: tool discovery, `get_me`, send + delete roundtrip, validation errors come back as `isError`. |\n| `test_auth.py` | Auth middleware: 401 on missing/wrong token, 200 on correct token, `/healthz` always public, MCP endpoint protected too. |\n\n## License\n\n[WTFPL](LICENSE) — do whatever the fuck you want.\n",
  "bytes": 27437,
  "sha": "6c3644c11667ba815919c52e9144375401d44e338d77813efb46f71e783d248b",
  "repo_slug": "psyb0t/docker-telethon-plus",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_psyb0t_telethon_plus_854aa79f/readme"
}