tg_messenger
Standalone, reusable Telegram messenger client for manual chatting in DMs. Three interfaces (CLI / TUI / Web) over a shared UI-agnostic core, built on Telethon.
- Runs standalone in its own venv (
tg-messenger ...). - Reusable as an external dependency:
from tg_messenger.core import StandaloneTelegramClient. - Own independent StringSession storage (
~/.tg/), with optional injection of an externally supplied session string.
See PLAN.md for the full design and the TDD build sequence.
Quickstart
python -m venv .venv && ./.venv/bin/pip install -e ".[dev]"
./.venv/bin/tg-messenger --help
The base install is core + CLI — only telethon + pydantic + click, so the client
stays a lightweight import for other projects (the goal behind issue #6). Everything
heavier is an opt-in extra, pulled in only when you need that feature. Forget one and
the command fails with a copy-paste install hint, never a raw ModuleNotFoundError.
pip install tg-messenger # core + CLI — import StandaloneTelegramClient
pip install 'tg-messenger[web]' # + FastAPI web UI (tg-messenger serve)
pip install 'tg-messenger[tui]' # + Textual TUI (tg-messenger tui)
pip install 'tg-messenger[all]' # everything below at once
| Extra | Pulls in | Needed for |
|---|---|---|
[web] |
fastapi, uvicorn, jinja2 | tg-messenger serve |
[tui] |
textual | tg-messenger tui |
[crypto] |
cryptography | at-rest session encryption / SSO with the factory |
[agent] |
langchain, langgraph, deepagents | tg-messenger agent (LLM auto-reply) |
[interop] |
httpx | tg-messenger worker — task exchange with tg_content_factory |
[interop] is just httpx on purpose: core/ never talks HTTP, so the heavy [agent]
LLM stack and the worker's HTTP client are separate installs. [dev] pulls
[web,tui,crypto] so the full test/lint toolchain and every interface run locally.
Running it
1. Set your Telegram API credentials. Get an api_id / api_hash from
https://my.telegram.org (API development tools) and set TG_API_ID / TG_API_HASH —
see .env.example:
TG_API_ID=12345678
TG_API_HASH=abcdef1234567890abcdef1234567890
Put them in ~/.tg/.env for a persistent config that's read no matter which
directory you launch from — so tg-messenger tui works from anywhere. A .env in the
current directory also works, and is handy for a project-local override. The
precedence, highest first, is: real environment variables > cwd .env > ~/.tg/.env
(each layer only fills in what the higher ones didn't set).
You don't have to hand-edit the file. Save your keys interactively with:
tg-messenger config set-api
# (omit a flag to be prompted for it; the hash is hidden as you type)
# or pass them: tg-messenger config set-api --api-id 1234567 --api-hash abcd…
They are written to ~/.tg/.env at 0600 (the directory at 0700), merging with any
keys already there (e.g. SESSION_ENCRYPTION_KEY, TG_SEND_RATE) — never overwritten.
And if you just run tg-messenger login without any creds set yet, you'll be prompted
to save them right then, so a brand-new install goes straight to logged-in. Your
credential values are never echoed. They are required for every command that touches
Telegram.
2. Log in (phone → code → optional 2FA password). The code arrives in your Telegram app, not by SMS:
tg-messenger login --phone +1234567890
The session is saved under ~/.tg/sessions/, so you only log in once. (You can
also log in interactively from the Web or TUI — see below.) Everything the app
persists — sessions, logs and per-profile SQLite — lives under a single root
~/.tg/. Override it with TG_HOME; if ~/.tg/ doesn't exist yet but the
legacy ~/.tg_messenger/ does, that older directory is read in place (no data is
moved), so an existing login keeps working.
3. Start an interface — same core, pick whichever you like:
tg-messenger chat # interactive terminal REPL — see incoming, send replies
tg-messenger tui # full-screen Textual UI (needs [tui])
tg-messenger serve # web UI on http://127.0.0.1:8090 (needs [web])
Or run one-off commands without a UI:
tg-messenger dialogs # list your DMs (--groups for groups/channels)
tg-messenger read 7 # print history of dialog 7
tg-messenger send 7 "hello" # send a message
tg-messenger --help # every command
Add -v for DEBUG logging, --profile NAME to target a specific account (see
Multiple accounts).
Use as a library
tg_messenger ships a py.typed marker and a pinned public API — import the core
client without dragging in any UI stack:
import asyncio
from tg_messenger import StandaloneTelegramClient
async def main():
client = StandaloneTelegramClient(
api_id=12345,
api_hash="...",
external_session="...", # or session_name=... for on-disk StringSession
)
await client.connect()
for dialog in await client.dialogs():
print(dialog.id, dialog.title)
await client.send_text(dialog.id, "hello")
async for event in client.listen(): # incoming DMs
print(event.message.text)
asyncio.run(main())
The public surface (tg_messenger.__all__) also exports SessionStore, LoginFlow,
LOGIN_HINT, EventBus, run_with_flood_wait_retry and HandledFloodWaitError.
Search
Every dialog shows its id (id — title), and every front-end can search.
Find a dialog (by title, @username, or id — filtered locally over the
already-loaded list, no extra request):
tg-messenger dialogs --find ann # DMs whose title/username/id matches "ann"
tg-messenger dialogs --groups --find dev
The web UI has a search box above the dialog list; the TUI filters the list as you type into its search field.
Search messages inside a dialog (Telegram's own server-side search):
tg-messenger search 7 "invoice" # last messages in dialog 7 matching "invoice"
tg-messenger search -1001234567 "lunch" --limit 50
The web UI exposes the same via GET /dialogs/{id}/search?q=. There is intentionally
no global content search across all chats — that belongs to tg_content_factory.
Sending media
Send a file, photo, video, GIF or voice note through any front-end.
CLI — send takes --file plus optional media modifiers:
tg-messenger send 7 "look at this" --file ./photo.jpg # caption from TEXT
tg-messenger send 7 --file ./report.pdf --caption "Q3" # caption from --caption
tg-messenger send 7 --file ./note.ogg --voice # send as a voice note
tg-messenger send 7 --file ./clip.mp4 --video-note # round video note
tg-messenger send 7 --file ./photo.jpg --as-file # plain document, no preview
--voice, --video-note and --as-file are mutually exclusive. A missing path
fails fast (no network call) with a clear error.
TUI — the composer understands an @PATH syntax: type @ followed by a path
(quote it if it has spaces) and an optional caption.
@./photo.jpg # send the file, no caption
@"~/My Pics/cat.png" so cute # quoted path + caption
@/tmp/report.pdf Q3 results # path + caption
A non-existent path is reported in the TUI (a toast) and nothing is sent. A message
not starting with @ is sent as plain text as before.
Web — the 📎 button by the composer opens a file picker; the current composer text
becomes the caption. Uploads are capped at TG_WEB_MAX_UPLOAD_MB (default 50) — a
larger file is rejected with HTTP 413, an empty one with 400.
Logging in from the Web or TUI
You don't have to use the CLI login command — both the Web and the TUI can sign
you in interactively (phone → code → optional 2FA password):
- Web — when the served session isn't logged in, every page redirects to the
/tg-loginwizard: enter your phone, then the code Telegram sends (the page tells you where it went — usually the in-app "Telegram" service chat), then a 2FA password if your account has one. On success the session is saved and you land in the chat. WithTG_WEB_PASSset,/tg-loginsits behind the web password. - TUI —
tg-messenger tuiagainst a logged-out session opens a login screen instead of exiting: type the phone, press Enter, type the code, press Enter (and the 2FA password if asked). A wrong code is reported and you can retry; Ctrl+C quits cleanly. On success the dialog list loads as usual.
The whole flow runs over one connected client (Telegram binds the login code to
that single session), and the phone number and code are never written to the logs.
Web authorization
Set TG_WEB_PASS to put the whole web UI behind a password. With it, every route
sits behind an HMAC-cookie session: GET /login shows a password form, a correct
password (compared in constant time) sets a signed cookie valid for 7 days, and
GET /logout clears it. The cookie is signed with a per-process random key
(secrets.token_bytes), so a restart invalidates all sessions and the value cannot
be forged or extended. A wrong password is logged (WARNING with the client IP) and
delayed before a 401. Unauthenticated requests redirect browsers to /login and
return 401 to API/SSE callers (including GET /stream/{id}).
Without TG_WEB_PASS, a bind to 127.0.0.1 is unauthenticated as before, but
binding to a non-localhost host (e.g. --host 0.0.0.0) is refused — set the
password or pass --insecure (a deliberate, logged bypass).
TG_WEB_PASS=secret tg-messenger serve --host 0.0.0.0
This adds authentication, not transport encryption. There is no built-in HTTPS: terminate TLS at a reverse proxy (nginx/caddy) in front of the server.
Multiple accounts (profiles)
Each saved login is a profile (a session file under ~/.tg/sessions/).
Log in to as many as you like and pick one per run with the global --profile flag:
tg-messenger --profile work login --phone +1... # create/replace the "work" profile
tg-messenger --profile personal login --phone +1...
tg-messenger profiles # list saved profiles
tg-messenger --profile work dialogs # any command targets a profile
tg-messenger --profile personal serve # CLI / TUI / web all accept --profile
With more than one profile and no --profile, the CLI and TUI pop a selection
menu; a non-interactive shell errors instead of guessing. One process serves one
profile, and each non-default profile gets its own log file
(~/.tg/logs/tg_messenger_<profile>.log). The web exposes a read-only
GET /profiles listing saved profiles with the active one flagged.
Session encryption & SSO with tg_content_factory
By default sessions live as plaintext 0600 files under ~/.tg/sessions/.
Set SESSION_ENCRYPTION_KEY (and pip install 'tg-messenger[crypto]') to store them
encrypted instead — Fernet over a PBKDF2-derived key, format enc:v2:, byte-compatible
with tg_content_factory. A plaintext file
read under a key is lazily rewritten encrypted; an encrypted file read without the key errors
with a hint.
Two ways to share one login across both projects (single sign-on):
- Shared key (option A): put the same
SESSION_ENCRYPTION_KEYin both.envfiles — the encrypted session strings become mutually readable. - Export / import (option B):
tg-messenger login --export-session # prints the plaintext StringSession (full access!) tg-messenger login --import-session # reads a StringSession from stdin (no echo) and saves it
or inject directly as a library:StandaloneTelegramClient(..., external_session=STRING).
Session strings are never written to logs.
Interop with tg_content_factory (worker + agent tools)
Two cooperating projects, split by role: tg_messenger is the hands (it reads
and sends messages through your account) and tg_content_factory
is the memory + search (it indexes conversations and holds a task queue). They
talk over HTTP — and httpx lives only in tg_messenger/interop/, never in the
core.
Install the extra:
pip install 'tg-messenger[interop]'
Run the worker — it claims tasks from the factory, executes them and reports back:
tg-messenger worker --factory-url http://127.0.0.1:8000 \
--types dm_reply,chat_answer,fetch_history --interval 5
Task types: dm_reply/chat_answer ({peer, text} → send a message; with
{peer, prompt} the optional [agent] answers first), fetch_history and
fetch_dialogs (read and return serialized models). Auth to the factory is HTTP
Basic (empty username + TG_FACTORY_PASSWORD).
When TG_FACTORY_URL is set, the AI agent also gains two tools — factory_search
(recall from the factory's archive, beyond Telegram's recent history) and
factory_create_task (enqueue background work).
Worked example — "where to go in St. Petersburg": you DM the agent "посоветуй,
куда сходить на экскурсию в Питере"; it calls factory_search to pull what your
chats already said about СПб excursions from the factory's index, optionally
factory_create_task to have the factory compile a richer answer, and replies with
a grounded recommendation — memory (factory) plus hands (messenger).
Accepted risk (v1, full trust): tasks from the factory run on your account with no source authorization beyond the shared password — whoever can enqueue tasks drives your "hands". Trust the factory as you trust yourself; keep the password in env only, never in logs or the repo.
Outgoing rate limit (automated senders)
Several commands send on your behalf in the background — agent, heartbeat run,
worker, ghostwrite, and moderate (warn/notice actions). A systematically high
send rate is the main account-ban risk (worse than any single FloodWait), so a
token-bucket caps every outgoing message in the process.
The default cap is 20 messages/minute. You can override it with TG_SEND_RATE;
setting TG_SEND_RATE=0 explicitly turns the cap off (no ceiling). When it is
off, automated sender commands log a WARNING on start so the unbounded state is never
silent:
TG_SEND_RATE=0 tg-messenger agent
Scope: the cap is per-process, not per-account. Each running command (a separate
agent, worker, serve, tui, …) holds its own bucket, so two senders running at
once can put up to 2 × TG_SEND_RATE on the same account. If you run several senders
in parallel, size TG_SEND_RATE with that multiplication in mind (or run one at a time).
When the cap is reached, a send waits for the next token (nothing is lost) and logs a WARNING — it never errors. Reads (dialogs/history) are not limited.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tg_messenger-0.1.0.tar.gz.
File metadata
- Download URL: tg_messenger-0.1.0.tar.gz
- Upload date:
- Size: 802.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33b568811cf4ce3364ddf6e50a352c9b0c3f66da62f5494be37afe28686764a0
|
|
| MD5 |
19565e24658eb33c808ca49ea9640759
|
|
| BLAKE2b-256 |
83391874af7fdd197dd25fa86a6a3548bdad1b49535f935c6ba3fc3727d5aaa8
|
File details
Details for the file tg_messenger-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tg_messenger-0.1.0-py3-none-any.whl
- Upload date:
- Size: 288.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca6ccb346c22deb8fb4c11ffee59ee5c27f98bb324aea49c182219009c495378
|
|
| MD5 |
b4ef862e330cd328dbb79782e382ebdd
|
|
| BLAKE2b-256 |
dc110e30ef244780d38bc00b3aa50e5acc9a0ccc5dc87865abae8249af000955
|