Skip to main content

Bulk-leave Telegram groups and channels whose title matches a keyword. Dry run by default.

Project description

tg-bulk-leave

Bulk-leave Telegram groups and channels whose title matches a keyword.

Built for the situation where you've accumulated hundreds of chats — partner groups, deal rooms, airdrop channels, one-off intro chats — and want them gone without clicking through each one.

Leaving is irreversible. For a private group you'd need a fresh invite to get back in. The tool defaults to a dry run and requires an explicit typed confirmation before it touches anything.


How it works

scan all dialogs  →  title matches a keyword?  →  title matches protected?  →  leave
                              │ no                        │ yes
                              └── skip ───────────────────┴── skip

Matching is a case-insensitive substring test against the chat title. A chat is a candidate if any keyword appears in its title, unless any protected string does — protection always wins.

DMs are never touched. Everything runs locally on your machine: your session and credentials never leave it.

Install

Requires Python 3.11+.

uv tool install tg-bulk-leave        # recommended
# or
pipx install tg-bulk-leave

Until the first PyPI release, install from a clone instead:

git clone https://github.com/soos3d/tg-bulk-leave && cd tg-bulk-leave
uv tool install .                    # or: pipx install .

Setup

1. Get API credentials

Go to my.telegram.orgAPI development tools and create an app (any title, platform "Desktop" is fine). Note the api_id and api_hash.

  • The login code arrives inside the Telegram app, not by SMS.
  • The api_hash is a secret and cannot be revoked — treat it like a password.

Getting a bare "ERROR" on app creation? This is a known my.telegram.org issue (Telethon #4661): turn off any VPN/proxy/custom DNS, use a connection whose IP is in the same country as your phone number, try another browser or network, and retry later.

2. Provide the credentials

Export them, or put them in a .env file in the directory you run from (see .env.example; exported variables win over .env):

TG_API_ID=1234567
TG_API_HASH=your_api_hash_here

3. Configure keywords

Either per run, on the command line:

tg-bulk-leave --keyword acme --protected "Acme Alumni"

or persistently in a TOML config file (see config.example.toml):

keywords  = ["acme", "airdrop"]   # leave chats matching any of these
protected = ["Acme Alumni"]       # ...except these, always

The file lives in your platform's config directory — tg-bulk-leave --help prints the exact path (macOS: ~/Library/Application Support/tg-bulk-leave/config.toml, Linux: ~/.config/tg-bulk-leave/config.toml) — or pass --config PATH.

Flags and file combine safely: --keyword replaces the file's keywords for that run, while --protected only ever adds protection — a command-line flag can never drop a safeguard you configured.

Usage

# 1. Dry run — lists every match, leaves nothing. Always start here.
tg-bulk-leave

# 2. Cautious first real run — leave only the first 5 matches.
tg-bulk-leave --execute --limit 5

# 3. The real thing.
tg-bulk-leave --execute

--execute prints the full list, then requires you to type LEAVE (exact case) to proceed. Anything else aborts.

Read the dry run properly. Substring matching casts a wide net, and a short keyword gets swallowed by longer words — "art" also matches Smart Contract Devs. Expand the protected list until the dry-run list contains only chats you're willing to lose, and use --limit for your first real run.

First run

You'll be asked for your phone number and the login code Telegram sends you (plus your 2FA password, if set). A session file is then written to ~/.config/tg_cleanup/ (override with TG_SESSION) so later runs skip the login.

Output

Every run writes timestamped CSVs into reports/ (created in the directory you run from), 0600 inside a 0700 directory:

File When Columns
reports/telegram_matches_<stamp>.csv every run title, type, id, username
reports/telegram_left_<stamp>.csv --execute only title, type, id, result

The leave log is flushed after every chat, so if the run dies partway you still have an accurate record of what actually happened.

Crashed run? Just rerun it — chats you've already left are skipped.

Note that leaving a group does not remove it from your Telegram dialog list; the conversation and its history stay until you delete it. Membership is therefore checked explicitly via the left flag (and the ChatForbidden / ChannelForbidden types), not by absence from the dialog list. Without that check a rerun would retry every chat you'd already left.

Defunct legacy groups are skipped for the same reason: a deleted group (deactivated) and one upgraded to a supergroup (migrated_to) both linger in the dialog list without setting the left flag, and neither can be left — the supergroup appears separately and holds the real membership.

Rate limiting

Telegram throttles bulk leaves hard. The tool sleeps a random 2.5–5.0s between chats and backs off when told to. These delays are deliberately not configurable — lowering them earns a much longer ban.

If Telegram demands a wait longer than the abort cap (300s), the run aborts rather than silently parking for hours — rerun later to continue.

Security

The Telethon session file is the sensitive artifact here — it grants full access to your account with no login code. It is deliberately stored outside any repository (~/.config/tg_cleanup/) and *.session is gitignored. Every run re-applies 0700 to that directory and 0600 to the session file, so it stays owner-only whatever your umask is — and an existing world-readable session from an older version is tightened on the next run. Treat it like a password; don't sync it or commit it.

Credentials live only in .env or the environment, never in source. The generated CSVs list every chat you belong to, so they're written 0600 and gitignored too.

Chat titles are attacker-controlled — anyone can name a group =HYPERLINK(...). Titles are escaped before being written to CSV so they can't execute when you open the report in a spreadsheet.

Development

git clone https://github.com/soos3d/tg-bulk-leave && cd tg-bulk-leave
uv sync --dev

uv run pytest        # coverage gate at 80%, configured in pyproject.toml
uv run ruff check

See CONTRIBUTING.md for the safety invariants a change must not break, and SECURITY.md for the threat model.

The suite covers the logic that decides what gets left: keyword/protected precedence, config loading and flag semantics, entity classification, the DeleteChatUserRequest vs LeaveChannelRequest dispatch, FloodWait backoff and abort, CSV escaping, file permissions, and the confirmation gate. Tests use real Telethon entity types rather than stubs, so classification bugs surface.

One subtlety worth knowing if you touch leave(): legacy groups and channels need different API calls, and you can't tell them apart by flags. A gigagroup has both broadcast=False and megagroup=False, so flag-sniffing misclassifies it as a legacy group and sends a channel id to DeleteChatUserRequest — which can act on an unrelated chat. Dispatch on type (isinstance), and note that ChatForbidden / ChannelForbidden are not subclasses of Chat / Channel.

Roadmap

This project is free, open source, and local-first — your session never leaves your machine, and that stays true.

  • Now: proper packaging (this release), then PyPI publication, CI, and a SECURITY.md.
  • Later, maybe: an optional hosted service for recurring cleanup (e.g. auto-leave groups with no activity for 30+ days), which needs an always-on machine. If that ever ships, it will be a separate opt-in product; the CLI remains free and fully local. If you'd want such a service, open an issue — that's how demand gets measured.

Requirements

Python 3.11+, Telethon 1.44.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tg_bulk_leave-0.1.0.tar.gz (47.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tg_bulk_leave-0.1.0-py3-none-any.whl (14.5 kB view details)

Uploaded Python 3

File details

Details for the file tg_bulk_leave-0.1.0.tar.gz.

File metadata

  • Download URL: tg_bulk_leave-0.1.0.tar.gz
  • Upload date:
  • Size: 47.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for tg_bulk_leave-0.1.0.tar.gz
Algorithm Hash digest
SHA256 275c432f7d638d074ad5d748da5d5090d94ec10b1ee7538d5240692263f9bf66
MD5 5d822f8aadfb2b98950ce1023f77a835
BLAKE2b-256 9bc870d326336e8e25f0b26f1804ddc4149ada9c610b917bca678629002b0297

See more details on using hashes here.

Provenance

The following attestation bundles were made for tg_bulk_leave-0.1.0.tar.gz:

Publisher: release.yml on soos3d/tg-bulk-leave

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tg_bulk_leave-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tg_bulk_leave-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for tg_bulk_leave-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c8173eec462ea2ad1191c6b7b9bb61de1bc186296bb18e8728b352ea37d380d7
MD5 fa7801e39cf45fc7dd9578a5dbd565bc
BLAKE2b-256 ba3df07e399fdced8745bcb43cea0d5fb84f9562f5f90d4c252a787871b74908

See more details on using hashes here.

Provenance

The following attestation bundles were made for tg_bulk_leave-0.1.0-py3-none-any.whl:

Publisher: release.yml on soos3d/tg-bulk-leave

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page