telegram-userbot-cli 0.2.0__tar.gz → 0.3.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 (24) hide show
  1. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/PKG-INFO +24 -1
  2. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/README-zh.md +22 -0
  3. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/README.md +23 -0
  4. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/ROADMAP.md +15 -8
  5. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/pyproject.toml +1 -1
  6. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/__init__.py +1 -1
  7. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/cli.py +2 -2
  8. telegram_userbot_cli-0.3.0/src/tgcli/groups.py +534 -0
  9. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.env.example +0 -0
  10. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  11. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.github/workflows/release.yml +0 -0
  12. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.gitignore +0 -0
  13. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/LICENSE +0 -0
  14. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/__main__.py +0 -0
  15. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/auth.py +0 -0
  16. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/chats.py +0 -0
  17. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/client.py +0 -0
  18. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/config.py +0 -0
  19. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/dialogs.py +0 -0
  20. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/media.py +0 -0
  21. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/messages.py +0 -0
  22. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/output.py +0 -0
  23. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/resolve.py +0 -0
  24. {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.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.3.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,6 +27,8 @@ 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
32
34
  - Media: download, send files / albums / voice notes / stickers / GIFs,
@@ -135,6 +137,27 @@ tg logout # remove the session
135
137
  | `tg status <user>` | Show a user's online status |
136
138
  | `tg common-chats <user>` | List chats you share with a user |
137
139
 
140
+ ### Group administration (v3)
141
+
142
+ | Command | Description |
143
+ |---|---|
144
+ | `tg new-group <title> <user...>` | Create a basic group with initial members |
145
+ | `tg new-channel <title> [--about text]` | Create a channel; `--group` creates a supergroup |
146
+ | `tg invite <chat> <user...>` | Invite users (supergroups & basic groups) |
147
+ | `tg kick <chat> <user...>` | Remove users (supergroup kick = view-messages ban; `unban` to reverse) |
148
+ | `tg leave <chat>` | Leave a group/channel |
149
+ | `tg ban <chat> <user> [days]` | Ban a user (permanent unless days given); supergroups only |
150
+ | `tg unban <chat> <user>` | Unban a user |
151
+ | `tg banned <chat>` | List banned users (`--json` supported) |
152
+ | `tg admins <chat>` | List admins with ranks |
153
+ | `tg promote <chat> <user> [--title rank]` | Promote with the standard right set |
154
+ | `tg demote <chat> <user>` | Remove admin rights |
155
+ | `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` |
156
+ | `tg slow-mode <chat> [seconds]` | Set slow mode (0 = disable); supergroups only |
157
+ | `tg topics <chat>` | List forum topics |
158
+ | `tg topic-create <chat> <title> [text]` | Create a forum topic, optionally with a first message |
159
+ | `tg admin-log <chat> [-n]` | Recent admin actions |
160
+
138
161
  Global flags: `--account` (v1: default only), `--json` (raw JSON output).
139
162
  Exit codes: `0` ok, `1` command error, `2` config/connection/session error,
140
163
  `130` interrupted.
@@ -13,6 +13,7 @@
13
13
 
14
14
  - 扫码登录(`tg login --qr`),使用独立会话
15
15
  - 消息:发送 / 引用回复 / 编辑 / 删除 / 转发 / 投票 / 定时 / 草稿 / 表情回应 / 置顶
16
+ - 群组管理:建群 / 建频道、邀请 / 踢人 / 封禁、管理员升降级(细粒度权限)、慢速模式、论坛话题、管理日志
16
17
  - 聊天管理:归档、免打扰、修改群名 / 简介 / 群头像、邀请链接、清空历史
17
18
  - 媒体:下载、发文件 / 相册 / 语音条 / 贴纸 / GIF、发送名片
18
19
  - 聊天:会话列表、历史记录、置顶消息、会话内与全局搜索、在线状态、共同群组
@@ -114,6 +115,27 @@ tg logout # 退出登录并删除会话
114
115
  | `tg status <用户>` | 查看用户在线状态 |
115
116
  | `tg common-chats <用户>` | 列出与该用户的共同群组 |
116
117
 
118
+ ### 群组管理(v3)
119
+
120
+ | 命令 | 说明 |
121
+ |---|---|
122
+ | `tg new-group <标题> <用户...>` | 建基本群并拉入初始成员 |
123
+ | `tg new-channel <标题> [--about 简介]` | 建频道;`--group` 建超级群组 |
124
+ | `tg invite <会话> <用户...>` | 邀请用户(超级群组与基本群都支持) |
125
+ | `tg kick <会话> <用户...>` | 移出用户(超级群组的踢人 = 禁言封禁,`unban` 可恢复) |
126
+ | `tg leave <会话>` | 退出群组/频道 |
127
+ | `tg ban <会话> <用户> [天数]` | 封禁(不带天数 = 永久);仅超级群组 |
128
+ | `tg unban <会话> <用户>` | 解除封禁 |
129
+ | `tg banned <会话>` | 封禁名单(支持 `--json`) |
130
+ | `tg admins <会话>` | 管理员列表(含头衔) |
131
+ | `tg promote <会话> <用户> [--title 头衔]` | 升级为管理员(标准权限集) |
132
+ | `tg demote <会话> <用户>` | 撤销管理员 |
133
+ | `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` 定头衔 |
134
+ | `tg slow-mode <会话> [秒]` | 设置慢速模式(0 = 关闭);仅超级群组 |
135
+ | `tg topics <会话>` | 列出论坛话题 |
136
+ | `tg topic-create <会话> <标题> [首条消息]` | 创建论坛话题 |
137
+ | `tg admin-log <会话> [-n]` | 近期管理操作日志 |
138
+
117
139
  全局选项:`--account`(v1 仅 default)、`--json`(原始 JSON 输出)。
118
140
  退出码:`0` 成功、`1` 命令错误、`2` 配置/连接/会话错误、`130` 中断。
119
141
 
@@ -14,6 +14,8 @@ 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
19
21
  - Media: download, send files / albums / voice notes / stickers / GIFs,
@@ -122,6 +124,27 @@ tg logout # remove the session
122
124
  | `tg status <user>` | Show a user's online status |
123
125
  | `tg common-chats <user>` | List chats you share with a user |
124
126
 
127
+ ### Group administration (v3)
128
+
129
+ | Command | Description |
130
+ |---|---|
131
+ | `tg new-group <title> <user...>` | Create a basic group with initial members |
132
+ | `tg new-channel <title> [--about text]` | Create a channel; `--group` creates a supergroup |
133
+ | `tg invite <chat> <user...>` | Invite users (supergroups & basic groups) |
134
+ | `tg kick <chat> <user...>` | Remove users (supergroup kick = view-messages ban; `unban` to reverse) |
135
+ | `tg leave <chat>` | Leave a group/channel |
136
+ | `tg ban <chat> <user> [days]` | Ban a user (permanent unless days given); supergroups only |
137
+ | `tg unban <chat> <user>` | Unban a user |
138
+ | `tg banned <chat>` | List banned users (`--json` supported) |
139
+ | `tg admins <chat>` | List admins with ranks |
140
+ | `tg promote <chat> <user> [--title rank]` | Promote with the standard right set |
141
+ | `tg demote <chat> <user>` | Remove admin rights |
142
+ | `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` |
143
+ | `tg slow-mode <chat> [seconds]` | Set slow mode (0 = disable); supergroups only |
144
+ | `tg topics <chat>` | List forum topics |
145
+ | `tg topic-create <chat> <title> [text]` | Create a forum topic, optionally with a first message |
146
+ | `tg admin-log <chat> [-n]` | Recent admin actions |
147
+
125
148
  Global flags: `--account` (v1: default only), `--json` (raw JSON output).
126
149
  Exit codes: `0` ok, `1` command error, `2` config/connection/session error,
127
150
  `130` interrupted.
@@ -37,16 +37,23 @@ Chat management:
37
37
  keep the other side's copy)
38
38
  - ✅ `status <user>` / `common-chats <user>` — online status, common chats
39
39
 
40
- ### v3 — Group administration
40
+ ### v3 — Group administration (shipped)
41
41
 
42
- - 🚧 `new-group <title> <user...>` / `new-channel <title>` — create groups/channels
43
- - 🚧 `invite <chat> <user...>` / `kick <chat> <user...>` / `leave <chat>`
44
- - 🚧 `ban <chat> <user> [days]` / `unban <chat> <user>` / `banned <chat>`
45
- - 🚧 `admins <chat>` / `promote <chat> <user> [--title]` / `demote <chat> <user>` /
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>` /
46
51
  `admin-rights <chat> <user> ...` — admin management with granular rights
47
- - 🚧 `slow-mode <chat> [seconds]` set/disable slow mode
48
- - 🚧 `topic <chat> ...`forum topics (list/create)
49
- - 🚧 `admin-log <chat>` — recent admin actions
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
50
57
 
51
58
  ### v4 — Contacts, profile & misc
52
59
 
@@ -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.3.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.3.0"
@@ -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, dialogs, groups, media, messages, resolve
10
10
  from .output import TgError
11
11
 
12
12
 
@@ -41,7 +41,7 @@ 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
+ for mod in (messages, chats, media, dialogs, groups):
45
45
  mod.setup(sub, common)
46
46
 
47
47
  resolve.setup_alias(sub, common)
@@ -0,0 +1,534 @@
1
+ """Group administration: create, members, bans, admins, slow mode, topics, admin log."""
2
+
3
+ import random
4
+ import re
5
+ from datetime import datetime, timedelta, timezone
6
+
7
+ from telethon import functions, types
8
+
9
+ from .client import run_with_client
10
+ from .output import TgError, json_pp
11
+ from .resolve import resolve_chat
12
+
13
+ # rights granted by `promote`: full admin minus the dangerous ones
14
+ _PROMOTE_RIGHTS = dict(
15
+ change_info=True,
16
+ post_messages=True,
17
+ edit_messages=True,
18
+ delete_messages=True,
19
+ ban_users=True,
20
+ invite_users=True,
21
+ pin_messages=True,
22
+ manage_call=True,
23
+ manage_topics=True,
24
+ other=True,
25
+ )
26
+
27
+ # granular flags accepted by `admin-rights` (arg name == ChatAdminRights field)
28
+ _RIGHT_FLAGS = [
29
+ "change_info",
30
+ "post_messages",
31
+ "edit_messages",
32
+ "delete_messages",
33
+ "ban_users",
34
+ "invite_users",
35
+ "pin_messages",
36
+ "add_admins",
37
+ "anonymous",
38
+ "manage_call",
39
+ "other",
40
+ "manage_topics",
41
+ "post_stories",
42
+ "edit_stories",
43
+ "delete_stories",
44
+ ]
45
+
46
+
47
+ async def _peer(client, chat):
48
+ return await client.get_input_entity(await resolve_chat(client, chat))
49
+
50
+
51
+ def _require_group(peer):
52
+ """Return 'channel' (supergroup/channel) or 'basic', or raise for non-groups."""
53
+ if isinstance(peer, types.InputPeerChannel):
54
+ return "channel"
55
+ if isinstance(peer, types.InputPeerChat):
56
+ return "basic"
57
+ raise TgError("this command only works for groups and channels")
58
+
59
+
60
+ def _users_by_id(res) -> dict:
61
+ return {u.id: u for u in getattr(res, "users", []) or []}
62
+
63
+
64
+ def _display(users: dict, uid) -> str:
65
+ u = users.get(uid)
66
+ if u is None:
67
+ return str(uid)
68
+ name = " ".join(x for x in (u.first_name, u.last_name) if x)
69
+ return name or getattr(u, "username", None) and f"@{u.username}" or str(uid)
70
+
71
+
72
+ def _action_text(action) -> str:
73
+ name = type(action).__name__.removeprefix("AdminLogEventAction")
74
+ return re.sub(r"([a-z])([A-Z])", r"\1 \2", name).lower()
75
+
76
+
77
+ async def cmd_new_group(args) -> None:
78
+ if not args.users:
79
+ raise TgError("a group needs at least one member", "e.g. tg new-group Friends @alice")
80
+ async with run_with_client(args) as client:
81
+ res = await client(functions.messages.CreateChatRequest(users=args.users, title=args.title))
82
+ upd = getattr(res, "updates", res)
83
+ chat = next(iter(getattr(upd, "chats", []) or []), None)
84
+ print(f"Group created: {args.title} (id: {getattr(chat, 'id', '?')})")
85
+
86
+
87
+ async def cmd_new_channel(args) -> None:
88
+ async with run_with_client(args) as client:
89
+ res = await client(
90
+ functions.channels.CreateChannelRequest(
91
+ title=args.title,
92
+ about=args.about or "",
93
+ broadcast=not args.group,
94
+ megagroup=args.group,
95
+ )
96
+ )
97
+ ch = next(iter(getattr(res, "chats", []) or []), None)
98
+ kind = "supergroup" if args.group else "channel"
99
+ if ch is not None and getattr(ch, "id", None):
100
+ print(f"{kind.capitalize()} created: {args.title} (id: -100{ch.id})")
101
+ else:
102
+ print(f"{kind.capitalize()} created: {args.title}")
103
+
104
+
105
+ async def cmd_invite(args) -> None:
106
+ async with run_with_client(args) as client:
107
+ peer = await _peer(client, args.chat)
108
+ kind = _require_group(peer)
109
+ n = 0
110
+ if kind == "channel":
111
+ res = await client(functions.channels.InviteToChannelRequest(peer, args.users))
112
+ missing = getattr(res, "missing_invitees", []) or []
113
+ n = len(args.users) - len(missing)
114
+ if missing:
115
+ print(f"warning: {len(missing)} user(s) could not be invited (privacy settings)")
116
+ else:
117
+ for u in args.users:
118
+ await client(
119
+ functions.messages.AddChatUserRequest(
120
+ chat_id=peer.chat_id, user_id=u, fwd_limit=0
121
+ )
122
+ )
123
+ n += 1
124
+ print(f"Invited {n} user(s)")
125
+
126
+
127
+ async def cmd_kick(args) -> None:
128
+ async with run_with_client(args) as client:
129
+ peer = await _peer(client, args.chat)
130
+ kind = _require_group(peer)
131
+ for u in args.users:
132
+ if kind == "channel":
133
+ # no dedicated kick call: "remove" == view-messages ban, as official clients do
134
+ await client(
135
+ functions.channels.EditBannedRequest(
136
+ peer,
137
+ u,
138
+ types.ChatBannedRights(until_date=None, view_messages=True),
139
+ )
140
+ )
141
+ else:
142
+ await client(
143
+ functions.messages.DeleteChatUserRequest(chat_id=peer.chat_id, user_id=u)
144
+ )
145
+ print(f"Kicked {len(args.users)} user(s)")
146
+
147
+
148
+ async def cmd_leave(args) -> None:
149
+ async with run_with_client(args) as client:
150
+ peer = await _peer(client, args.chat)
151
+ kind = _require_group(peer)
152
+ if kind == "channel":
153
+ await client(functions.channels.LeaveChannelRequest(peer))
154
+ else:
155
+ await client(
156
+ functions.messages.DeleteChatUserRequest(
157
+ chat_id=peer.chat_id, user_id=types.InputUserSelf()
158
+ )
159
+ )
160
+ print("Left the group")
161
+
162
+
163
+ async def _require_channel(peer):
164
+ if _require_group(peer) != "channel":
165
+ raise TgError(
166
+ "this command is only supported in supergroups and channels",
167
+ "basic groups have no ban list / slow mode / topics",
168
+ )
169
+ return peer
170
+
171
+
172
+ async def cmd_ban(args) -> None:
173
+ until = None
174
+ if args.days:
175
+ until = datetime.now(timezone.utc) + timedelta(days=args.days)
176
+ async with run_with_client(args) as client:
177
+ peer = await _require_channel(await _peer(client, args.chat))
178
+ await client(
179
+ functions.channels.EditBannedRequest(
180
+ peer,
181
+ args.user,
182
+ types.ChatBannedRights(until_date=until, view_messages=True),
183
+ )
184
+ )
185
+ print(f"Banned {args.user}" + (f" for {args.days}d" if args.days else " permanently"))
186
+
187
+
188
+ async def cmd_unban(args) -> None:
189
+ async with run_with_client(args) as client:
190
+ peer = await _require_channel(await _peer(client, args.chat))
191
+ await client(
192
+ functions.channels.EditBannedRequest(
193
+ peer, args.user, types.ChatBannedRights(until_date=None)
194
+ )
195
+ )
196
+ print(f"Unbanned {args.user}")
197
+
198
+
199
+ async def cmd_banned(args) -> None:
200
+ async with run_with_client(args) as client:
201
+ peer = await _require_channel(await _peer(client, args.chat))
202
+ res = await client(
203
+ functions.channels.GetParticipantsRequest(
204
+ # despite the name, Kicked (not Banned) is the filter that
205
+ # actually returns banned users on the current TL layer
206
+ peer,
207
+ types.ChannelParticipantsKicked(q=""),
208
+ offset=0,
209
+ limit=100,
210
+ hash=0,
211
+ )
212
+ )
213
+ rows = [p for p in res.participants if isinstance(p, types.ChannelParticipantBanned)]
214
+ users = _users_by_id(res)
215
+ if args.json:
216
+ print(json_pp([p.to_dict() for p in rows]))
217
+ return
218
+ if not rows:
219
+ print("(no banned users)")
220
+ return
221
+ for p in rows:
222
+ uid = p.peer.user_id
223
+ until = getattr(p.banned_rights, "until_date", None)
224
+ dur = f"until {until:%Y-%m-%d}" if until and until.year > 1971 else "permanent"
225
+ print(f"{uid:>14} {_display(users, uid)} {dur} by {p.kicked_by}")
226
+
227
+
228
+ async def cmd_admins(args) -> None:
229
+ async with run_with_client(args) as client:
230
+ peer = await _peer(client, args.chat)
231
+ kind = _require_group(peer)
232
+ if kind == "channel":
233
+ res = await client(
234
+ functions.channels.GetParticipantsRequest(
235
+ peer,
236
+ types.ChannelParticipantsAdmins(),
237
+ offset=0,
238
+ limit=50,
239
+ hash=0,
240
+ )
241
+ )
242
+ rows = []
243
+ for p in res.participants:
244
+ if isinstance(p, types.ChannelParticipantCreator):
245
+ rows.append((p.user_id, p.rank or "creator"))
246
+ else:
247
+ rows.append((p.user_id, getattr(p, "rank", "") or "admin"))
248
+ users = _users_by_id(res)
249
+ else:
250
+ res = await client(functions.messages.GetFullChatRequest(chat_id=peer.chat_id))
251
+ users = _users_by_id(res)
252
+ rows = []
253
+ for p in res.full_chat.participants.participants:
254
+ if isinstance(p, types.ChatParticipantCreator):
255
+ rows.append((p.user_id, "creator"))
256
+ elif isinstance(p, types.ChatParticipantAdmin):
257
+ rows.append((p.user_id, "admin"))
258
+ if args.json:
259
+ print(
260
+ json_pp(
261
+ [{"id": uid, "rank": rank, "user": _display(users, uid)} for uid, rank in rows]
262
+ )
263
+ )
264
+ return
265
+ if not rows:
266
+ print("(no admins)")
267
+ return
268
+ for uid, rank in rows:
269
+ print(f"{uid:>14} {rank:<8} {_display(users, uid)}")
270
+
271
+
272
+ async def cmd_promote(args) -> None:
273
+ async with run_with_client(args) as client:
274
+ peer = await _peer(client, args.chat)
275
+ if _require_group(peer) == "channel":
276
+ rank = args.title or "admin"
277
+ await client(
278
+ functions.channels.EditAdminRequest(
279
+ peer,
280
+ args.user,
281
+ types.ChatAdminRights(**_PROMOTE_RIGHTS),
282
+ rank=rank,
283
+ )
284
+ )
285
+ print(f"Promoted {args.user} (rank: {rank})")
286
+ else:
287
+ if args.title:
288
+ raise TgError("basic groups have no admin titles")
289
+ await client(
290
+ functions.messages.EditChatAdminRequest(
291
+ chat_id=peer.chat_id, user_id=args.user, is_admin=True
292
+ )
293
+ )
294
+ print(f"Promoted {args.user}")
295
+
296
+
297
+ async def cmd_demote(args) -> None:
298
+ async with run_with_client(args) as client:
299
+ peer = await _peer(client, args.chat)
300
+ if _require_group(peer) == "channel":
301
+ await client(
302
+ functions.channels.EditAdminRequest(
303
+ peer, args.user, types.ChatAdminRights(), rank=""
304
+ )
305
+ )
306
+ else:
307
+ await client(
308
+ functions.messages.EditChatAdminRequest(
309
+ chat_id=peer.chat_id, user_id=args.user, is_admin=False
310
+ )
311
+ )
312
+ print(f"Demoted {args.user}")
313
+
314
+
315
+ async def cmd_admin_rights(args) -> None:
316
+ granted = {f: True for f in _RIGHT_FLAGS if getattr(args, f, False)}
317
+ if not granted and not args.rank:
318
+ raise TgError(
319
+ "no rights requested",
320
+ "pass flags like --pin-messages --delete-messages,"
321
+ " or use `tg promote` for the standard set",
322
+ )
323
+ async with run_with_client(args) as client:
324
+ peer = await _require_channel(await _peer(client, args.chat))
325
+ await client(
326
+ functions.channels.EditAdminRequest(
327
+ peer, args.user, types.ChatAdminRights(**granted), rank=args.rank or ""
328
+ )
329
+ )
330
+ names = ", ".join(granted) or "(none)"
331
+ print(
332
+ f"Rights set for {args.user}: {names}" + (f" (rank: {args.rank})" if args.rank else "")
333
+ )
334
+
335
+
336
+ async def cmd_slow_mode(args) -> None:
337
+ seconds = args.seconds or 0
338
+ async with run_with_client(args) as client:
339
+ peer = await _require_channel(await _peer(client, args.chat))
340
+ await client(functions.channels.ToggleSlowModeRequest(peer, seconds))
341
+ print(f"Slow mode {'disabled' if seconds == 0 else f'set to {seconds}s'}")
342
+
343
+
344
+ async def _forum_check(client, peer):
345
+ ent = await client.get_entity(peer)
346
+ if not getattr(ent, "forum", False):
347
+ raise TgError("this group has forum mode disabled", "topics only exist in forum groups")
348
+ return ent
349
+
350
+
351
+ async def cmd_topics(args) -> None:
352
+ async with run_with_client(args) as client:
353
+ peer = await _peer(client, args.chat)
354
+ await _require_channel(peer)
355
+ await _forum_check(client, peer)
356
+ res = await client(
357
+ functions.messages.GetForumTopicsRequest(
358
+ peer=peer,
359
+ offset_date=None,
360
+ offset_id=0,
361
+ offset_topic=0,
362
+ limit=args.n,
363
+ )
364
+ )
365
+ topics = getattr(res, "topics", []) or []
366
+ if args.json:
367
+ print(json_pp([t.to_dict() for t in topics]))
368
+ return
369
+ if not topics:
370
+ print("(no topics)")
371
+ return
372
+ for t in topics:
373
+ unread = f" ({t.unread_count} unread)" if t.unread_count else ""
374
+ print(f"[{t.id}] {t.title}{unread}")
375
+
376
+
377
+ async def cmd_topic_create(args) -> None:
378
+ async with run_with_client(args) as client:
379
+ peer = await _peer(client, args.chat)
380
+ await _require_channel(peer)
381
+ await _forum_check(client, peer)
382
+ res = await client(
383
+ functions.messages.CreateForumTopicRequest(
384
+ peer=peer, title=args.title, random_id=random.randint(1, 2**31)
385
+ )
386
+ )
387
+ topic_id = None
388
+ for u in getattr(res, "updates", []) or []:
389
+ msg = getattr(u, "message", None)
390
+ if msg is not None:
391
+ topic_id = msg.id
392
+ break
393
+ if topic_id is None: # fallback: find the fresh topic by title
394
+ lst = await client(
395
+ functions.messages.GetForumTopicsRequest(
396
+ peer=peer, offset_date=None, offset_id=0, offset_topic=0, limit=20
397
+ )
398
+ )
399
+ topic_id = next((t.id for t in lst.topics if t.title == args.title), None)
400
+ if args.text and topic_id is not None:
401
+ await client.send_message(peer, " ".join(args.text), reply_to=topic_id)
402
+ print(f"Topic created: [{topic_id}] {args.title}")
403
+
404
+
405
+ async def cmd_admin_log(args) -> None:
406
+ async with run_with_client(args) as client:
407
+ peer = await _require_channel(await _peer(client, args.chat))
408
+ res = await client(
409
+ functions.channels.GetAdminLogRequest(
410
+ peer,
411
+ q="",
412
+ max_id=0,
413
+ min_id=0,
414
+ limit=args.n,
415
+ )
416
+ )
417
+ events = getattr(res, "events", []) or []
418
+ users = _users_by_id(res)
419
+ if args.json:
420
+ print(json_pp([e.to_dict() for e in events]))
421
+ return
422
+ if not events:
423
+ print("(no admin actions recorded)")
424
+ return
425
+ for e in events:
426
+ date = e.date.astimezone().strftime("%Y-%m-%d %H:%M")
427
+ print(f"[{e.id}] {date} {_display(users, e.user_id)}: {_action_text(e.action)}")
428
+
429
+
430
+ def setup(subparsers, common=None) -> None:
431
+ parents = [common] if common else []
432
+ sp = subparsers.add_parser(
433
+ "new-group", parents=parents, help="Create a basic group, e.g. tg new-group Friends @alice"
434
+ )
435
+ sp.add_argument("title")
436
+ sp.add_argument("users", nargs="+")
437
+ sp.set_defaults(func=cmd_new_group)
438
+
439
+ sp = subparsers.add_parser(
440
+ "new-channel", parents=parents, help="Create a channel (--group for a supergroup)"
441
+ )
442
+ sp.add_argument("title")
443
+ sp.add_argument("--about", default="")
444
+ sp.add_argument("--group", action="store_true", help="create a supergroup instead")
445
+ sp.set_defaults(func=cmd_new_channel)
446
+
447
+ sp = subparsers.add_parser("invite", parents=parents, help="Invite users to a group")
448
+ sp.add_argument("chat")
449
+ sp.add_argument("users", nargs="+")
450
+ sp.set_defaults(func=cmd_invite)
451
+
452
+ sp = subparsers.add_parser("kick", parents=parents, help="Remove users from a group")
453
+ sp.add_argument("chat")
454
+ sp.add_argument("users", nargs="+")
455
+ sp.set_defaults(func=cmd_kick)
456
+
457
+ sp = subparsers.add_parser("leave", parents=parents, help="Leave a group/channel")
458
+ sp.add_argument("chat")
459
+ sp.set_defaults(func=cmd_leave)
460
+
461
+ sp = subparsers.add_parser(
462
+ "ban", parents=parents, help="Ban a user (permanent unless [days] given); supergroups only"
463
+ )
464
+ sp.add_argument("chat")
465
+ sp.add_argument("user")
466
+ sp.add_argument("days", nargs="?", type=int, default=None)
467
+ sp.set_defaults(func=cmd_ban)
468
+
469
+ sp = subparsers.add_parser("unban", parents=parents, help="Unban a user; supergroups only")
470
+ sp.add_argument("chat")
471
+ sp.add_argument("user")
472
+ sp.set_defaults(func=cmd_unban)
473
+
474
+ sp = subparsers.add_parser(
475
+ "banned", parents=parents, help="List banned users; supergroups only"
476
+ )
477
+ sp.add_argument("chat")
478
+ sp.set_defaults(func=cmd_banned)
479
+
480
+ sp = subparsers.add_parser("admins", parents=parents, help="List admins (and their ranks)")
481
+ sp.add_argument("chat")
482
+ sp.set_defaults(func=cmd_admins)
483
+
484
+ sp = subparsers.add_parser(
485
+ "promote", parents=parents, help="Promote to admin with the standard right set"
486
+ )
487
+ sp.add_argument("chat")
488
+ sp.add_argument("user")
489
+ sp.add_argument("--title", default=None, help="admin rank/title")
490
+ sp.set_defaults(func=cmd_promote)
491
+
492
+ sp = subparsers.add_parser("demote", parents=parents, help="Remove admin rights")
493
+ sp.add_argument("chat")
494
+ sp.add_argument("user")
495
+ sp.set_defaults(func=cmd_demote)
496
+
497
+ sp = subparsers.add_parser(
498
+ "admin-rights", parents=parents, help="Set exact admin rights (supergroups only)"
499
+ )
500
+ sp.add_argument("chat")
501
+ sp.add_argument("user")
502
+ for f in _RIGHT_FLAGS:
503
+ sp.add_argument(f"--{f.replace('_', '-')}", action="store_true", dest=f)
504
+ sp.add_argument("--rank", default="", help="admin rank/title")
505
+ sp.set_defaults(func=cmd_admin_rights)
506
+
507
+ sp = subparsers.add_parser(
508
+ "slow-mode", parents=parents, help="Set slow mode seconds (0 = disable); supergroups only"
509
+ )
510
+ sp.add_argument("chat")
511
+ sp.add_argument("seconds", nargs="?", type=int, default=0)
512
+ sp.set_defaults(func=cmd_slow_mode)
513
+
514
+ sp = subparsers.add_parser("topics", parents=parents, help="List forum topics")
515
+ sp.add_argument("chat")
516
+ sp.add_argument("-n", type=int, default=20)
517
+ sp.set_defaults(func=cmd_topics)
518
+
519
+ sp = subparsers.add_parser(
520
+ "topic-create",
521
+ parents=parents,
522
+ help="Create a forum topic, optionally with a first message",
523
+ )
524
+ sp.add_argument("chat")
525
+ sp.add_argument("title")
526
+ sp.add_argument("text", nargs="*", default=[])
527
+ sp.set_defaults(func=cmd_topic_create)
528
+
529
+ sp = subparsers.add_parser(
530
+ "admin-log", parents=parents, help="Recent admin actions; supergroups only"
531
+ )
532
+ sp.add_argument("chat")
533
+ sp.add_argument("-n", type=int, default=20)
534
+ sp.set_defaults(func=cmd_admin_log)