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.
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/PKG-INFO +24 -1
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/README-zh.md +22 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/README.md +23 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/ROADMAP.md +15 -8
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/pyproject.toml +1 -1
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/__init__.py +1 -1
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/cli.py +2 -2
- telegram_userbot_cli-0.3.0/src/tgcli/groups.py +534 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.env.example +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.github/workflows/release.yml +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/.gitignore +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/LICENSE +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/__main__.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/auth.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/chats.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/client.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/config.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/dialogs.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/media.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/messages.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/output.py +0 -0
- {telegram_userbot_cli-0.2.0 → telegram_userbot_cli-0.3.0}/src/tgcli/resolve.py +0 -0
- {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.
|
|
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
|
-
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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
|
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
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.
|
|
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"
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|