telegram-userbot-cli 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/PKG-INFO +47 -1
  2. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/README-zh.md +44 -0
  3. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/README.md +46 -0
  4. telegram_userbot_cli-0.4.0/ROADMAP.md +82 -0
  5. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/pyproject.toml +1 -1
  6. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/__init__.py +1 -1
  7. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/auth.py +38 -15
  8. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/chats.py +212 -1
  9. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/cli.py +5 -2
  10. telegram_userbot_cli-0.4.0/src/tgcli/contacts.py +173 -0
  11. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/dialogs.py +117 -1
  12. telegram_userbot_cli-0.4.0/src/tgcli/groups.py +534 -0
  13. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/media.py +40 -2
  14. telegram_userbot_cli-0.2.0/ROADMAP.md +0 -68
  15. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/.env.example +0 -0
  16. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  17. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/.github/workflows/release.yml +0 -0
  18. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/.gitignore +0 -0
  19. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/LICENSE +0 -0
  20. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/__main__.py +0 -0
  21. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/client.py +0 -0
  22. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/config.py +0 -0
  23. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/messages.py +0 -0
  24. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/output.py +0 -0
  25. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/src/tgcli/resolve.py +0 -0
  26. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: telegram-userbot-cli
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Full-featured Telegram command-line client powered by a Telethon userbot (your own account, not a bot token)
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -27,8 +27,12 @@ no daemon, no external service**. Everything the CLI does lives in the CLI.
27
27
  - QR-code login (`tg login --qr`) with an independent session
28
28
  - Messaging: send / reply / edit / delete / forward / polls / schedule /
29
29
  drafts / reactions / pinning
30
+ - Group administration: create groups/channels, invite / kick / ban,
31
+ admin promotion with granular rights, slow mode, forum topics, admin log
30
32
  - Chat management: archive, mute, edit title / description / photo,
31
33
  invite links, clear history
34
+ - Contacts, privacy settings, blocking, dialog folders, multi-account,
35
+ voice transcription
32
36
  - Media: download, send files / albums / voice notes / stickers / GIFs,
33
37
  contact cards
34
38
  - Chats: list dialogs, history, pinned messages, in-chat & global search,
@@ -135,6 +139,48 @@ tg logout # remove the session
135
139
  | `tg status <user>` | Show a user's online status |
136
140
  | `tg common-chats <user>` | List chats you share with a user |
137
141
 
142
+ ### Group administration (v3)
143
+
144
+ | Command | Description |
145
+ |---|---|
146
+ | `tg new-group <title> <user...>` | Create a basic group with initial members |
147
+ | `tg new-channel <title> [--about text]` | Create a channel; `--group` creates a supergroup |
148
+ | `tg invite <chat> <user...>` | Invite users (supergroups & basic groups) |
149
+ | `tg kick <chat> <user...>` | Remove users (supergroup kick = view-messages ban; `unban` to reverse) |
150
+ | `tg leave <chat>` | Leave a group/channel |
151
+ | `tg ban <chat> <user> [days]` | Ban a user (permanent unless days given); supergroups only |
152
+ | `tg unban <chat> <user>` | Unban a user |
153
+ | `tg banned <chat>` | List banned users (`--json` supported) |
154
+ | `tg admins <chat>` | List admins with ranks |
155
+ | `tg promote <chat> <user> [--title rank]` | Promote with the standard right set |
156
+ | `tg demote <chat> <user>` | Remove admin rights |
157
+ | `tg admin-rights <chat> <user> <flags...>` | Set exact rights: `--change-info --post-messages --edit-messages --delete-messages --ban-users --invite-users --pin-messages --add-admins --anonymous --manage-call --other --manage-topics …`; `--rank` |
158
+ | `tg slow-mode <chat> [seconds]` | Set slow mode (0 = disable); supergroups only |
159
+ | `tg topics <chat>` | List forum topics |
160
+ | `tg topic-create <chat> <title> [text]` | Create a forum topic, optionally with a first message |
161
+ | `tg admin-log <chat> [-n]` | Recent admin actions |
162
+
163
+ ### Contacts, privacy & misc (v4)
164
+
165
+ | Command | Description |
166
+ |---|---|
167
+ | `tg contact add <user> [name] [--phone]` | Add/update a contact |
168
+ | `tg contact del <user...>` | Delete contacts (by name/id or `+phone`) |
169
+ | `tg contact import <file>` | Bulk import from `.json` / `.csv` (`phone,first_name,last_name`) |
170
+ | `tg contact export [file]` | Export contacts as re-importable JSON (default stdout) |
171
+ | `tg photo-del [--all]` | Delete your most recent profile photo (or all) |
172
+ | `tg privacy get [key...]` | Show privacy settings (default: all) |
173
+ | `tg privacy set <key> all\|contacts\|nobody` | Change a privacy setting; `--allow USER` / `--deny USER` refine it |
174
+ | `tg block <user>` / `tg unblock <user>` / `tg blocked` | Block management |
175
+ | `tg folders` | List dialog folders |
176
+ | `tg folder-create <name> --chat <c>...` | Create a folder with chats |
177
+ | `tg folder-assign <folder> <chat...>` | Add chats to a folder |
178
+ | `tg folder-del <folder>` | Delete a folder |
179
+ | `tg folder-order <folder...>` | Reorder folders (pass all in the wanted order) |
180
+ | `tg accounts` | List configured accounts |
181
+ | `tg login --account <label>` | Log in an additional account |
182
+ | `tg vtr <chat> <msg_id>` | Transcribe a voice note (Telegram Premium required) |
183
+
138
184
  Global flags: `--account` (v1: default only), `--json` (raw JSON output).
139
185
  Exit codes: `0` ok, `1` command error, `2` config/connection/session error,
140
186
  `130` interrupted.
@@ -13,7 +13,9 @@
13
13
 
14
14
  - 扫码登录(`tg login --qr`),使用独立会话
15
15
  - 消息:发送 / 引用回复 / 编辑 / 删除 / 转发 / 投票 / 定时 / 草稿 / 表情回应 / 置顶
16
+ - 群组管理:建群 / 建频道、邀请 / 踢人 / 封禁、管理员升降级(细粒度权限)、慢速模式、论坛话题、管理日志
16
17
  - 聊天管理:归档、免打扰、修改群名 / 简介 / 群头像、邀请链接、清空历史
18
+ - 联系人、隐私设置、封锁管理、会话文件夹、多账号、语音转写
17
19
  - 媒体:下载、发文件 / 相册 / 语音条 / 贴纸 / GIF、发送名片
18
20
  - 聊天:会话列表、历史记录、置顶消息、会话内与全局搜索、在线状态、共同群组
19
21
  - 联系人与个人资料管理
@@ -114,6 +116,48 @@ tg logout # 退出登录并删除会话
114
116
  | `tg status <用户>` | 查看用户在线状态 |
115
117
  | `tg common-chats <用户>` | 列出与该用户的共同群组 |
116
118
 
119
+ ### 群组管理(v3)
120
+
121
+ | 命令 | 说明 |
122
+ |---|---|
123
+ | `tg new-group <标题> <用户...>` | 建基本群并拉入初始成员 |
124
+ | `tg new-channel <标题> [--about 简介]` | 建频道;`--group` 建超级群组 |
125
+ | `tg invite <会话> <用户...>` | 邀请用户(超级群组与基本群都支持) |
126
+ | `tg kick <会话> <用户...>` | 移出用户(超级群组的踢人 = 禁言封禁,`unban` 可恢复) |
127
+ | `tg leave <会话>` | 退出群组/频道 |
128
+ | `tg ban <会话> <用户> [天数]` | 封禁(不带天数 = 永久);仅超级群组 |
129
+ | `tg unban <会话> <用户>` | 解除封禁 |
130
+ | `tg banned <会话>` | 封禁名单(支持 `--json`) |
131
+ | `tg admins <会话>` | 管理员列表(含头衔) |
132
+ | `tg promote <会话> <用户> [--title 头衔]` | 升级为管理员(标准权限集) |
133
+ | `tg demote <会话> <用户>` | 撤销管理员 |
134
+ | `tg admin-rights <会话> <用户> <权限...>` | 设置精确权限:`--change-info --post-messages --edit-messages --delete-messages --ban-users --invite-users --pin-messages --add-admins --anonymous --manage-call --other --manage-topics …`;`--rank` 定头衔 |
135
+ | `tg slow-mode <会话> [秒]` | 设置慢速模式(0 = 关闭);仅超级群组 |
136
+ | `tg topics <会话>` | 列出论坛话题 |
137
+ | `tg topic-create <会话> <标题> [首条消息]` | 创建论坛话题 |
138
+ | `tg admin-log <会话> [-n]` | 近期管理操作日志 |
139
+
140
+ ### 联系人、隐私与其他(v4)
141
+
142
+ | 命令 | 说明 |
143
+ |---|---|
144
+ | `tg contact add <用户> [姓名] [--phone]` | 新增/更新联系人 |
145
+ | `tg contact del <用户...>` | 删除联系人(按名称/id 或 `+手机号`) |
146
+ | `tg contact import <文件>` | 从 `.json` / `.csv` 批量导入(`phone,first_name,last_name`) |
147
+ | `tg contact export [文件]` | 导出为可再导入的 JSON(默认输出到终端) |
148
+ | `tg photo-del [--all]` | 删除最近一张头像(`--all` 删除全部) |
149
+ | `tg privacy get [键...]` | 查看隐私设置(默认全部) |
150
+ | `tg privacy set <键> all\|contacts\|nobody` | 修改隐私设置;`--allow 用户` / `--deny 用户` 精细化 |
151
+ | `tg block <用户>` / `tg unblock <用户>` / `tg blocked` | 封锁管理 |
152
+ | `tg folders` | 列出会话文件夹 |
153
+ | `tg folder-create <名称> --chat <会话>...` | 创建文件夹并加入会话 |
154
+ | `tg folder-assign <文件夹> <会话...>` | 向文件夹追加会话 |
155
+ | `tg folder-del <文件夹>` | 删除文件夹 |
156
+ | `tg folder-order <文件夹...>` | 重排文件夹(按想要的顺序全部列出) |
157
+ | `tg accounts` | 列出已配置的账号 |
158
+ | `tg login --account <标签>` | 登录额外账号 |
159
+ | `tg vtr <会话> <消息id>` | 语音转文字(需要 Telegram Premium) |
160
+
117
161
  全局选项:`--account`(v1 仅 default)、`--json`(原始 JSON 输出)。
118
162
  退出码:`0` 成功、`1` 命令错误、`2` 配置/连接/会话错误、`130` 中断。
119
163
 
@@ -14,8 +14,12 @@ no daemon, no external service**. Everything the CLI does lives in the CLI.
14
14
  - QR-code login (`tg login --qr`) with an independent session
15
15
  - Messaging: send / reply / edit / delete / forward / polls / schedule /
16
16
  drafts / reactions / pinning
17
+ - Group administration: create groups/channels, invite / kick / ban,
18
+ admin promotion with granular rights, slow mode, forum topics, admin log
17
19
  - Chat management: archive, mute, edit title / description / photo,
18
20
  invite links, clear history
21
+ - Contacts, privacy settings, blocking, dialog folders, multi-account,
22
+ voice transcription
19
23
  - Media: download, send files / albums / voice notes / stickers / GIFs,
20
24
  contact cards
21
25
  - Chats: list dialogs, history, pinned messages, in-chat & global search,
@@ -122,6 +126,48 @@ tg logout # remove the session
122
126
  | `tg status <user>` | Show a user's online status |
123
127
  | `tg common-chats <user>` | List chats you share with a user |
124
128
 
129
+ ### Group administration (v3)
130
+
131
+ | Command | Description |
132
+ |---|---|
133
+ | `tg new-group <title> <user...>` | Create a basic group with initial members |
134
+ | `tg new-channel <title> [--about text]` | Create a channel; `--group` creates a supergroup |
135
+ | `tg invite <chat> <user...>` | Invite users (supergroups & basic groups) |
136
+ | `tg kick <chat> <user...>` | Remove users (supergroup kick = view-messages ban; `unban` to reverse) |
137
+ | `tg leave <chat>` | Leave a group/channel |
138
+ | `tg ban <chat> <user> [days]` | Ban a user (permanent unless days given); supergroups only |
139
+ | `tg unban <chat> <user>` | Unban a user |
140
+ | `tg banned <chat>` | List banned users (`--json` supported) |
141
+ | `tg admins <chat>` | List admins with ranks |
142
+ | `tg promote <chat> <user> [--title rank]` | Promote with the standard right set |
143
+ | `tg demote <chat> <user>` | Remove admin rights |
144
+ | `tg admin-rights <chat> <user> <flags...>` | Set exact rights: `--change-info --post-messages --edit-messages --delete-messages --ban-users --invite-users --pin-messages --add-admins --anonymous --manage-call --other --manage-topics …`; `--rank` |
145
+ | `tg slow-mode <chat> [seconds]` | Set slow mode (0 = disable); supergroups only |
146
+ | `tg topics <chat>` | List forum topics |
147
+ | `tg topic-create <chat> <title> [text]` | Create a forum topic, optionally with a first message |
148
+ | `tg admin-log <chat> [-n]` | Recent admin actions |
149
+
150
+ ### Contacts, privacy & misc (v4)
151
+
152
+ | Command | Description |
153
+ |---|---|
154
+ | `tg contact add <user> [name] [--phone]` | Add/update a contact |
155
+ | `tg contact del <user...>` | Delete contacts (by name/id or `+phone`) |
156
+ | `tg contact import <file>` | Bulk import from `.json` / `.csv` (`phone,first_name,last_name`) |
157
+ | `tg contact export [file]` | Export contacts as re-importable JSON (default stdout) |
158
+ | `tg photo-del [--all]` | Delete your most recent profile photo (or all) |
159
+ | `tg privacy get [key...]` | Show privacy settings (default: all) |
160
+ | `tg privacy set <key> all\|contacts\|nobody` | Change a privacy setting; `--allow USER` / `--deny USER` refine it |
161
+ | `tg block <user>` / `tg unblock <user>` / `tg blocked` | Block management |
162
+ | `tg folders` | List dialog folders |
163
+ | `tg folder-create <name> --chat <c>...` | Create a folder with chats |
164
+ | `tg folder-assign <folder> <chat...>` | Add chats to a folder |
165
+ | `tg folder-del <folder>` | Delete a folder |
166
+ | `tg folder-order <folder...>` | Reorder folders (pass all in the wanted order) |
167
+ | `tg accounts` | List configured accounts |
168
+ | `tg login --account <label>` | Log in an additional account |
169
+ | `tg vtr <chat> <msg_id>` | Transcribe a voice note (Telegram Premium required) |
170
+
125
171
  Global flags: `--account` (v1: default only), `--json` (raw JSON output).
126
172
  Exit codes: `0` ok, `1` command error, `2` config/connection/session error,
127
173
  `130` interrupted.
@@ -0,0 +1,82 @@
1
+ # Roadmap
2
+
3
+ The v1 release covers the core daily workflow. The goal is full coverage of
4
+ the Telegram feature surface reachable from a userbot, roughly matching the
5
+ 126-tool scope of [chigwell/telegram-mcp](https://github.com/chigwell/telegram-mcp).
6
+
7
+ Status legend: ✅ done · 🚧 planned · ❌ deliberately out of scope.
8
+
9
+ ## Milestones
10
+
11
+ ### v2 — Messaging enhancements & chat management (shipped)
12
+
13
+ Messaging:
14
+
15
+ - ✅ `schedule <chat> <time> <text...>` — send scheduled messages
16
+ (`send_message(..., schedule=...)`), list/delete scheduled
17
+ (`scheduled <chat>` / `unschedule <chat> <ids...>`)
18
+ - ✅ `draft <chat> [text]` / `draft-clear <chat>` — save/get/clear drafts
19
+ - ✅ `react <chat> <msg_id> <emoji>` / `unreact` — send/remove reactions
20
+ (note: reacting to Saved Messages is premium-only on Telegram's side)
21
+ - ✅ `sticker <chat> <file.webp>` — send stickers (telethon handles via `send_file`)
22
+ - ✅ `gif <chat> <query>` — search GIFs (inline query to the built-in @gif bot;
23
+ `messages.searchGifs` was removed from the TL layer) and send
24
+ - ✅ `contact-card <chat> <name> <phone>` — send a contact card
25
+ - ✅ `pin <chat> <msg_id>` / `unpin <chat> [msg_id]` — pin/unpin messages
26
+ - ✅ `read <chat>` — mark a chat as read
27
+ - ✅ `msg-link <chat> <msg_id>` — export a t.me message link (channels/supergroups)
28
+
29
+ Chat management:
30
+
31
+ - ✅ `archive <chat>` / `unarchive <chat>` — archive/unarchive dialogs
32
+ - ✅ `mute <chat> [hours]` / `unmute <chat>` — mute/unmute notifications
33
+ - ✅ `chat-title <chat> <title>` / `chat-about <chat> <text>` /
34
+ `chat-photo <chat> <file>` — edit chat info
35
+ - ✅ `invite-link <chat>` / `join <link>` — export/import invite links
36
+ - ✅ `clear-history <chat>` — delete all messages in a chat (`--self-only` to
37
+ keep the other side's copy)
38
+ - ✅ `status <user>` / `common-chats <user>` — online status, common chats
39
+
40
+ ### v3 — Group administration (shipped)
41
+
42
+ - ✅ `new-group <title> <user...>` / `new-channel <title>` — create groups
43
+ (`--group` flag makes a supergroup) /channels
44
+ - ✅ `invite <chat> <user...>` / `kick <chat> <user...>` / `leave <chat>`
45
+ (basic-group kick/leave use `messages.DeleteChatUser`, supergroup kick is a
46
+ view-messages ban like official clients)
47
+ - ✅ `ban <chat> <user> [days]` / `unban <chat> <user>` / `banned <chat>`
48
+ (the working participant filter on the current TL layer is
49
+ `ChannelParticipantsKicked`, not `ChannelParticipantsBanned`)
50
+ - ✅ `admins <chat>` / `promote <chat> <user> [--title]` / `demote <chat> <user>` /
51
+ `admin-rights <chat> <user> ...` — admin management with granular rights
52
+ (basic groups use `messages.EditChatAdmin`, no titles)
53
+ - ✅ `slow-mode <chat> [seconds]` — set/disable slow mode
54
+ - ✅ `topics <chat>` / `topic-create <chat> <title> [text]` — forum topics
55
+ (list/create)
56
+ - ✅ `admin-log <chat>` — recent admin actions
57
+
58
+ ### v4 — Contacts, profile & misc (shipped)
59
+
60
+ - ✅ `contact add/del` — add/delete contacts (`contact del` also matches `+phone`)
61
+ - ✅ `contact import <file>` / `contact export` — bulk JSON/CSV import,
62
+ re-importable JSON export (note: Telegram no longer creates contacts for
63
+ unregistered numbers — unmatched imports return 0)
64
+ - ✅ `photo-del` — delete the most recent profile photo (`--all` for all)
65
+ - ✅ `privacy get/set` — 11 keys (lastseen, phone, calls, p2p, groups, photo,
66
+ forwards, voicemail, about, birthday, addedby); set rules must use the
67
+ `InputPrivacyValue*` classes — the output `PrivacyValue*` classes are
68
+ silently ignored by the server
69
+ - ✅ `block <user>` / `unblock <user>` / `blocked` — block management
70
+ - ✅ `folders` / `folder-create` / `folder-assign` / `folder-del` /
71
+ `folder-order` — dialog folders (list/create/assign/reorder)
72
+ - ✅ `accounts` — multi-account support (`accounts` lists every
73
+ `TELEGRAM_SESSION_STRING*`; `login --account LABEL` and
74
+ `<command> --account LABEL` both work)
75
+ - ✅ `vtr <chat> <msg_id>` — voice transcription via raw
76
+ `messages.TranscribeAudioRequest` (telethon has no wrapper). Telegram
77
+ Premium is required server-side; free accounts get a clean error
78
+
79
+ ### Out of scope
80
+
81
+ - ❌ Agent-oriented event tools (`wait_for_settled_message`, incoming feed
82
+ watchers) — meaningless for an interactive CLI.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "telegram-userbot-cli"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Full-featured Telegram command-line client powered by a Telethon userbot (your own account, not a bot token)"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """tgcli — standalone Telegram command-line client."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
@@ -1,8 +1,10 @@
1
- """Login / logout flows."""
1
+ """Login / logout / multi-account flows."""
2
2
 
3
3
  import asyncio
4
4
  import getpass
5
5
  import io
6
+ import os
7
+ import re
6
8
  import sys
7
9
  from datetime import date, datetime, timezone
8
10
 
@@ -14,6 +16,7 @@ from .client import build_client
14
16
  from .output import TgError
15
17
 
16
18
  _MAX_QR_REFRESHES = 5
19
+ _SESSION_ENV_RE = re.compile(r"TELEGRAM_SESSION_STRING(?:_([A-Z0-9_]+))?$")
17
20
 
18
21
 
19
22
  def render_qr(url: str) -> None:
@@ -38,6 +41,19 @@ def _seconds_until(qr) -> float:
38
41
  return 120.0
39
42
 
40
43
 
44
+ def _save_session(args, session_str: str) -> None:
45
+ account = getattr(args, "account", None)
46
+ key = config.session_env_var(account)
47
+ config.write_env(
48
+ key,
49
+ session_str,
50
+ comment=f"generated by tg login on {date.today()}; keep secret, "
51
+ "do NOT share with the MCP daemon session",
52
+ )
53
+ where = f" for account '{account}'" if account else ""
54
+ print(f"Login successful{where}. Session written to .env — verify with: tg me")
55
+
56
+
41
57
  async def _qr_login(args) -> None:
42
58
  api_id, api_hash = config.require_credentials()
43
59
  client = build_client(StringSession(), api_id, api_hash)
@@ -65,13 +81,7 @@ async def _qr_login(args) -> None:
65
81
  session_str = StringSession.save(client.session)
66
82
  finally:
67
83
  await client.disconnect()
68
- config.write_env(
69
- "TELEGRAM_SESSION_STRING",
70
- session_str,
71
- comment=f"generated by tg login on {date.today()}; keep secret, "
72
- "do NOT share with the MCP daemon session",
73
- )
74
- print("Login successful. Session written to .env — verify with: tg me")
84
+ _save_session(args, session_str)
75
85
 
76
86
 
77
87
  async def _phone_login(args) -> None:
@@ -90,13 +100,7 @@ async def _phone_login(args) -> None:
90
100
  session_str = StringSession.save(client.session)
91
101
  finally:
92
102
  await client.disconnect()
93
- config.write_env(
94
- "TELEGRAM_SESSION_STRING",
95
- session_str,
96
- comment=f"generated by tg login on {date.today()}; keep secret, "
97
- "do NOT share with the MCP daemon session",
98
- )
99
- print("Login successful. Session written to .env — verify with: tg me")
103
+ _save_session(args, session_str)
100
104
 
101
105
 
102
106
  async def cmd_login(args) -> None:
@@ -117,3 +121,22 @@ async def cmd_logout(args) -> None:
117
121
  await client.disconnect()
118
122
  config.clear_env(config.session_env_var(getattr(args, "account", None)))
119
123
  print("Logged out; session removed from .env. The MCP daemon's session is untouched.")
124
+
125
+
126
+ async def cmd_accounts(args) -> None:
127
+ """List every configured account (TELEGRAM_SESSION_STRING* entries)."""
128
+ rows = []
129
+ for key in os.environ:
130
+ m = _SESSION_ENV_RE.fullmatch(key)
131
+ if m and os.environ[key].strip():
132
+ label = m.group(1).lower() if m.group(1) else "default"
133
+ rows.append((label, key))
134
+ rows.sort(key=lambda r: (r[0] != "default", r[0]))
135
+ if not rows:
136
+ print("(no accounts configured — run: tg login)")
137
+ return
138
+ print(f"{'account':<16} env variable")
139
+ for label, key in rows:
140
+ note = " (default)" if label == "default" else ""
141
+ print(f"{label:<16} {key}{note}")
142
+ print("\nuse with: tg <command> --account <label> | add one: tg login --account <label>")
@@ -1,4 +1,4 @@
1
- """Chat / search / contacts / profile commands."""
1
+ """Chat / search / contacts / profile / privacy / block commands."""
2
2
 
3
3
  from datetime import datetime
4
4
 
@@ -8,6 +8,21 @@ from .client import run_with_client
8
8
  from .output import TgError, fmt_message_row, json_pp
9
9
  from .resolve import resolve_chat
10
10
 
11
+ # friendly names for the privacy keys (value -> InputPrivacyKey class name)
12
+ _PRIVACY_KEYS = {
13
+ "lastseen": "InputPrivacyKeyStatusTimestamp",
14
+ "phone": "InputPrivacyKeyPhoneNumber",
15
+ "calls": "InputPrivacyKeyPhoneCall",
16
+ "p2p": "InputPrivacyKeyPhoneP2P",
17
+ "groups": "InputPrivacyKeyChatInvite",
18
+ "photo": "InputPrivacyKeyProfilePhoto",
19
+ "forwards": "InputPrivacyKeyForwards",
20
+ "voicemail": "InputPrivacyKeyVoiceMessages",
21
+ "about": "InputPrivacyKeyAbout",
22
+ "birthday": "InputPrivacyKeyBirthday",
23
+ "addedby": "InputPrivacyKeyAddedByPhone",
24
+ }
25
+
11
26
 
12
27
  async def cmd_me(args) -> None:
13
28
  async with run_with_client(args) as client:
@@ -186,6 +201,171 @@ async def cmd_profile(args) -> None:
186
201
  print(json_pp(me.to_dict() if me else {}))
187
202
 
188
203
 
204
+ async def cmd_photo_del(args) -> None:
205
+ async with run_with_client(args) as client:
206
+ photos = await client.get_profile_photos("me")
207
+ if not photos:
208
+ print("(no profile photos)")
209
+ return
210
+ if not args.all:
211
+ photos = photos[:1] # just the most recent one
212
+ await client(
213
+ functions.photos.DeletePhotosRequest(
214
+ id=[
215
+ types.InputPhoto(
216
+ id=p.id, access_hash=p.access_hash, file_reference=p.file_reference
217
+ )
218
+ for p in photos
219
+ ]
220
+ )
221
+ )
222
+ print(f"Deleted {len(photos)} profile photo(s)")
223
+
224
+
225
+ def _privacy_key(name: str) -> types.TypeInputPrivacyKey:
226
+ cls_name = _PRIVACY_KEYS.get(name.lower())
227
+ if not cls_name:
228
+ raise TgError(
229
+ f"unknown privacy key: {name!r}",
230
+ f"choose from: {', '.join(sorted(_PRIVACY_KEYS))}",
231
+ )
232
+ return getattr(types, cls_name)()
233
+
234
+
235
+ def _privacy_rule_text(rule, users: dict) -> str:
236
+ kind = type(rule).__name__.removeprefix("PrivacyValue")
237
+ if kind == "AllowAll":
238
+ return "everyone"
239
+ if kind == "AllowContacts":
240
+ return "contacts"
241
+ if kind == "AllowCloseFriends":
242
+ return "close friends"
243
+ if kind == "AllowBots":
244
+ return "bots"
245
+ if kind == "AllowPremium":
246
+ return "premium"
247
+ if kind == "AllowChatParticipants":
248
+ return "members of " + ", ".join(_display_chat(c, users) for c in rule.chats)
249
+ if kind == "AllowUsers":
250
+ return "+" + ", ".join(_display_chat(users.get(u), users) or str(u) for u in rule.users)
251
+ if kind == "DisallowAll":
252
+ return "nobody"
253
+ if kind == "DisallowContacts":
254
+ return "no contacts"
255
+ if kind == "DisallowUsers":
256
+ return "-" + ", ".join(_display_chat(users.get(u), users) or str(u) for u in rule.users)
257
+ return kind
258
+
259
+
260
+ def _display_chat(chat, users: dict) -> str:
261
+ if chat is None:
262
+ return "?"
263
+ title = getattr(chat, "title", None)
264
+ if title:
265
+ return title
266
+ name = " ".join(
267
+ x for x in (getattr(chat, "first_name", None), getattr(chat, "last_name", None)) if x
268
+ )
269
+ return name or "?"
270
+
271
+
272
+ async def cmd_privacy_get(args) -> None:
273
+ keys = args.key or sorted(_PRIVACY_KEYS)
274
+ async with run_with_client(args) as client:
275
+ rows = []
276
+ for name in keys:
277
+ res = await client(functions.account.GetPrivacyRequest(key=_privacy_key(name)))
278
+ users = {u.id: u for u in getattr(res, "users", []) or []}
279
+ rules = " + ".join(_privacy_rule_text(r, users) for r in res.rules) or "(unset)"
280
+ rows.append((name, rules))
281
+ if args.json:
282
+ print(json_pp([{"key": k, "rules": v} for k, v in rows]))
283
+ return
284
+ for name, rules in rows:
285
+ print(f"{name:<10} {rules}")
286
+
287
+
288
+ def _input_user(client, peer) -> types.TypeInputUser:
289
+ if isinstance(peer, types.InputPeerUser):
290
+ return types.InputUser(user_id=peer.user_id, access_hash=peer.access_hash)
291
+ if isinstance(peer, types.InputPeerSelf):
292
+ return types.InputUserSelf()
293
+ raise TgError("only users can appear in privacy rules")
294
+
295
+
296
+ async def cmd_privacy_set(args) -> None:
297
+ base = args.value.lower()
298
+ base_map = {"all": "everyone", "contacts": "contacts", "nobody": "nobody"}
299
+ if base not in base_map:
300
+ raise TgError(
301
+ f"unknown privacy value: {args.value!r}",
302
+ "use all / contacts / nobody, with optional --allow USER / --deny USER",
303
+ )
304
+ async with run_with_client(args) as client:
305
+ # rules need Input* rule classes and InputUser entries
306
+ resolved_allow = [
307
+ _input_user(client, await client.get_input_entity(await resolve_chat(client, u)))
308
+ for u in args.allow
309
+ ]
310
+ resolved_deny = [
311
+ _input_user(client, await client.get_input_entity(await resolve_chat(client, u)))
312
+ for u in args.deny
313
+ ]
314
+ rules: list = []
315
+ if base == "all":
316
+ rules.append(types.InputPrivacyValueAllowAll())
317
+ elif base == "contacts":
318
+ rules.append(types.InputPrivacyValueAllowContacts())
319
+ else:
320
+ rules.append(types.InputPrivacyValueDisallowAll())
321
+ if resolved_allow:
322
+ rules.append(types.InputPrivacyValueAllowUsers(users=resolved_allow))
323
+ if resolved_deny:
324
+ rules.append(types.InputPrivacyValueDisallowUsers(users=resolved_deny))
325
+ await client(functions.account.SetPrivacyRequest(key=_privacy_key(args.key), rules=rules))
326
+ desc = base_map[base]
327
+ if resolved_allow:
328
+ desc += ", allow " + ", ".join(args.allow)
329
+ if resolved_deny:
330
+ desc += ", deny " + ", ".join(args.deny)
331
+ print(f"{args.key} set to {desc}")
332
+
333
+
334
+ async def cmd_block(args) -> None:
335
+ async with run_with_client(args) as client:
336
+ entity = await resolve_chat(client, args.user)
337
+ await client(functions.contacts.BlockRequest(id=entity))
338
+ print(f"Blocked {args.user}")
339
+
340
+
341
+ async def cmd_unblock(args) -> None:
342
+ async with run_with_client(args) as client:
343
+ entity = await resolve_chat(client, args.user)
344
+ await client(functions.contacts.UnblockRequest(id=entity))
345
+ print(f"Unblocked {args.user}")
346
+
347
+
348
+ async def cmd_blocked(args) -> None:
349
+ async with run_with_client(args) as client:
350
+ res = await client(functions.contacts.GetBlockedRequest(offset=0, limit=100))
351
+ users = {u.id: u for u in getattr(res, "users", []) or []}
352
+ rows = []
353
+ for pb in getattr(res, "blocked", []) or []:
354
+ pid = pb.peer_id.user_id if isinstance(pb.peer_id, types.PeerUser) else pb.peer_id
355
+ rows.append((pid, getattr(pb, "date", None)))
356
+ if args.json:
357
+ print(json_pp([p.to_dict() for p in getattr(res, "blocked", []) or []]))
358
+ return
359
+ if not rows:
360
+ print("(nobody is blocked)")
361
+ return
362
+ for pid, date in rows:
363
+ u = users.get(pid)
364
+ name = _display_chat(u, users) if u else str(pid)
365
+ when = date.astimezone().strftime("%Y-%m-%d") if date else ""
366
+ print(f"{pid:>14} {name} {when}")
367
+
368
+
189
369
  def setup(subparsers, common=None) -> None:
190
370
  parents = [common] if common else []
191
371
  sp = subparsers.add_parser("me", parents=parents, help="Show your account info")
@@ -233,3 +413,34 @@ def setup(subparsers, common=None) -> None:
233
413
  sp.add_argument("--bio", default=None)
234
414
  sp.add_argument("--photo", default=None, help="path to an image")
235
415
  sp.set_defaults(func=cmd_profile)
416
+
417
+ sp = subparsers.add_parser(
418
+ "photo-del", parents=parents, help="Delete your most recent profile photo (--all for all)"
419
+ )
420
+ sp.add_argument("--all", action="store_true", help="delete every profile photo")
421
+ sp.set_defaults(func=cmd_photo_del)
422
+
423
+ sp = subparsers.add_parser("privacy", parents=parents, help="Show privacy settings")
424
+ sub = sp.add_subparsers(dest="privacy_cmd", required=True)
425
+ g = sub.add_parser("get", help="privacy get [key...] (default: all)")
426
+ g.add_argument("key", nargs="*", default=[])
427
+ g.set_defaults(func=cmd_privacy_get)
428
+ s = sub.add_parser(
429
+ "set", help="privacy set KEY all|contacts|nobody [--allow USER] [--deny USER]"
430
+ )
431
+ s.add_argument("key", help=f"one of: {', '.join(sorted(_PRIVACY_KEYS))}")
432
+ s.add_argument("value")
433
+ s.add_argument("-a", "--allow", action="append", default=[], metavar="USER")
434
+ s.add_argument("-d", "--deny", action="append", default=[], metavar="USER")
435
+ s.set_defaults(func=cmd_privacy_set)
436
+
437
+ sp = subparsers.add_parser("block", parents=parents, help="Block a user")
438
+ sp.add_argument("user")
439
+ sp.set_defaults(func=cmd_block)
440
+
441
+ sp = subparsers.add_parser("unblock", parents=parents, help="Unblock a user")
442
+ sp.add_argument("user")
443
+ sp.set_defaults(func=cmd_unblock)
444
+
445
+ sp = subparsers.add_parser("blocked", parents=parents, help="List blocked users")
446
+ sp.set_defaults(func=cmd_blocked)
@@ -6,7 +6,7 @@ import sys
6
6
 
7
7
  from telethon import errors
8
8
 
9
- from . import __version__, auth, chats, dialogs, media, messages, resolve
9
+ from . import __version__, auth, chats, contacts, dialogs, groups, media, messages, resolve
10
10
  from .output import TgError
11
11
 
12
12
 
@@ -41,7 +41,10 @@ def build_parser() -> argparse.ArgumentParser:
41
41
  sp2 = sub.add_parser("logout", parents=[common], help="Log out and remove the session")
42
42
  sp2.set_defaults(func=auth.cmd_logout)
43
43
 
44
- for mod in (messages, chats, media, dialogs):
44
+ sp3 = sub.add_parser("accounts", parents=[common], help="List configured accounts")
45
+ sp3.set_defaults(func=auth.cmd_accounts)
46
+
47
+ for mod in (messages, chats, media, dialogs, groups, contacts):
45
48
  mod.setup(sub, common)
46
49
 
47
50
  resolve.setup_alias(sub, common)