tlgr-cli 2.0.1__py3-none-any.whl
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.
- tlgr/__init__.py +3 -0
- tlgr/__main__.py +6 -0
- tlgr/actions/__init__.py +45 -0
- tlgr/actions/forward.py +74 -0
- tlgr/actions/reply.py +32 -0
- tlgr/cli/__init__.py +259 -0
- tlgr/cli/confirm.py +55 -0
- tlgr/cli/errors.py +84 -0
- tlgr/cli/gen.py +690 -0
- tlgr/cli/globals.py +273 -0
- tlgr/cli/introspect.py +170 -0
- tlgr/cli/params.py +189 -0
- tlgr/cli/render.py +418 -0
- tlgr/core/__init__.py +0 -0
- tlgr/core/accounts.py +384 -0
- tlgr/core/config.py +358 -0
- tlgr/core/custom_tl.py +170 -0
- tlgr/core/errors.py +687 -0
- tlgr/core/eventtypes.py +1170 -0
- tlgr/core/identity.py +127 -0
- tlgr/core/launchd.py +122 -0
- tlgr/core/logging.py +194 -0
- tlgr/core/media.py +134 -0
- tlgr/core/output.py +251 -0
- tlgr/core/pagination.py +227 -0
- tlgr/core/paths.py +360 -0
- tlgr/core/peers.py +427 -0
- tlgr/core/process.py +138 -0
- tlgr/core/signing.py +38 -0
- tlgr/core/systemd.py +96 -0
- tlgr/core/telethon_compat.py +295 -0
- tlgr/core/text.py +211 -0
- tlgr/core/timefmt.py +199 -0
- tlgr/core/tl.py +98 -0
- tlgr/daemon/__init__.py +0 -0
- tlgr/daemon/app.py +869 -0
- tlgr/daemon/dispatch.py +446 -0
- tlgr/daemon/events.py +723 -0
- tlgr/daemon/files.py +431 -0
- tlgr/daemon/idle.py +119 -0
- tlgr/daemon/jobs.py +68 -0
- tlgr/daemon/main.py +161 -0
- tlgr/daemon/peercred.py +75 -0
- tlgr/daemon/policy.py +113 -0
- tlgr/daemon/preauth.py +366 -0
- tlgr/daemon/ratelimit.py +391 -0
- tlgr/daemon/server.py +24 -0
- tlgr/daemon/session.py +648 -0
- tlgr/daemon/sessions.py +274 -0
- tlgr/daemon/singleton.py +114 -0
- tlgr/daemon/stream.py +193 -0
- tlgr/daemon/transfers.py +219 -0
- tlgr/daemon/webhook.py +390 -0
- tlgr/data/catalog_index.json +1 -0
- tlgr/data/parity_waivers.toml +90 -0
- tlgr/filters/__init__.py +42 -0
- tlgr/filters/compose.py +121 -0
- tlgr/filters/content.py +85 -0
- tlgr/filters/context.py +114 -0
- tlgr/filters/message.py +161 -0
- tlgr/filters/temporal.py +87 -0
- tlgr/filters/user.py +36 -0
- tlgr/gateway/__init__.py +1 -0
- tlgr/gateway/config.py +161 -0
- tlgr/gateway/engine.py +215 -0
- tlgr/gateway/event.py +22 -0
- tlgr/jobs/__init__.py +0 -0
- tlgr/jobs/base.py +81 -0
- tlgr/jobs/client.py +37 -0
- tlgr/models/__init__.py +1220 -0
- tlgr/models/admin.py +744 -0
- tlgr/models/auth.py +510 -0
- tlgr/models/base.py +81 -0
- tlgr/models/bot.py +576 -0
- tlgr/models/business.py +265 -0
- tlgr/models/call.py +586 -0
- tlgr/models/config.py +101 -0
- tlgr/models/contact.py +481 -0
- tlgr/models/daemon.py +336 -0
- tlgr/models/dialog.py +626 -0
- tlgr/models/envelope.py +68 -0
- tlgr/models/error.py +30 -0
- tlgr/models/event.py +79 -0
- tlgr/models/export.py +66 -0
- tlgr/models/gift.py +275 -0
- tlgr/models/inline.py +84 -0
- tlgr/models/location.py +115 -0
- tlgr/models/media.py +507 -0
- tlgr/models/message.py +584 -0
- tlgr/models/net.py +232 -0
- tlgr/models/notify.py +105 -0
- tlgr/models/page.py +32 -0
- tlgr/models/payment.py +172 -0
- tlgr/models/peer.py +400 -0
- tlgr/models/poll.py +119 -0
- tlgr/models/premium.py +161 -0
- tlgr/models/privacy.py +93 -0
- tlgr/models/profile.py +217 -0
- tlgr/models/reaction.py +160 -0
- tlgr/models/resolve.py +175 -0
- tlgr/models/settings.py +103 -0
- tlgr/models/stars.py +101 -0
- tlgr/models/sticker.py +243 -0
- tlgr/models/story.py +467 -0
- tlgr/models/sync.py +105 -0
- tlgr/models/todo.py +36 -0
- tlgr/models/webapp.py +89 -0
- tlgr/ops/__init__.py +63 -0
- tlgr/ops/_admin.py +313 -0
- tlgr/ops/_auth.py +599 -0
- tlgr/ops/_bots.py +586 -0
- tlgr/ops/_calls.py +535 -0
- tlgr/ops/_common.py +160 -0
- tlgr/ops/_layer.py +46 -0
- tlgr/ops/_media.py +592 -0
- tlgr/ops/_params.py +212 -0
- tlgr/ops/_rights.py +402 -0
- tlgr/ops/_send.py +593 -0
- tlgr/ops/_serialize.py +667 -0
- tlgr/ops/_settings.py +306 -0
- tlgr/ops/_spec.py +167 -0
- tlgr/ops/_story.py +743 -0
- tlgr/ops/account.py +2604 -0
- tlgr/ops/agent.py +937 -0
- tlgr/ops/auth.py +1282 -0
- tlgr/ops/bot.py +4880 -0
- tlgr/ops/business.py +1520 -0
- tlgr/ops/call.py +1610 -0
- tlgr/ops/chat.py +4025 -0
- tlgr/ops/chat_admin.py +929 -0
- tlgr/ops/chat_extra.py +1061 -0
- tlgr/ops/chat_invite.py +716 -0
- tlgr/ops/chat_manage.py +1691 -0
- tlgr/ops/chat_member.py +1357 -0
- tlgr/ops/chat_stats.py +902 -0
- tlgr/ops/chat_topic.py +905 -0
- tlgr/ops/conference.py +791 -0
- tlgr/ops/config.py +1698 -0
- tlgr/ops/contact.py +2330 -0
- tlgr/ops/daemon.py +1397 -0
- tlgr/ops/draft.py +299 -0
- tlgr/ops/emoji.py +343 -0
- tlgr/ops/events.py +1327 -0
- tlgr/ops/export.py +596 -0
- tlgr/ops/folder.py +1322 -0
- tlgr/ops/gif.py +522 -0
- tlgr/ops/gift.py +1546 -0
- tlgr/ops/giveaway.py +541 -0
- tlgr/ops/inline.py +773 -0
- tlgr/ops/job.py +799 -0
- tlgr/ops/location.py +917 -0
- tlgr/ops/media.py +4495 -0
- tlgr/ops/message.py +3769 -0
- tlgr/ops/net.py +536 -0
- tlgr/ops/notify.py +840 -0
- tlgr/ops/passport.py +464 -0
- tlgr/ops/payment.py +907 -0
- tlgr/ops/poll.py +1078 -0
- tlgr/ops/premium.py +488 -0
- tlgr/ops/privacy.py +794 -0
- tlgr/ops/profile.py +1481 -0
- tlgr/ops/proxy.py +750 -0
- tlgr/ops/reaction.py +1475 -0
- tlgr/ops/resolve.py +1140 -0
- tlgr/ops/search.py +521 -0
- tlgr/ops/settings.py +1066 -0
- tlgr/ops/stars.py +594 -0
- tlgr/ops/sticker.py +1602 -0
- tlgr/ops/story.py +3216 -0
- tlgr/ops/sync.py +788 -0
- tlgr/ops/todo.py +514 -0
- tlgr/ops/user.py +1406 -0
- tlgr/ops/vc.py +2351 -0
- tlgr/ops/webapp.py +717 -0
- tlgr/ops/webhook.py +418 -0
- tlgr/parity.py +386 -0
- tlgr/processors/__init__.py +125 -0
- tlgr/processors/regex.py +26 -0
- tlgr/processors/text.py +56 -0
- tlgr/registry.py +519 -0
- tlgr/schema.py +173 -0
- tlgr/transport/__init__.py +30 -0
- tlgr/transport/autostart.py +293 -0
- tlgr/transport/client.py +805 -0
- tlgr/transport/ndjson.py +44 -0
- tlgr/version.py +31 -0
- tlgr_cli-2.0.1.dist-info/METADATA +957 -0
- tlgr_cli-2.0.1.dist-info/RECORD +192 -0
- tlgr_cli-2.0.1.dist-info/WHEEL +5 -0
- tlgr_cli-2.0.1.dist-info/entry_points.txt +2 -0
- tlgr_cli-2.0.1.dist-info/licenses/LICENSE +21 -0
- tlgr_cli-2.0.1.dist-info/top_level.txt +1 -0
tlgr/ops/privacy.py
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"""The `privacy` group: Settings ▸ Privacy and Security, minus the sessions.
|
|
2
|
+
|
|
3
|
+
Fourteen privacy keys, eight global switches and two blocklists, and the
|
|
4
|
+
whole group exists to make two dangerous APIs safe to use from a script.
|
|
5
|
+
|
|
6
|
+
* **`account.setPrivacy` replaces the whole ordered rule vector.** Sending
|
|
7
|
+
"allow contacts" wipes every exception the user had. So `privacy set`
|
|
8
|
+
always GETs first, and the `--add-*`/`--remove` flags exist precisely so a
|
|
9
|
+
script never has to re-state a list it did not mean to touch.
|
|
10
|
+
* **`account.setGlobalPrivacySettings` replaces the whole constructor.** Same
|
|
11
|
+
hazard, same answer: read, patch the named fields, write. A switch nobody
|
|
12
|
+
mentioned is written back exactly as it was found.
|
|
13
|
+
|
|
14
|
+
The rule vocabulary — `everybody`, `contacts`, `close-friends`, `premium`,
|
|
15
|
+
`bots`, `nobody` — is the one the official clients show, not the one the TL
|
|
16
|
+
schema uses, because `privacyValueDisallowAll` plus `privacyValueAllowUsers`
|
|
17
|
+
is a *shape* rather than a setting anybody chose.
|
|
18
|
+
|
|
19
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Annotated, Any
|
|
25
|
+
|
|
26
|
+
from tlgr.core.errors import UsageError
|
|
27
|
+
from tlgr.core.pagination import PageKind
|
|
28
|
+
from tlgr.models.base import Request
|
|
29
|
+
from tlgr.models.contact import BlockedPeer, BlockedSet
|
|
30
|
+
from tlgr.models.page import Page
|
|
31
|
+
from tlgr.models.peer import PeerRef
|
|
32
|
+
from tlgr.models.privacy import GlobalPrivacy, PaidMessageRevenue, PrivacyRule, PrivacySettings
|
|
33
|
+
from tlgr.ops import _settings
|
|
34
|
+
from tlgr.ops._common import client
|
|
35
|
+
from tlgr.ops._params import arg, opt
|
|
36
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
37
|
+
|
|
38
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|
|
39
|
+
|
|
40
|
+
#: tlgr's key name → the `inputPrivacyKey*` class name. The tlgr spelling is
|
|
41
|
+
#: the GUI's row label; the TL name is an implementation detail nobody should
|
|
42
|
+
#: have to learn to mute their last-seen time.
|
|
43
|
+
KEYS: dict[str, str] = {
|
|
44
|
+
"last-seen": "InputPrivacyKeyStatusTimestamp",
|
|
45
|
+
"profile-photo": "InputPrivacyKeyProfilePhoto",
|
|
46
|
+
"phone-number": "InputPrivacyKeyPhoneNumber",
|
|
47
|
+
"forwards": "InputPrivacyKeyForwards",
|
|
48
|
+
"calls": "InputPrivacyKeyPhoneCall",
|
|
49
|
+
"phone-p2p": "InputPrivacyKeyPhoneP2P",
|
|
50
|
+
"chat-invite": "InputPrivacyKeyChatInvite",
|
|
51
|
+
"voice-messages": "InputPrivacyKeyVoiceMessages",
|
|
52
|
+
"bio": "InputPrivacyKeyAbout",
|
|
53
|
+
"birthday": "InputPrivacyKeyBirthday",
|
|
54
|
+
"gifts-auto-save": "InputPrivacyKeyStarGiftsAutoSave",
|
|
55
|
+
"no-paid-messages": "InputPrivacyKeyNoPaidMessages",
|
|
56
|
+
"saved-music": "InputPrivacyKeySavedMusic",
|
|
57
|
+
"added-by-phone": "InputPrivacyKeyAddedByPhone",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#: The base rules, in the order `account.setPrivacy` wants them written: the
|
|
61
|
+
#: broad allow/disallow first, exceptions after, because the server applies
|
|
62
|
+
#: the vector in order and a trailing `allowAll` would undo everything.
|
|
63
|
+
BASES = ("everybody", "contacts", "close-friends", "premium", "bots", "nobody")
|
|
64
|
+
|
|
65
|
+
#: `disallowedGiftsSettings` field ⇄ the keyword `--disallow-gifts` takes.
|
|
66
|
+
GIFT_KINDS: dict[str, str] = {
|
|
67
|
+
"unlimited": "disallow_unlimited_stargifts",
|
|
68
|
+
"limited": "disallow_limited_stargifts",
|
|
69
|
+
"unique": "disallow_unique_stargifts",
|
|
70
|
+
"premium": "disallow_premium_gifts",
|
|
71
|
+
"from-channels": "disallow_stargifts_from_channels",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _key_tl(name: str) -> Any:
|
|
76
|
+
"""The `inputPrivacyKey*` for a tlgr key name, or a usage error that lists them."""
|
|
77
|
+
from telethon.tl import types
|
|
78
|
+
|
|
79
|
+
wanted = name.strip().lower()
|
|
80
|
+
if wanted == "stories":
|
|
81
|
+
raise UsageError(
|
|
82
|
+
"story visibility is not a privacy key: the audience is chosen per "
|
|
83
|
+
"story (`story post --audience`) and the exclusion list is "
|
|
84
|
+
"`story blocklist set` / `privacy blocked set --stories`",
|
|
85
|
+
field="key",
|
|
86
|
+
)
|
|
87
|
+
if wanted not in KEYS:
|
|
88
|
+
raise UsageError(
|
|
89
|
+
f"unknown privacy key {name!r}; one of: {' '.join(sorted(KEYS))}", field="key"
|
|
90
|
+
)
|
|
91
|
+
return getattr(types, KEYS[wanted])()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _rules_model(key: str, rules: Any) -> PrivacySettings:
|
|
95
|
+
"""A `privacyValue*` vector split into the shape the GUI shows.
|
|
96
|
+
|
|
97
|
+
The base is whichever broad rule the vector carries; the four exception
|
|
98
|
+
lists are the user/chat rules beside it. `raw_rules` keeps the server's
|
|
99
|
+
own ordering so a later write can reproduce it exactly.
|
|
100
|
+
"""
|
|
101
|
+
model = PrivacySettings(key=key, base="nobody")
|
|
102
|
+
for rule in rules or []:
|
|
103
|
+
name = type(rule).__name__.removeprefix("PrivacyValue")
|
|
104
|
+
action = "allow" if name.startswith("Allow") else "disallow"
|
|
105
|
+
scope = name.removeprefix("Allow").removeprefix("Disallow")
|
|
106
|
+
ids = [int(v) for v in (getattr(rule, "users", None) or getattr(rule, "chats", None) or [])]
|
|
107
|
+
model.raw_rules.append(
|
|
108
|
+
PrivacyRule(action=action, scope=_SCOPE_NAMES.get(scope, scope.lower()), ids=ids)
|
|
109
|
+
)
|
|
110
|
+
if scope == "Users":
|
|
111
|
+
(model.allow_users if action == "allow" else model.deny_users).extend(ids)
|
|
112
|
+
elif scope == "ChatParticipants":
|
|
113
|
+
(model.allow_chats if action == "allow" else model.deny_chats).extend(ids)
|
|
114
|
+
elif scope in _BASE_FOR:
|
|
115
|
+
model.base = _BASE_FOR[scope] if action == "allow" else _DENY_BASE[scope]
|
|
116
|
+
return model
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_SCOPE_NAMES = {
|
|
120
|
+
"All": "all",
|
|
121
|
+
"Contacts": "contacts",
|
|
122
|
+
"CloseFriends": "close-friends",
|
|
123
|
+
"Premium": "premium",
|
|
124
|
+
"Bots": "bots",
|
|
125
|
+
"Users": "users",
|
|
126
|
+
"ChatParticipants": "chats",
|
|
127
|
+
}
|
|
128
|
+
_BASE_FOR = {
|
|
129
|
+
"All": "everybody",
|
|
130
|
+
"Contacts": "contacts",
|
|
131
|
+
"CloseFriends": "close-friends",
|
|
132
|
+
"Premium": "premium",
|
|
133
|
+
"Bots": "bots",
|
|
134
|
+
}
|
|
135
|
+
#: `disallowContacts` and friends are the *other* half of the same switch:
|
|
136
|
+
#: "not my contacts" is how "nobody" is spelled for some keys.
|
|
137
|
+
_DENY_BASE = {
|
|
138
|
+
"All": "nobody",
|
|
139
|
+
"Contacts": "nobody",
|
|
140
|
+
"CloseFriends": "nobody",
|
|
141
|
+
"Premium": "nobody",
|
|
142
|
+
"Bots": "nobody",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# privacy get / set
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class GetReq(Request):
|
|
152
|
+
key: Annotated[
|
|
153
|
+
str | None,
|
|
154
|
+
arg(0, metavar="KEY", required=False, help="One key; omit for every key."),
|
|
155
|
+
] = None
|
|
156
|
+
resolve: Annotated[
|
|
157
|
+
bool, opt("--resolve/--no-resolve", help="Resolve exception ids to names.")
|
|
158
|
+
] = True
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def get(ctx: OpContext, req: GetReq) -> Page[PrivacySettings]:
|
|
162
|
+
"""Read one privacy setting, or all of them.
|
|
163
|
+
|
|
164
|
+
The output is exactly what `privacy set` accepts back, which is what
|
|
165
|
+
makes "copy this account's privacy to that one" a pipeline rather than a
|
|
166
|
+
reading exercise.
|
|
167
|
+
"""
|
|
168
|
+
from telethon.tl.functions import account as fn
|
|
169
|
+
|
|
170
|
+
handle = client(ctx)
|
|
171
|
+
wanted = [req.key.strip().lower()] if req.key else sorted(KEYS)
|
|
172
|
+
rows: list[PrivacySettings] = []
|
|
173
|
+
for name in wanted:
|
|
174
|
+
answer = await handle(fn.GetPrivacyRequest(key=_key_tl(name)))
|
|
175
|
+
model = _rules_model(name, getattr(answer, "rules", None))
|
|
176
|
+
if req.resolve:
|
|
177
|
+
known = _settings.entity_map(answer)
|
|
178
|
+
model.peers = [
|
|
179
|
+
peer
|
|
180
|
+
for raw_id in (*model.allow_users, *model.deny_users)
|
|
181
|
+
if (peer := _settings.peer_model(known.get(raw_id))) is not None
|
|
182
|
+
]
|
|
183
|
+
rows.append(model)
|
|
184
|
+
return Page(items=rows, has_more=False, total=len(rows))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
SPEC_GET = OperationSpec(
|
|
188
|
+
id="privacy.get",
|
|
189
|
+
request=GetReq,
|
|
190
|
+
response=Page[PrivacySettings],
|
|
191
|
+
impl=get,
|
|
192
|
+
summary="Read one privacy setting, or all of them",
|
|
193
|
+
description=(
|
|
194
|
+
"`base` is the headline value the GUI shows and the four lists are "
|
|
195
|
+
"its exceptions; `raw_rules` keeps the server's own ordered vector so "
|
|
196
|
+
"nothing is lost in the translation."
|
|
197
|
+
),
|
|
198
|
+
paginated=PageKind.LOCAL,
|
|
199
|
+
idempotent=True,
|
|
200
|
+
columns=("key", "base", "allow_users", "deny_users"),
|
|
201
|
+
headers=("Key", "Base", "Always allow", "Never allow"),
|
|
202
|
+
example={
|
|
203
|
+
"items": [{"key": "last-seen", "base": "contacts", "deny_users": [777123]}],
|
|
204
|
+
"has_more": False,
|
|
205
|
+
},
|
|
206
|
+
example_args="privacy get last-seen",
|
|
207
|
+
covers=(
|
|
208
|
+
"contacts-users.privacy-about",
|
|
209
|
+
"contacts-users.privacy-added-by-phone",
|
|
210
|
+
"contacts-users.privacy-chat-invite",
|
|
211
|
+
"contacts-users.privacy-forwards",
|
|
212
|
+
"contacts-users.privacy-gifts",
|
|
213
|
+
"contacts-users.privacy-no-paid-messages",
|
|
214
|
+
"contacts-users.privacy-phone-number",
|
|
215
|
+
"contacts-users.privacy-voice-messages",
|
|
216
|
+
"gift.auto-save-privacy",
|
|
217
|
+
"privacy.get-rules",
|
|
218
|
+
"privacy.key-birthday",
|
|
219
|
+
"privacy.key-calls",
|
|
220
|
+
"privacy.key-no-paid-messages",
|
|
221
|
+
"privacy.key-phone-number",
|
|
222
|
+
"privacy.key-voice-messages",
|
|
223
|
+
),
|
|
224
|
+
covers_partial=(
|
|
225
|
+
"privacy.key-bio",
|
|
226
|
+
"privacy.key-chat-invite",
|
|
227
|
+
"privacy.key-forwards",
|
|
228
|
+
"privacy.key-gifts-auto-save",
|
|
229
|
+
"privacy.key-last-seen",
|
|
230
|
+
"privacy.key-profile-photo",
|
|
231
|
+
"privacy.key-saved-music",
|
|
232
|
+
),
|
|
233
|
+
coverage_note="Writing any of these keys is `privacy set`.",
|
|
234
|
+
tags=frozenset({"agent-safe"}),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class SetReq(Request):
|
|
239
|
+
key: Annotated[str, arg(0, metavar="KEY", help="The privacy key to change.")]
|
|
240
|
+
rule: Annotated[
|
|
241
|
+
str | None,
|
|
242
|
+
arg(1, metavar="RULE", required=False, help="everybody|contacts|close-friends|nobody…"),
|
|
243
|
+
] = None
|
|
244
|
+
allow: Annotated[
|
|
245
|
+
str | None, opt("--allow", metavar="LIST", help="Replace the 'always allow' list.")
|
|
246
|
+
] = None
|
|
247
|
+
disallow: Annotated[
|
|
248
|
+
str | None, opt("--disallow", metavar="LIST", help="Replace the 'never allow' list.")
|
|
249
|
+
] = None
|
|
250
|
+
add_allow: Annotated[
|
|
251
|
+
str | None, opt("--add-allow", metavar="LIST", help="Append to the allow list.")
|
|
252
|
+
] = None
|
|
253
|
+
add_disallow: Annotated[
|
|
254
|
+
str | None, opt("--add-disallow", metavar="LIST", help="Append to the deny list.")
|
|
255
|
+
] = None
|
|
256
|
+
remove: Annotated[
|
|
257
|
+
str | None, opt("--remove", metavar="LIST", help="Drop these from both lists.")
|
|
258
|
+
] = None
|
|
259
|
+
clear_exceptions: Annotated[
|
|
260
|
+
bool, opt("--clear-exceptions", help="Send only the base rule.")
|
|
261
|
+
] = False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def _ids_of(ctx: OpContext, text: str | None) -> tuple[list[int], list[int]]:
|
|
265
|
+
"""A comma-separated peer list, split into `(user ids, chat ids)`."""
|
|
266
|
+
users: list[int] = []
|
|
267
|
+
chats: list[int] = []
|
|
268
|
+
for entry in (text or "").split(","):
|
|
269
|
+
ref = entry.strip()
|
|
270
|
+
if not ref:
|
|
271
|
+
continue
|
|
272
|
+
peer = await _settings.resolve(ctx, ref)
|
|
273
|
+
marked = _settings.peer_of(peer)
|
|
274
|
+
(users if marked > 0 else chats).append(abs(marked) if marked < 0 else marked)
|
|
275
|
+
return users, chats
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def set_(ctx: OpContext, req: SetReq) -> PrivacySettings:
|
|
279
|
+
"""Change a privacy key: a base rule plus optional exception lists.
|
|
280
|
+
|
|
281
|
+
Read-modify-write, always. `account.setPrivacy` takes the *complete*
|
|
282
|
+
ordered vector, so sending only what changed would delete everything
|
|
283
|
+
else — which is the whole reason `--add-allow` and `--remove` exist.
|
|
284
|
+
"""
|
|
285
|
+
from telethon.tl import types
|
|
286
|
+
from telethon.tl.functions import account as fn
|
|
287
|
+
|
|
288
|
+
name = req.key.strip().lower()
|
|
289
|
+
key = _key_tl(name)
|
|
290
|
+
handle = client(ctx)
|
|
291
|
+
current = _rules_model(
|
|
292
|
+
name, getattr(await handle(fn.GetPrivacyRequest(key=key)), "rules", None)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
base = current.base
|
|
296
|
+
if req.rule is not None:
|
|
297
|
+
base = req.rule.strip().lower()
|
|
298
|
+
if base not in BASES:
|
|
299
|
+
raise UsageError(f"RULE is one of: {' '.join(BASES)}", field="rule")
|
|
300
|
+
|
|
301
|
+
allow_users, allow_chats = list(current.allow_users), list(current.allow_chats)
|
|
302
|
+
deny_users, deny_chats = list(current.deny_users), list(current.deny_chats)
|
|
303
|
+
|
|
304
|
+
if req.clear_exceptions:
|
|
305
|
+
allow_users, allow_chats, deny_users, deny_chats = [], [], [], []
|
|
306
|
+
if req.allow is not None:
|
|
307
|
+
allow_users, allow_chats = await _ids_of(ctx, req.allow)
|
|
308
|
+
if req.disallow is not None:
|
|
309
|
+
deny_users, deny_chats = await _ids_of(ctx, req.disallow)
|
|
310
|
+
if req.add_allow:
|
|
311
|
+
more_users, more_chats = await _ids_of(ctx, req.add_allow)
|
|
312
|
+
allow_users += [i for i in more_users if i not in allow_users]
|
|
313
|
+
allow_chats += [i for i in more_chats if i not in allow_chats]
|
|
314
|
+
if req.add_disallow:
|
|
315
|
+
more_users, more_chats = await _ids_of(ctx, req.add_disallow)
|
|
316
|
+
deny_users += [i for i in more_users if i not in deny_users]
|
|
317
|
+
deny_chats += [i for i in more_chats if i not in deny_chats]
|
|
318
|
+
if req.remove:
|
|
319
|
+
gone_users, gone_chats = await _ids_of(ctx, req.remove)
|
|
320
|
+
allow_users = [i for i in allow_users if i not in gone_users]
|
|
321
|
+
deny_users = [i for i in deny_users if i not in gone_users]
|
|
322
|
+
allow_chats = [i for i in allow_chats if i not in gone_chats]
|
|
323
|
+
deny_chats = [i for i in deny_chats if i not in gone_chats]
|
|
324
|
+
|
|
325
|
+
rules: list[Any] = []
|
|
326
|
+
# The exceptions go first: the server evaluates the vector in order, so a
|
|
327
|
+
# broad rule written before them would decide every case on its own.
|
|
328
|
+
if allow_users:
|
|
329
|
+
rules.append(
|
|
330
|
+
types.InputPrivacyValueAllowUsers(
|
|
331
|
+
users=[await _settings.input_user(ctx, str(i)) for i in allow_users]
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
if deny_users:
|
|
335
|
+
rules.append(
|
|
336
|
+
types.InputPrivacyValueDisallowUsers(
|
|
337
|
+
users=[await _settings.input_user(ctx, str(i)) for i in deny_users]
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
if allow_chats:
|
|
341
|
+
rules.append(types.InputPrivacyValueAllowChatParticipants(chats=allow_chats))
|
|
342
|
+
if deny_chats:
|
|
343
|
+
rules.append(types.InputPrivacyValueDisallowChatParticipants(chats=deny_chats))
|
|
344
|
+
rules.append(_base_rule(base))
|
|
345
|
+
|
|
346
|
+
answer = await handle(fn.SetPrivacyRequest(key=key, rules=rules))
|
|
347
|
+
ctx.emit("privacy_set", {"key": name, "base": base})
|
|
348
|
+
return _rules_model(name, getattr(answer, "rules", None))
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _base_rule(base: str) -> Any:
|
|
352
|
+
from telethon.tl import types
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"everybody": types.InputPrivacyValueAllowAll,
|
|
356
|
+
"contacts": types.InputPrivacyValueAllowContacts,
|
|
357
|
+
"close-friends": types.InputPrivacyValueAllowCloseFriends,
|
|
358
|
+
"premium": types.InputPrivacyValueAllowPremium,
|
|
359
|
+
"bots": types.InputPrivacyValueAllowBots,
|
|
360
|
+
"nobody": types.InputPrivacyValueDisallowAll,
|
|
361
|
+
}[base]()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
SPEC_SET = OperationSpec(
|
|
365
|
+
id="privacy.set",
|
|
366
|
+
request=SetReq,
|
|
367
|
+
response=PrivacySettings,
|
|
368
|
+
impl=set_,
|
|
369
|
+
summary="Change a privacy setting: a base rule plus optional exception lists",
|
|
370
|
+
description=(
|
|
371
|
+
"`account.setPrivacy` replaces the whole ordered vector, so this "
|
|
372
|
+
"always reads the current rules first. `--add-allow`/`--remove` edit "
|
|
373
|
+
"the lists in place; `--allow`/`--disallow` replace them."
|
|
374
|
+
),
|
|
375
|
+
mutating=True,
|
|
376
|
+
idempotent=True,
|
|
377
|
+
rate_class="send",
|
|
378
|
+
columns=("key", "base", "allow_users", "deny_users"),
|
|
379
|
+
headers=("Key", "Base", "Always allow", "Never allow"),
|
|
380
|
+
example={"key": "last-seen", "base": "contacts", "deny_users": [777123]},
|
|
381
|
+
example_args="privacy set last-seen contacts --add-disallow @nosy",
|
|
382
|
+
covers=(
|
|
383
|
+
"bots.privacy-rule-bots",
|
|
384
|
+
"calls.privacy-p2p",
|
|
385
|
+
"calls.privacy-who-can-call",
|
|
386
|
+
"contacts-users.privacy-exception-lists",
|
|
387
|
+
"contacts-users.user-status-reveal",
|
|
388
|
+
"dialogs.new-chats-privacy",
|
|
389
|
+
"gift.privacy-disallowed",
|
|
390
|
+
"privacy.exceptions",
|
|
391
|
+
"privacy.key-bio",
|
|
392
|
+
"privacy.key-chat-invite",
|
|
393
|
+
"privacy.key-forwards",
|
|
394
|
+
"privacy.key-gifts-auto-save",
|
|
395
|
+
"privacy.key-last-seen",
|
|
396
|
+
"privacy.key-profile-photo",
|
|
397
|
+
"privacy.key-saved-music",
|
|
398
|
+
"privacy.set-rules",
|
|
399
|
+
),
|
|
400
|
+
covers_partial=(
|
|
401
|
+
"privacy.key-birthday",
|
|
402
|
+
"privacy.key-calls",
|
|
403
|
+
"privacy.key-no-paid-messages",
|
|
404
|
+
"privacy.key-phone-number",
|
|
405
|
+
"privacy.key-voice-messages",
|
|
406
|
+
),
|
|
407
|
+
coverage_note="Reading any of these keys back is `privacy get`.",
|
|
408
|
+
tags=frozenset({"visible-to-others"}),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
# privacy global get / set
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _global_model(raw: Any) -> GlobalPrivacy:
|
|
418
|
+
gifts = getattr(raw, "disallowed_gifts", None)
|
|
419
|
+
return GlobalPrivacy(
|
|
420
|
+
hide_read_marks=getattr(raw, "hide_read_marks", None),
|
|
421
|
+
archive_and_mute_new_noncontact_peers=getattr(
|
|
422
|
+
raw, "archive_and_mute_new_noncontact_peers", None
|
|
423
|
+
),
|
|
424
|
+
new_noncontact_peers_require_premium=getattr(
|
|
425
|
+
raw, "new_noncontact_peers_require_premium", None
|
|
426
|
+
),
|
|
427
|
+
noncontact_peers_paid_stars=getattr(raw, "noncontact_peers_paid_stars", None),
|
|
428
|
+
keep_archived_unmuted=getattr(raw, "keep_archived_unmuted", None),
|
|
429
|
+
keep_archived_folders=getattr(raw, "keep_archived_folders", None),
|
|
430
|
+
display_gifts_button=getattr(raw, "display_gifts_button", None),
|
|
431
|
+
disallowed_gifts=sorted(
|
|
432
|
+
word for word, field in GIFT_KINDS.items() if getattr(gifts, field, False)
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class GlobalGetReq(Request):
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
async def global_get(ctx: OpContext, req: GlobalGetReq) -> GlobalPrivacy:
|
|
442
|
+
"""The account-wide privacy switches: read marks, archiving, paid messages."""
|
|
443
|
+
from telethon.tl.functions import account as fn
|
|
444
|
+
|
|
445
|
+
return _global_model(await client(ctx)(fn.GetGlobalPrivacySettingsRequest()))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
SPEC_GLOBAL_GET = OperationSpec(
|
|
449
|
+
id="privacy.global.get",
|
|
450
|
+
request=GlobalGetReq,
|
|
451
|
+
response=GlobalPrivacy,
|
|
452
|
+
impl=global_get,
|
|
453
|
+
summary="Read the global privacy settings (read time, archiving, paid messages, gifts)",
|
|
454
|
+
idempotent=True,
|
|
455
|
+
columns=(
|
|
456
|
+
"hide_read_marks",
|
|
457
|
+
"archive_and_mute_new_noncontact_peers",
|
|
458
|
+
"new_noncontact_peers_require_premium",
|
|
459
|
+
"noncontact_peers_paid_stars",
|
|
460
|
+
),
|
|
461
|
+
headers=("Hide read marks", "Archive strangers", "Premium only", "Stars/message"),
|
|
462
|
+
example={"hide_read_marks": False, "new_noncontact_peers_require_premium": True},
|
|
463
|
+
example_args="privacy global get",
|
|
464
|
+
covers=(
|
|
465
|
+
"contacts-users.privacy-global",
|
|
466
|
+
"privacy.global-disallowed-gifts",
|
|
467
|
+
"privacy.global-keep-archived-unmuted",
|
|
468
|
+
"privacy.global-require-premium-to-message",
|
|
469
|
+
),
|
|
470
|
+
covers_partial=(
|
|
471
|
+
"privacy.global-archive-new-noncontacts",
|
|
472
|
+
"privacy.global-display-gifts-button",
|
|
473
|
+
"privacy.global-hide-read-marks",
|
|
474
|
+
"privacy.global-keep-archived-folders",
|
|
475
|
+
"privacy.global-paid-messages-price",
|
|
476
|
+
),
|
|
477
|
+
coverage_note="Writing any of these switches is `privacy global set`.",
|
|
478
|
+
tags=frozenset({"agent-safe"}),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class GlobalSetReq(Request):
|
|
483
|
+
hide_read_marks: Annotated[
|
|
484
|
+
str | None, opt("--hide-read-marks", metavar="ON|OFF", help="Hide when I read messages.")
|
|
485
|
+
] = None
|
|
486
|
+
archive_new_noncontacts: Annotated[
|
|
487
|
+
str | None,
|
|
488
|
+
opt("--archive-new-noncontacts", metavar="ON|OFF", help="Archive+mute unknown senders."),
|
|
489
|
+
] = None
|
|
490
|
+
require_premium_to_message: Annotated[
|
|
491
|
+
str | None,
|
|
492
|
+
opt("--require-premium-to-message", metavar="ON|OFF", help="Premium non-contacts only."),
|
|
493
|
+
] = None
|
|
494
|
+
paid_messages_price: Annotated[
|
|
495
|
+
int | None,
|
|
496
|
+
opt("--paid-messages-price", metavar="STARS", help="Stars per message; 0 turns it off."),
|
|
497
|
+
] = None
|
|
498
|
+
keep_archived_unmuted: Annotated[
|
|
499
|
+
str | None,
|
|
500
|
+
opt("--keep-archived-unmuted", metavar="ON|OFF", help="Unmuted archived chats stay put."),
|
|
501
|
+
] = None
|
|
502
|
+
keep_archived_folders: Annotated[
|
|
503
|
+
str | None,
|
|
504
|
+
opt("--keep-archived-folders", metavar="ON|OFF", help="Folder chats stay archived."),
|
|
505
|
+
] = None
|
|
506
|
+
display_gifts_button: Annotated[
|
|
507
|
+
str | None,
|
|
508
|
+
opt("--display-gifts-button", metavar="ON|OFF", help="Gift button in private chats."),
|
|
509
|
+
] = None
|
|
510
|
+
disallow_gifts: Annotated[
|
|
511
|
+
str | None, opt("--disallow-gifts", metavar="LIST", help="Gift categories to refuse.")
|
|
512
|
+
] = None
|
|
513
|
+
allow_gifts: Annotated[
|
|
514
|
+
str | None, opt("--allow-gifts", metavar="LIST", help="Gift categories to accept again.")
|
|
515
|
+
] = None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
async def global_set(ctx: OpContext, req: GlobalSetReq) -> GlobalPrivacy:
|
|
519
|
+
"""Change one global privacy switch; tlgr does the read-modify-write.
|
|
520
|
+
|
|
521
|
+
`account.setGlobalPrivacySettings` replaces the whole constructor, so a
|
|
522
|
+
flag nobody passed would be written back as `false` unless the current
|
|
523
|
+
value is fetched first. It is, every time.
|
|
524
|
+
"""
|
|
525
|
+
from telethon.tl import types
|
|
526
|
+
from telethon.tl.functions import account as fn
|
|
527
|
+
|
|
528
|
+
handle = client(ctx)
|
|
529
|
+
current = await handle(fn.GetGlobalPrivacySettingsRequest())
|
|
530
|
+
before = _global_model(current)
|
|
531
|
+
|
|
532
|
+
values: dict[str, Any] = {
|
|
533
|
+
"hide_read_marks": before.hide_read_marks,
|
|
534
|
+
"archive_and_mute_new_noncontact_peers": before.archive_and_mute_new_noncontact_peers,
|
|
535
|
+
"new_noncontact_peers_require_premium": before.new_noncontact_peers_require_premium,
|
|
536
|
+
"keep_archived_unmuted": before.keep_archived_unmuted,
|
|
537
|
+
"keep_archived_folders": before.keep_archived_folders,
|
|
538
|
+
"display_gifts_button": before.display_gifts_button,
|
|
539
|
+
}
|
|
540
|
+
changed: list[str] = []
|
|
541
|
+
for flag, field in (
|
|
542
|
+
("hide_read_marks", "hide_read_marks"),
|
|
543
|
+
("archive_new_noncontacts", "archive_and_mute_new_noncontact_peers"),
|
|
544
|
+
("require_premium_to_message", "new_noncontact_peers_require_premium"),
|
|
545
|
+
("keep_archived_unmuted", "keep_archived_unmuted"),
|
|
546
|
+
("keep_archived_folders", "keep_archived_folders"),
|
|
547
|
+
("display_gifts_button", "display_gifts_button"),
|
|
548
|
+
):
|
|
549
|
+
value = _settings.on_off(getattr(req, flag), field=flag)
|
|
550
|
+
if value is not None:
|
|
551
|
+
values[field] = value
|
|
552
|
+
changed.append(field)
|
|
553
|
+
|
|
554
|
+
stars = before.noncontact_peers_paid_stars
|
|
555
|
+
if req.paid_messages_price is not None:
|
|
556
|
+
if req.paid_messages_price < 0:
|
|
557
|
+
raise UsageError(
|
|
558
|
+
"--paid-messages-price cannot be negative", field="paid_messages_price"
|
|
559
|
+
)
|
|
560
|
+
stars = req.paid_messages_price or None
|
|
561
|
+
changed.append("noncontact_peers_paid_stars")
|
|
562
|
+
|
|
563
|
+
kinds = set(before.disallowed_gifts)
|
|
564
|
+
for text, add in ((req.disallow_gifts, True), (req.allow_gifts, False)):
|
|
565
|
+
for word in (part.strip().lower() for part in (text or "").split(",") if part.strip()):
|
|
566
|
+
if word not in GIFT_KINDS:
|
|
567
|
+
raise UsageError(
|
|
568
|
+
f"unknown gift category {word!r}; one of: {' '.join(sorted(GIFT_KINDS))}",
|
|
569
|
+
field="disallow_gifts",
|
|
570
|
+
)
|
|
571
|
+
kinds.add(word) if add else kinds.discard(word)
|
|
572
|
+
changed.append("disallowed_gifts")
|
|
573
|
+
|
|
574
|
+
if not changed:
|
|
575
|
+
raise UsageError("nothing to change: pass at least one flag", field="hide_read_marks")
|
|
576
|
+
|
|
577
|
+
gifts = (
|
|
578
|
+
types.DisallowedGiftsSettings(**{GIFT_KINDS[word]: True for word in sorted(kinds)})
|
|
579
|
+
if kinds
|
|
580
|
+
else None
|
|
581
|
+
)
|
|
582
|
+
answer = await handle(
|
|
583
|
+
fn.SetGlobalPrivacySettingsRequest(
|
|
584
|
+
settings=types.GlobalPrivacySettings(
|
|
585
|
+
**{k: v or None for k, v in values.items()},
|
|
586
|
+
noncontact_peers_paid_stars=stars,
|
|
587
|
+
disallowed_gifts=gifts,
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
ctx.emit("privacy_global", {"changed": sorted(set(changed))})
|
|
592
|
+
result = _global_model(answer)
|
|
593
|
+
result.changed = sorted(set(changed))
|
|
594
|
+
return result
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
SPEC_GLOBAL_SET = OperationSpec(
|
|
598
|
+
id="privacy.global.set",
|
|
599
|
+
request=GlobalSetReq,
|
|
600
|
+
response=GlobalPrivacy,
|
|
601
|
+
impl=global_set,
|
|
602
|
+
summary="Change global privacy settings (one flag at a time; tlgr read-modify-writes)",
|
|
603
|
+
mutating=True,
|
|
604
|
+
idempotent=True,
|
|
605
|
+
rate_class="send",
|
|
606
|
+
columns=("hide_read_marks", "new_noncontact_peers_require_premium", "changed"),
|
|
607
|
+
headers=("Hide read marks", "Premium only", "Changed"),
|
|
608
|
+
example={"hide_read_marks": True, "changed": ["hide_read_marks"]},
|
|
609
|
+
example_args="privacy global set --hide-read-marks on",
|
|
610
|
+
covers=(
|
|
611
|
+
"privacy.global-archive-new-noncontacts",
|
|
612
|
+
"privacy.global-display-gifts-button",
|
|
613
|
+
"privacy.global-hide-read-marks",
|
|
614
|
+
"privacy.global-keep-archived-folders",
|
|
615
|
+
"privacy.global-paid-messages-price",
|
|
616
|
+
),
|
|
617
|
+
covers_partial=(
|
|
618
|
+
"privacy.global-disallowed-gifts",
|
|
619
|
+
"privacy.global-keep-archived-unmuted",
|
|
620
|
+
"privacy.global-require-premium-to-message",
|
|
621
|
+
),
|
|
622
|
+
coverage_note="Reading them back is `privacy global get`.",
|
|
623
|
+
tags=frozenset({"visible-to-others"}),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# ---------------------------------------------------------------------------
|
|
628
|
+
# privacy blocked list / set
|
|
629
|
+
# ---------------------------------------------------------------------------
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class BlockedListReq(Request):
|
|
633
|
+
stories: Annotated[
|
|
634
|
+
bool, opt("--stories", help="The 'blocked from my stories' list instead.")
|
|
635
|
+
] = False
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
async def blocked_list(ctx: OpContext, req: BlockedListReq) -> Page[BlockedPeer]:
|
|
639
|
+
"""Blocked users and bots, or the separate story blocklist.
|
|
640
|
+
|
|
641
|
+
The same list `contact blocked list` answers with — reached from the
|
|
642
|
+
Privacy screen, which is where the GUI puts it, rather than from the
|
|
643
|
+
address book.
|
|
644
|
+
"""
|
|
645
|
+
from tlgr.ops.contact import BlockedListReq as ContactBlockedListReq
|
|
646
|
+
from tlgr.ops.contact import blocked_list as contact_blocked_list
|
|
647
|
+
|
|
648
|
+
return await contact_blocked_list(ctx, ContactBlockedListReq(stories=req.stories))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
SPEC_BLOCKED_LIST = OperationSpec(
|
|
652
|
+
id="privacy.blocked.list",
|
|
653
|
+
request=BlockedListReq,
|
|
654
|
+
response=Page[BlockedPeer],
|
|
655
|
+
impl=blocked_list,
|
|
656
|
+
summary="List blocked users (and the separate story-only block list)",
|
|
657
|
+
aliases=("block.list",),
|
|
658
|
+
paginated=PageKind.PARTICIPANTS,
|
|
659
|
+
idempotent=True,
|
|
660
|
+
columns=("peer.id", "peer.title", "date", "kind"),
|
|
661
|
+
headers=("Id", "Peer", "Blocked", "List"),
|
|
662
|
+
example={
|
|
663
|
+
"items": [{"peer": {"id": 777123, "raw_id": 777123, "kind": "user"}, "kind": "main"}],
|
|
664
|
+
"has_more": False,
|
|
665
|
+
},
|
|
666
|
+
example_args="privacy blocked list",
|
|
667
|
+
covers_partial=("privacy.blocked-list",),
|
|
668
|
+
coverage_note="The write half is `privacy blocked set`; `contact blocked list` is the same list.",
|
|
669
|
+
tags=frozenset({"agent-safe"}),
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class BlockedSetReq(Request):
|
|
674
|
+
peer: Annotated[
|
|
675
|
+
tuple[PeerRef, ...],
|
|
676
|
+
arg(0, metavar="PEER", required=False, variadic=True, kind="peer", help="Who to block."),
|
|
677
|
+
] = ()
|
|
678
|
+
unblock: Annotated[bool, opt("--unblock", help="Remove the block instead of adding it.")] = (
|
|
679
|
+
False
|
|
680
|
+
)
|
|
681
|
+
stories: Annotated[bool, opt("--stories", help="Only block them from seeing my stories.")] = (
|
|
682
|
+
False
|
|
683
|
+
)
|
|
684
|
+
replace_with: Annotated[
|
|
685
|
+
str | None,
|
|
686
|
+
opt("--replace-with", metavar="LIST", help="Replace the whole list at once."),
|
|
687
|
+
] = None
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
async def blocked_set(ctx: OpContext, req: BlockedSetReq) -> BlockedSet:
|
|
691
|
+
"""Block or unblock peers, or replace the whole blocklist.
|
|
692
|
+
|
|
693
|
+
The answer is always the diff — who this call blocked and who it
|
|
694
|
+
unblocked — because `--replace-with` is `contacts.setBlocked`, which
|
|
695
|
+
*replaces* the list: everyone not named is unblocked. One shape for both
|
|
696
|
+
paths means a script never has to branch on which flag it passed.
|
|
697
|
+
"""
|
|
698
|
+
from telethon.tl.functions import contacts as fn
|
|
699
|
+
|
|
700
|
+
if req.replace_with is not None:
|
|
701
|
+
from tlgr.models.peer import parse_peer_ref
|
|
702
|
+
from tlgr.ops.contact import BlockedSetReq as ContactBlockedSetReq
|
|
703
|
+
from tlgr.ops.contact import blocked_set as contact_blocked_set
|
|
704
|
+
|
|
705
|
+
refs = [
|
|
706
|
+
parse_peer_ref(part.strip()) for part in req.replace_with.split(",") if part.strip()
|
|
707
|
+
]
|
|
708
|
+
return await contact_blocked_set(ctx, ContactBlockedSetReq(user=refs, stories=req.stories))
|
|
709
|
+
|
|
710
|
+
if not req.peer:
|
|
711
|
+
raise UsageError("give one or more peers, or --replace-with", field="peer")
|
|
712
|
+
|
|
713
|
+
handle = client(ctx)
|
|
714
|
+
marked: list[int] = []
|
|
715
|
+
for ref in req.peer:
|
|
716
|
+
peer = await _settings.resolve(ctx, ref)
|
|
717
|
+
request = fn.UnblockRequest if req.unblock else fn.BlockRequest
|
|
718
|
+
await handle(request(id=peer, my_stories_from=req.stories or None))
|
|
719
|
+
marked.append(_settings.peer_of(peer))
|
|
720
|
+
ctx.emit("privacy_blocked", {"peer_ids": marked, "unblock": req.unblock})
|
|
721
|
+
return BlockedSet(
|
|
722
|
+
count=len(marked),
|
|
723
|
+
blocked=[] if req.unblock else marked,
|
|
724
|
+
unblocked=marked if req.unblock else [],
|
|
725
|
+
kind="stories" if req.stories else "main",
|
|
726
|
+
applied=True,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
SPEC_BLOCKED_SET = OperationSpec(
|
|
731
|
+
id="privacy.blocked.set",
|
|
732
|
+
request=BlockedSetReq,
|
|
733
|
+
response=BlockedSet,
|
|
734
|
+
impl=blocked_set,
|
|
735
|
+
summary="Block or unblock a user or bot (also the story-only block list)",
|
|
736
|
+
description=(
|
|
737
|
+
"The same operation as `user block` / `user unblock`, reached from "
|
|
738
|
+
"the Privacy screen. `--replace-with` is the bulk form and answers "
|
|
739
|
+
"with the diff it applied."
|
|
740
|
+
),
|
|
741
|
+
aliases=("block.set",),
|
|
742
|
+
mutating=True,
|
|
743
|
+
rate_class="send",
|
|
744
|
+
columns=("count", "blocked", "unblocked", "kind"),
|
|
745
|
+
headers=("Peers", "Blocked", "Unblocked", "List"),
|
|
746
|
+
example={"count": 1, "blocked": [777123], "unblocked": [], "kind": "main", "applied": True},
|
|
747
|
+
example_args="privacy blocked set @spammer",
|
|
748
|
+
covers=("privacy.blocked-list",),
|
|
749
|
+
tags=frozenset({"visible-to-others"}),
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
# ---------------------------------------------------------------------------
|
|
754
|
+
# privacy revenue get
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class RevenueGetReq(Request):
|
|
759
|
+
user: Annotated[PeerRef, arg(0, metavar="USER", kind="user", help="Who paid me.")]
|
|
760
|
+
parent_peer: Annotated[
|
|
761
|
+
str | None,
|
|
762
|
+
opt("--parent-peer", metavar="CHAT", help="Business or channel parent peer."),
|
|
763
|
+
] = None
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
async def revenue_get(ctx: OpContext, req: RevenueGetReq) -> PaidMessageRevenue:
|
|
767
|
+
"""Stars earned from paid messages one user sent me."""
|
|
768
|
+
from telethon.tl.functions import account as fn
|
|
769
|
+
|
|
770
|
+
target = await _settings.input_user(ctx, req.user)
|
|
771
|
+
parent = await _settings.resolve(ctx, req.parent_peer) if req.parent_peer else None
|
|
772
|
+
result = await client(ctx)(fn.GetPaidMessagesRevenueRequest(user_id=target, parent_peer=parent))
|
|
773
|
+
return PaidMessageRevenue(
|
|
774
|
+
user_id=_settings.peer_of(await _settings.resolve(ctx, req.user)),
|
|
775
|
+
stars_amount=int(getattr(result, "stars_amount", 0) or 0),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
SPEC_REVENUE_GET = OperationSpec(
|
|
780
|
+
id="privacy.revenue.get",
|
|
781
|
+
request=RevenueGetReq,
|
|
782
|
+
response=PaidMessageRevenue,
|
|
783
|
+
impl=revenue_get,
|
|
784
|
+
summary="Stars earned from paid messages sent by a user",
|
|
785
|
+
idempotent=True,
|
|
786
|
+
columns=("user_id", "stars_amount"),
|
|
787
|
+
headers=("User", "Stars"),
|
|
788
|
+
example={"user_id": 777123, "stars_amount": 25},
|
|
789
|
+
example_args="privacy revenue get @alice",
|
|
790
|
+
covers=("privacy.paid-message-revenue",),
|
|
791
|
+
tags=frozenset({"agent-safe"}),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
__all__ = [name for name in dir() if name.startswith("SPEC_")]
|