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/_story.py
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
"""The story plumbing: items, audiences, overlays and feed state.
|
|
2
|
+
|
|
3
|
+
Four things in this group are genuinely fiddly, and every one of them is here
|
|
4
|
+
rather than in `ops/story.py` so that `post`, `edit` and `get` cannot disagree
|
|
5
|
+
about them.
|
|
6
|
+
|
|
7
|
+
* **Privacy rules are an ordered vector**, not a value. Telegram applies
|
|
8
|
+
`[base, allow…, disallow…]` in order, so building it in one place is what
|
|
9
|
+
makes "contacts, except Bob" mean the same thing on `story post` and
|
|
10
|
+
`story edit`.
|
|
11
|
+
* **Media areas round-trip.** `story get --areas-out` writes exactly the JSON
|
|
12
|
+
`--areas` reads back, which means the model → TL direction has to rebuild
|
|
13
|
+
every variant the TL → model direction can produce.
|
|
14
|
+
* **Story items come in three shapes** for one id (`storyItem`,
|
|
15
|
+
`storyItemSkipped`, `storyItemDeleted`), and a caller must be able to tell
|
|
16
|
+
a placeholder from a story with no caption.
|
|
17
|
+
* **The feed is not offset-paginated.** `stories.getAllStories` takes an
|
|
18
|
+
opaque `state` plus a `next` flag, so the cursor carries both and a naive
|
|
19
|
+
offset cursor would silently restart the walk.
|
|
20
|
+
|
|
21
|
+
Telethon is imported inside functions, never at module scope (§2.2).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from tlgr.core.errors import UsageError
|
|
31
|
+
from tlgr.core.timefmt import fmt_dt, to_unix
|
|
32
|
+
from tlgr.models.peer import PeerRef, parse_peer_ref
|
|
33
|
+
from tlgr.models.story import (
|
|
34
|
+
MediaArea,
|
|
35
|
+
StealthMode,
|
|
36
|
+
Story,
|
|
37
|
+
StoryAlbum,
|
|
38
|
+
StoryFwdHeader,
|
|
39
|
+
StoryPrivacy,
|
|
40
|
+
StoryViews,
|
|
41
|
+
)
|
|
42
|
+
from tlgr.ops._common import client
|
|
43
|
+
from tlgr.ops._serialize import entity_to_peer, media_summary, message_entities, peer_id_of
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"album_model",
|
|
47
|
+
"areas_from_json",
|
|
48
|
+
"build_areas",
|
|
49
|
+
"media_area_model",
|
|
50
|
+
"privacy_model",
|
|
51
|
+
"privacy_rules",
|
|
52
|
+
"stealth_model",
|
|
53
|
+
"story_ids",
|
|
54
|
+
"story_model",
|
|
55
|
+
"views_model",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
#: `--period` → the `period` seconds `stories.sendStory` wants.
|
|
59
|
+
PERIODS: dict[str, int] = {"6h": 6 * 3600, "12h": 12 * 3600, "24h": 86400, "48h": 48 * 3600}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def story_ids(values: Any) -> list[int]:
|
|
63
|
+
"""Story ids from the CLI, accepting `10-14` ranges like message ids do."""
|
|
64
|
+
from tlgr.ops._common import ids
|
|
65
|
+
|
|
66
|
+
return ids(tuple(str(v) for v in (values or ())))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Privacy
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
_BASE_RULES = ("everyone", "contacts", "close-friends", "selected")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _split_target(text: str) -> tuple[str, str]:
|
|
77
|
+
"""`chat:@team` → `("chat", "@team")`; anything else is a user."""
|
|
78
|
+
if text.startswith("chat:"):
|
|
79
|
+
return "chat", text[len("chat:") :]
|
|
80
|
+
return "user", text
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def privacy_rules(
|
|
84
|
+
ctx: Any,
|
|
85
|
+
*,
|
|
86
|
+
base: str,
|
|
87
|
+
allow: list[str] | tuple[str, ...] = (),
|
|
88
|
+
exclude: list[str] | tuple[str, ...] = (),
|
|
89
|
+
preset: str | None = None,
|
|
90
|
+
) -> list[Any]:
|
|
91
|
+
"""The `InputPrivacyRule` vector for an audience, in server order.
|
|
92
|
+
|
|
93
|
+
Order is load-bearing: Telegram evaluates the vector front to back, so the
|
|
94
|
+
base rule has to come first and the disallow entries last. Emitting them
|
|
95
|
+
in the order the flags happened to be typed is how "contacts, except Bob"
|
|
96
|
+
becomes "everyone".
|
|
97
|
+
"""
|
|
98
|
+
from telethon.tl import types
|
|
99
|
+
|
|
100
|
+
from tlgr.ops import _send
|
|
101
|
+
|
|
102
|
+
if preset:
|
|
103
|
+
base, allow, exclude = _preset(ctx, preset, base, list(allow), list(exclude))
|
|
104
|
+
|
|
105
|
+
if base not in _BASE_RULES:
|
|
106
|
+
raise UsageError(
|
|
107
|
+
f"--privacy: expected one of {', '.join(_BASE_RULES)}, not {base!r}", field="privacy"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
rules: list[Any] = []
|
|
111
|
+
if base == "everyone":
|
|
112
|
+
rules.append(types.InputPrivacyValueAllowAll())
|
|
113
|
+
elif base == "contacts":
|
|
114
|
+
rules.append(types.InputPrivacyValueAllowContacts())
|
|
115
|
+
elif base == "close-friends":
|
|
116
|
+
rules.append(types.InputPrivacyValueAllowCloseFriends())
|
|
117
|
+
|
|
118
|
+
async def targets(values: Any) -> tuple[list[Any], list[int]]:
|
|
119
|
+
users: list[Any] = []
|
|
120
|
+
chats: list[int] = []
|
|
121
|
+
for raw in values or ():
|
|
122
|
+
kind, reference = _split_target(str(raw))
|
|
123
|
+
peer = await _send.resolve(ctx, parse_peer_ref(reference))
|
|
124
|
+
if kind == "chat":
|
|
125
|
+
chats.append(abs(_send.peer_id_of(peer)) % 1_000_000_000_000)
|
|
126
|
+
else:
|
|
127
|
+
from telethon import utils
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
users.append(utils.get_input_user(peer))
|
|
131
|
+
except (TypeError, ValueError) as exc:
|
|
132
|
+
raise UsageError(
|
|
133
|
+
f"{reference!r} is not a user; use chat:{reference} for a group",
|
|
134
|
+
field="allow",
|
|
135
|
+
) from exc
|
|
136
|
+
return users, chats
|
|
137
|
+
|
|
138
|
+
allow_users, allow_chats = await targets(allow)
|
|
139
|
+
if allow_users:
|
|
140
|
+
rules.append(types.InputPrivacyValueAllowUsers(users=allow_users))
|
|
141
|
+
if allow_chats:
|
|
142
|
+
rules.append(types.InputPrivacyValueAllowChatParticipants(chats=allow_chats))
|
|
143
|
+
|
|
144
|
+
deny_users, deny_chats = await targets(exclude)
|
|
145
|
+
if deny_users:
|
|
146
|
+
rules.append(types.InputPrivacyValueDisallowUsers(users=deny_users))
|
|
147
|
+
if deny_chats:
|
|
148
|
+
rules.append(types.InputPrivacyValueDisallowChatParticipants(chats=deny_chats))
|
|
149
|
+
|
|
150
|
+
if base == "selected" and not allow_users and not allow_chats:
|
|
151
|
+
raise UsageError(
|
|
152
|
+
"--privacy selected needs at least one --allow, or the story is visible to nobody",
|
|
153
|
+
field="allow",
|
|
154
|
+
)
|
|
155
|
+
return rules
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _preset(
|
|
159
|
+
ctx: Any, name: str, base: str, allow: list[str], exclude: list[str]
|
|
160
|
+
) -> tuple[str, list[str], list[str]]:
|
|
161
|
+
"""A named audience from `[story.privacy_presets]` in the config.
|
|
162
|
+
|
|
163
|
+
Presets exist because the same eight-person allow list is retyped on every
|
|
164
|
+
post otherwise, and a mistyped one is a privacy incident rather than a
|
|
165
|
+
typo.
|
|
166
|
+
"""
|
|
167
|
+
config = getattr(ctx, "config", None)
|
|
168
|
+
table: dict[str, Any] = {}
|
|
169
|
+
if config is not None:
|
|
170
|
+
section = getattr(config, "story", None) or {}
|
|
171
|
+
if isinstance(section, dict):
|
|
172
|
+
table = section.get("privacy_presets") or {}
|
|
173
|
+
else:
|
|
174
|
+
table = getattr(section, "privacy_presets", None) or {}
|
|
175
|
+
preset = table.get(name) if isinstance(table, dict) else None
|
|
176
|
+
if preset is None:
|
|
177
|
+
raise UsageError(
|
|
178
|
+
f"--privacy-preset: no preset named {name!r} in [story.privacy_presets]",
|
|
179
|
+
field="privacy_preset",
|
|
180
|
+
)
|
|
181
|
+
if isinstance(preset, str):
|
|
182
|
+
return preset, allow, exclude
|
|
183
|
+
return (
|
|
184
|
+
str(preset.get("base") or base),
|
|
185
|
+
[*preset.get("allow", []), *allow],
|
|
186
|
+
[*preset.get("exclude", []), *exclude],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def privacy_model(rules: Any) -> StoryPrivacy | None:
|
|
191
|
+
"""A `privacyValue*` vector as the model. Only your own stories carry one."""
|
|
192
|
+
if not rules:
|
|
193
|
+
return None
|
|
194
|
+
out = StoryPrivacy()
|
|
195
|
+
for rule in rules:
|
|
196
|
+
name = type(rule).__name__
|
|
197
|
+
if name == "PrivacyValueAllowAll":
|
|
198
|
+
out.base = "everyone"
|
|
199
|
+
elif name == "PrivacyValueAllowContacts":
|
|
200
|
+
out.base = "contacts"
|
|
201
|
+
elif name == "PrivacyValueAllowCloseFriends":
|
|
202
|
+
out.base = "close-friends"
|
|
203
|
+
elif name == "PrivacyValueAllowUsers":
|
|
204
|
+
out.allow_users.extend(int(u) for u in (getattr(rule, "users", None) or []))
|
|
205
|
+
elif name == "PrivacyValueAllowChatParticipants":
|
|
206
|
+
out.allow_chats.extend(int(c) for c in (getattr(rule, "chats", None) or []))
|
|
207
|
+
elif name == "PrivacyValueDisallowUsers":
|
|
208
|
+
out.disallow_users.extend(int(u) for u in (getattr(rule, "users", None) or []))
|
|
209
|
+
elif name == "PrivacyValueDisallowChatParticipants":
|
|
210
|
+
out.disallow_chats.extend(int(c) for c in (getattr(rule, "chats", None) or []))
|
|
211
|
+
# No base rule came back, so the audience *is* the allow list.
|
|
212
|
+
has_base = any(
|
|
213
|
+
type(rule).__name__
|
|
214
|
+
in ("PrivacyValueAllowAll", "PrivacyValueAllowContacts", "PrivacyValueAllowCloseFriends")
|
|
215
|
+
for rule in rules
|
|
216
|
+
)
|
|
217
|
+
if not has_base and (out.allow_users or out.allow_chats):
|
|
218
|
+
out.base = "selected"
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# Media areas
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def media_area_model(raw: Any) -> MediaArea:
|
|
228
|
+
"""One TL media area as the flat, round-trippable model."""
|
|
229
|
+
coordinates = getattr(raw, "coordinates", None)
|
|
230
|
+
area = MediaArea(
|
|
231
|
+
x=float(getattr(coordinates, "x", 0.0) or 0.0),
|
|
232
|
+
y=float(getattr(coordinates, "y", 0.0) or 0.0),
|
|
233
|
+
w=float(getattr(coordinates, "w", 0.0) or 0.0),
|
|
234
|
+
h=float(getattr(coordinates, "h", 0.0) or 0.0),
|
|
235
|
+
rotation=float(getattr(coordinates, "rotation", 0.0) or 0.0),
|
|
236
|
+
radius=getattr(coordinates, "radius", None),
|
|
237
|
+
)
|
|
238
|
+
name = type(raw).__name__
|
|
239
|
+
geo = getattr(raw, "geo", None)
|
|
240
|
+
if geo is not None:
|
|
241
|
+
area.latitude = getattr(geo, "lat", None)
|
|
242
|
+
area.longitude = getattr(geo, "long", None)
|
|
243
|
+
if name == "MediaAreaGeoPoint":
|
|
244
|
+
area.type = "geo"
|
|
245
|
+
address = getattr(raw, "address", None)
|
|
246
|
+
if address is not None:
|
|
247
|
+
area.address = {
|
|
248
|
+
key: str(value)
|
|
249
|
+
for key, value in (
|
|
250
|
+
("country_iso2", getattr(address, "country_iso2", None)),
|
|
251
|
+
("state", getattr(address, "state", None)),
|
|
252
|
+
("city", getattr(address, "city", None)),
|
|
253
|
+
("street", getattr(address, "street", None)),
|
|
254
|
+
)
|
|
255
|
+
if value
|
|
256
|
+
}
|
|
257
|
+
elif name == "MediaAreaVenue":
|
|
258
|
+
area.type = "venue"
|
|
259
|
+
area.title = getattr(raw, "title", None)
|
|
260
|
+
area.address_text = getattr(raw, "address", None)
|
|
261
|
+
area.provider = getattr(raw, "provider", None)
|
|
262
|
+
area.venue_id = getattr(raw, "venue_id", None)
|
|
263
|
+
area.venue_type = getattr(raw, "venue_type", None)
|
|
264
|
+
elif name == "MediaAreaSuggestedReaction":
|
|
265
|
+
from tlgr.ops.reaction import name_of
|
|
266
|
+
|
|
267
|
+
area.type = "reaction"
|
|
268
|
+
area.reaction = name_of(getattr(raw, "reaction", None))
|
|
269
|
+
area.dark = bool(getattr(raw, "dark", False))
|
|
270
|
+
area.flipped = bool(getattr(raw, "flipped", False))
|
|
271
|
+
elif name == "MediaAreaChannelPost":
|
|
272
|
+
from tlgr.ops._serialize import marked_id
|
|
273
|
+
|
|
274
|
+
area.type = "channel_post"
|
|
275
|
+
area.chat_id = marked_id(int(getattr(raw, "channel_id", 0) or 0), "channel")
|
|
276
|
+
area.msg_id = getattr(raw, "msg_id", None)
|
|
277
|
+
elif name == "MediaAreaUrl":
|
|
278
|
+
area.type = "url"
|
|
279
|
+
area.url = getattr(raw, "url", None)
|
|
280
|
+
elif name == "MediaAreaWeather":
|
|
281
|
+
area.type = "weather"
|
|
282
|
+
area.emoji = getattr(raw, "emoji", None)
|
|
283
|
+
area.temperature_c = getattr(raw, "temperature_c", None)
|
|
284
|
+
area.color = getattr(raw, "color", None)
|
|
285
|
+
elif name == "MediaAreaStarGift":
|
|
286
|
+
area.type = "star_gift"
|
|
287
|
+
area.slug = getattr(raw, "slug", None)
|
|
288
|
+
return area
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _coordinates(area: MediaArea) -> Any:
|
|
292
|
+
from telethon.tl import types
|
|
293
|
+
|
|
294
|
+
return types.MediaAreaCoordinates(
|
|
295
|
+
x=area.x, y=area.y, w=area.w, h=area.h, rotation=area.rotation, radius=area.radius
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def _area_to_tl(ctx: Any, area: MediaArea) -> Any:
|
|
300
|
+
"""The model back into a TL media area, resolving what has to be resolved."""
|
|
301
|
+
from telethon.tl import types
|
|
302
|
+
|
|
303
|
+
from tlgr.ops import _send
|
|
304
|
+
|
|
305
|
+
coordinates = _coordinates(area)
|
|
306
|
+
if area.type == "geo":
|
|
307
|
+
address = None
|
|
308
|
+
if area.address:
|
|
309
|
+
address = types.GeoPointAddress(
|
|
310
|
+
country_iso2=str(area.address.get("country_iso2", "")),
|
|
311
|
+
state=area.address.get("state"),
|
|
312
|
+
city=area.address.get("city"),
|
|
313
|
+
street=area.address.get("street"),
|
|
314
|
+
)
|
|
315
|
+
return types.MediaAreaGeoPoint(
|
|
316
|
+
coordinates=coordinates,
|
|
317
|
+
geo=types.GeoPoint(
|
|
318
|
+
long=float(area.longitude or 0.0), lat=float(area.latitude or 0.0), access_hash=0
|
|
319
|
+
),
|
|
320
|
+
address=address,
|
|
321
|
+
)
|
|
322
|
+
if area.type == "venue":
|
|
323
|
+
return types.MediaAreaVenue(
|
|
324
|
+
coordinates=coordinates,
|
|
325
|
+
geo=types.GeoPoint(
|
|
326
|
+
long=float(area.longitude or 0.0), lat=float(area.latitude or 0.0), access_hash=0
|
|
327
|
+
),
|
|
328
|
+
title=str(area.title or ""),
|
|
329
|
+
address=str(area.address_text or ""),
|
|
330
|
+
provider=str(area.provider or ""),
|
|
331
|
+
venue_id=str(area.venue_id or ""),
|
|
332
|
+
venue_type=str(area.venue_type or ""),
|
|
333
|
+
)
|
|
334
|
+
if area.type == "reaction":
|
|
335
|
+
from tlgr.ops.reaction import to_tl
|
|
336
|
+
|
|
337
|
+
return types.MediaAreaSuggestedReaction(
|
|
338
|
+
coordinates=coordinates,
|
|
339
|
+
reaction=to_tl(str(area.reaction or "")),
|
|
340
|
+
dark=area.dark or None,
|
|
341
|
+
flipped=area.flipped or None,
|
|
342
|
+
)
|
|
343
|
+
if area.type == "channel_post":
|
|
344
|
+
from tlgr.ops._common import input_channel
|
|
345
|
+
|
|
346
|
+
peer = await _send.resolve(ctx, parse_peer_ref(str(area.chat_id)))
|
|
347
|
+
return types.InputMediaAreaChannelPost(
|
|
348
|
+
coordinates=coordinates, channel=input_channel(peer), msg_id=int(area.msg_id or 0)
|
|
349
|
+
)
|
|
350
|
+
if area.type == "url":
|
|
351
|
+
return types.MediaAreaUrl(coordinates=coordinates, url=str(area.url or ""))
|
|
352
|
+
if area.type == "weather":
|
|
353
|
+
return types.MediaAreaWeather(
|
|
354
|
+
coordinates=coordinates,
|
|
355
|
+
emoji=str(area.emoji or ""),
|
|
356
|
+
temperature_c=float(area.temperature_c or 0.0),
|
|
357
|
+
color=int(area.color or 0),
|
|
358
|
+
)
|
|
359
|
+
if area.type == "star_gift":
|
|
360
|
+
return types.MediaAreaStarGift(coordinates=coordinates, slug=str(area.slug or ""))
|
|
361
|
+
raise UsageError(f"unknown media area type {area.type!r}", field="areas")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _rect(text: str, flag: str) -> tuple[float, float, float, float, float, float | None]:
|
|
365
|
+
"""`X,Y,W,H[,ROT[,RADIUS]]` as floats. Percentages of the media, not pixels."""
|
|
366
|
+
parts = [p.strip() for p in text.split(",") if p.strip() != ""]
|
|
367
|
+
if len(parts) < 4:
|
|
368
|
+
raise UsageError(f"{flag}: the position must be X,Y,W,H", field=flag.lstrip("-"))
|
|
369
|
+
try:
|
|
370
|
+
numbers = [float(p) for p in parts]
|
|
371
|
+
except ValueError as exc:
|
|
372
|
+
raise UsageError(
|
|
373
|
+
f"{flag}: {text!r} is not a X,Y,W,H rectangle", field=flag.lstrip("-")
|
|
374
|
+
) from exc
|
|
375
|
+
x, y, w, h = numbers[:4]
|
|
376
|
+
rotation = numbers[4] if len(numbers) > 4 else 0.0
|
|
377
|
+
radius = numbers[5] if len(numbers) > 5 else None
|
|
378
|
+
return x, y, w, h, rotation, radius
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _split_spec(value: str, flag: str) -> tuple[str, str]:
|
|
382
|
+
"""`payload@X,Y,W,H` → `(payload, rect)`; the last `@` wins."""
|
|
383
|
+
payload, sep, rect = value.rpartition("@")
|
|
384
|
+
if not sep:
|
|
385
|
+
raise UsageError(
|
|
386
|
+
f"{flag}: expected PAYLOAD@X,Y,W,H — got {value!r}", field=flag.lstrip("-")
|
|
387
|
+
)
|
|
388
|
+
return payload, rect
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _area(flag: str, value: str, **fields: Any) -> tuple[MediaArea, str]:
|
|
392
|
+
payload, rect = _split_spec(value, flag)
|
|
393
|
+
x, y, w, h, rotation, radius = _rect(rect, flag)
|
|
394
|
+
return MediaArea(x=x, y=y, w=w, h=h, rotation=rotation, radius=radius, **fields), payload
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def build_areas(
|
|
398
|
+
ctx: Any,
|
|
399
|
+
*,
|
|
400
|
+
areas_file: str | None = None,
|
|
401
|
+
geo: tuple[str, ...] = (),
|
|
402
|
+
venue: tuple[str, ...] = (),
|
|
403
|
+
venue_near: str | None = None,
|
|
404
|
+
venue_pick: int = 0,
|
|
405
|
+
url: tuple[str, ...] = (),
|
|
406
|
+
reaction: tuple[str, ...] = (),
|
|
407
|
+
post: tuple[str, ...] = (),
|
|
408
|
+
weather: tuple[str, ...] = (),
|
|
409
|
+
gift: tuple[str, ...] = (),
|
|
410
|
+
) -> tuple[list[Any], list[MediaArea]]:
|
|
411
|
+
"""`(TL areas, the models that describe them)`.
|
|
412
|
+
|
|
413
|
+
`--areas` is the authoritative form because it is what `--areas-out`
|
|
414
|
+
writes; the `--area-*` flags are sugar that produces the same models, so a
|
|
415
|
+
story can be repositioned by editing the JSON rather than by retyping
|
|
416
|
+
every pill.
|
|
417
|
+
"""
|
|
418
|
+
models: list[MediaArea] = []
|
|
419
|
+
|
|
420
|
+
if areas_file:
|
|
421
|
+
models.extend(areas_from_json(areas_file))
|
|
422
|
+
|
|
423
|
+
for value in geo:
|
|
424
|
+
area, payload = _area("--area-geo", value, type="geo")
|
|
425
|
+
parts = [p.strip() for p in payload.split(",")]
|
|
426
|
+
if len(parts) < 2:
|
|
427
|
+
raise UsageError("--area-geo: expected LAT,LON[,ADDR]@X,Y,W,H", field="area_geo")
|
|
428
|
+
area.latitude, area.longitude = float(parts[0]), float(parts[1])
|
|
429
|
+
if len(parts) > 2:
|
|
430
|
+
keys = ("country_iso2", "state", "city", "street")
|
|
431
|
+
area.address = {k: v for k, v in zip(keys, parts[2:], strict=False) if v}
|
|
432
|
+
models.append(area)
|
|
433
|
+
|
|
434
|
+
for value in url:
|
|
435
|
+
area, payload = _area("--area-url", value, type="url")
|
|
436
|
+
area.url = payload
|
|
437
|
+
models.append(area)
|
|
438
|
+
|
|
439
|
+
for value in reaction:
|
|
440
|
+
area, payload = _area("--area-reaction", value, type="reaction")
|
|
441
|
+
emoji, *modifiers = payload.split(":")
|
|
442
|
+
area.reaction = emoji
|
|
443
|
+
area.dark = "dark" in modifiers
|
|
444
|
+
area.flipped = "flipped" in modifiers
|
|
445
|
+
models.append(area)
|
|
446
|
+
|
|
447
|
+
for value in post:
|
|
448
|
+
area, payload = _area("--area-post", value, type="channel_post")
|
|
449
|
+
chat, _, msg_id = payload.rpartition(":")
|
|
450
|
+
if not chat or not msg_id.isdigit():
|
|
451
|
+
raise UsageError("--area-post: expected CHAT:MSG_ID@X,Y,W,H", field="area_post")
|
|
452
|
+
from tlgr.ops import _send
|
|
453
|
+
|
|
454
|
+
peer = await _send.resolve(ctx, parse_peer_ref(chat))
|
|
455
|
+
area.chat_id = _send.peer_id_of(peer)
|
|
456
|
+
area.msg_id = int(msg_id)
|
|
457
|
+
models.append(area)
|
|
458
|
+
|
|
459
|
+
for value in gift:
|
|
460
|
+
area, payload = _area("--area-gift", value, type="star_gift")
|
|
461
|
+
area.slug = payload
|
|
462
|
+
models.append(area)
|
|
463
|
+
|
|
464
|
+
for value in weather:
|
|
465
|
+
area, payload = _area("--area-weather", value, type="weather")
|
|
466
|
+
if payload.strip().lower() == "auto":
|
|
467
|
+
models.append(await _resolve_weather(ctx, area, venue_near))
|
|
468
|
+
continue
|
|
469
|
+
parts = [p.strip() for p in payload.split(",")]
|
|
470
|
+
if len(parts) < 3:
|
|
471
|
+
raise UsageError(
|
|
472
|
+
"--area-weather: expected EMOJI,TEMP_C,#AARRGGBB@X,Y,W,H", field="area_weather"
|
|
473
|
+
)
|
|
474
|
+
area.emoji = parts[0]
|
|
475
|
+
area.temperature_c = float(parts[1])
|
|
476
|
+
area.color = int(parts[2].lstrip("#"), 16)
|
|
477
|
+
models.append(area)
|
|
478
|
+
|
|
479
|
+
for value in venue:
|
|
480
|
+
area, payload = _area("--area-venue", value, type="venue")
|
|
481
|
+
models.append(await _resolve_venue(ctx, area, payload, venue_near, venue_pick))
|
|
482
|
+
|
|
483
|
+
return [await _area_to_tl(ctx, area) for area in models], models
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def areas_from_json(path_or_text: str) -> list[MediaArea]:
|
|
487
|
+
"""Media areas from the JSON `story get --areas-out` writes.
|
|
488
|
+
|
|
489
|
+
A path is read; anything that starts with `[` is taken as inline JSON, so
|
|
490
|
+
a script can pipe the array in without a temp file.
|
|
491
|
+
"""
|
|
492
|
+
import msgspec
|
|
493
|
+
|
|
494
|
+
text = path_or_text.strip()
|
|
495
|
+
if not text.startswith("["):
|
|
496
|
+
try:
|
|
497
|
+
text = Path(text).expanduser().read_text(encoding="utf-8")
|
|
498
|
+
except OSError as exc:
|
|
499
|
+
raise UsageError(f"--areas: {exc.strerror or exc}", field="areas") from exc
|
|
500
|
+
try:
|
|
501
|
+
return msgspec.json.decode(text.encode(), type=list[MediaArea])
|
|
502
|
+
except (msgspec.DecodeError, msgspec.ValidationError, json.JSONDecodeError) as exc:
|
|
503
|
+
raise UsageError(f"--areas: {exc}", field="areas") from exc
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
async def _inline_query(ctx: Any, username: str, query: str, near: str | None) -> Any:
|
|
507
|
+
"""One inline-bot query, which is how venue and weather areas are built.
|
|
508
|
+
|
|
509
|
+
Neither is an API method: the official clients run an inline query against
|
|
510
|
+
a bot named in the server config and use the result's `query_id`, so tlgr
|
|
511
|
+
does the same rather than inventing a venue id the server will reject.
|
|
512
|
+
"""
|
|
513
|
+
from telethon.tl import types
|
|
514
|
+
from telethon.tl.functions import messages as fn
|
|
515
|
+
|
|
516
|
+
point = None
|
|
517
|
+
if near:
|
|
518
|
+
try:
|
|
519
|
+
lat, _, lon = near.partition(",")
|
|
520
|
+
point = types.InputGeoPoint(lat=float(lat), long=float(lon))
|
|
521
|
+
except ValueError as exc:
|
|
522
|
+
raise UsageError(
|
|
523
|
+
"--area-venue-near: expected LAT,LON", field="area_venue_near"
|
|
524
|
+
) from exc
|
|
525
|
+
bot = await client(ctx).get_input_entity(username)
|
|
526
|
+
return await client(ctx)(
|
|
527
|
+
fn.GetInlineBotResultsRequest(
|
|
528
|
+
bot=bot, peer=types.InputPeerSelf(), query=query, offset="", geo_point=point
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
async def _config_username(ctx: Any, field: str) -> str:
|
|
534
|
+
"""The inline bot the server names for venue or weather lookups.
|
|
535
|
+
|
|
536
|
+
Venue search is in `help.getConfig`; the weather bot arrived later and
|
|
537
|
+
lives in `help.getAppConfig`, so both are consulted rather than assuming
|
|
538
|
+
one of them.
|
|
539
|
+
"""
|
|
540
|
+
from telethon.tl.functions import help as help_fn
|
|
541
|
+
|
|
542
|
+
from tlgr.core.errors import NotSupportedError
|
|
543
|
+
from tlgr.ops import _media
|
|
544
|
+
|
|
545
|
+
config = await client(ctx)(help_fn.GetConfigRequest())
|
|
546
|
+
username = getattr(config, field, None)
|
|
547
|
+
if not username:
|
|
548
|
+
username = (await _media.app_config(ctx)).get(field)
|
|
549
|
+
if not username:
|
|
550
|
+
raise NotSupportedError(
|
|
551
|
+
f"this account's server config names no {field}, so there is nowhere to ask"
|
|
552
|
+
)
|
|
553
|
+
return str(username)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
async def _resolve_venue(
|
|
557
|
+
ctx: Any, area: MediaArea, query: str, near: str | None, pick: int
|
|
558
|
+
) -> MediaArea:
|
|
559
|
+
username = await _config_username(ctx, "venue_search_username")
|
|
560
|
+
result = await _inline_query(ctx, username, query, near)
|
|
561
|
+
rows = [
|
|
562
|
+
row
|
|
563
|
+
for row in (getattr(result, "results", None) or [])
|
|
564
|
+
if type(getattr(row, "send_message", None)).__name__ == "BotInlineMessageMediaVenue"
|
|
565
|
+
]
|
|
566
|
+
if not rows:
|
|
567
|
+
raise UsageError(f"--area-venue: no venue matched {query!r}", field="area_venue")
|
|
568
|
+
if pick >= len(rows):
|
|
569
|
+
raise UsageError(
|
|
570
|
+
f"--area-venue-pick {pick}: only {len(rows)} venues matched", field="area_venue_pick"
|
|
571
|
+
)
|
|
572
|
+
message = rows[pick].send_message
|
|
573
|
+
geo = getattr(message, "geo", None)
|
|
574
|
+
area.title = getattr(message, "title", None)
|
|
575
|
+
area.address_text = getattr(message, "address", None)
|
|
576
|
+
area.provider = getattr(message, "provider", None)
|
|
577
|
+
area.venue_id = getattr(message, "venue_id", None)
|
|
578
|
+
area.venue_type = getattr(message, "venue_type", None)
|
|
579
|
+
area.latitude = getattr(geo, "lat", None)
|
|
580
|
+
area.longitude = getattr(geo, "long", None)
|
|
581
|
+
return area
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
async def _resolve_weather(ctx: Any, area: MediaArea, near: str | None) -> MediaArea:
|
|
585
|
+
username = await _config_username(ctx, "weather_search_username")
|
|
586
|
+
result = await _inline_query(ctx, username, "", near)
|
|
587
|
+
rows = list(getattr(result, "results", None) or [])
|
|
588
|
+
if not rows:
|
|
589
|
+
raise UsageError(
|
|
590
|
+
"--area-weather auto: the weather bot returned nothing for that point",
|
|
591
|
+
field="area_weather",
|
|
592
|
+
)
|
|
593
|
+
message = getattr(rows[0], "send_message", None)
|
|
594
|
+
area.emoji = getattr(rows[0], "title", None) or "🌡"
|
|
595
|
+
text = str(getattr(message, "message", "") or getattr(rows[0], "description", "") or "")
|
|
596
|
+
digits = "".join(c for c in text if c.isdigit() or c in "-.")
|
|
597
|
+
area.temperature_c = float(digits) if digits else 0.0
|
|
598
|
+
area.color = 0xFF000000
|
|
599
|
+
return area
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# ---------------------------------------------------------------------------
|
|
603
|
+
# Items
|
|
604
|
+
# ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def views_model(raw: Any) -> StoryViews | None:
|
|
608
|
+
if raw is None:
|
|
609
|
+
return None
|
|
610
|
+
from tlgr.ops.reaction import name_of
|
|
611
|
+
|
|
612
|
+
return StoryViews(
|
|
613
|
+
views_count=int(getattr(raw, "views_count", 0) or 0),
|
|
614
|
+
forwards_count=getattr(raw, "forwards_count", None),
|
|
615
|
+
reactions_count=getattr(raw, "reactions_count", None),
|
|
616
|
+
has_viewers=bool(getattr(raw, "has_viewers", False)),
|
|
617
|
+
recent_viewers=[int(v) for v in (getattr(raw, "recent_viewers", None) or [])],
|
|
618
|
+
reactions={
|
|
619
|
+
name_of(getattr(count, "reaction", None)): int(getattr(count, "count", 0) or 0)
|
|
620
|
+
for count in (getattr(raw, "reactions", None) or [])
|
|
621
|
+
},
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _fwd(raw: Any) -> StoryFwdHeader | None:
|
|
626
|
+
if raw is None:
|
|
627
|
+
return None
|
|
628
|
+
return StoryFwdHeader(
|
|
629
|
+
from_id=peer_id_of(getattr(raw, "from_", None)),
|
|
630
|
+
from_name=getattr(raw, "from_name", None),
|
|
631
|
+
story_id=getattr(raw, "story_id", None),
|
|
632
|
+
modified=bool(getattr(raw, "modified", False)),
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def story_model(raw: Any, *, peer_id: int = 0, peer: Any = None, link: str | None = None) -> Story:
|
|
637
|
+
"""A `storyItem` / `storyItemSkipped` / `storyItemDeleted` as one model."""
|
|
638
|
+
from tlgr.ops.reaction import name_of
|
|
639
|
+
|
|
640
|
+
name = type(raw).__name__
|
|
641
|
+
date = getattr(raw, "date", None)
|
|
642
|
+
expire = getattr(raw, "expire_date", None)
|
|
643
|
+
story = Story(
|
|
644
|
+
id=int(getattr(raw, "id", 0) or 0),
|
|
645
|
+
peer_id=peer_id,
|
|
646
|
+
peer=entity_to_peer(peer) if peer is not None else None,
|
|
647
|
+
date=fmt_dt(date),
|
|
648
|
+
date_unix=to_unix(date),
|
|
649
|
+
expire_date=fmt_dt(expire),
|
|
650
|
+
expire_date_unix=to_unix(expire),
|
|
651
|
+
link=link,
|
|
652
|
+
deleted=name == "StoryItemDeleted",
|
|
653
|
+
skipped=name == "StoryItemSkipped",
|
|
654
|
+
live=bool(getattr(raw, "live", False)),
|
|
655
|
+
close_friends=bool(getattr(raw, "close_friends", False)),
|
|
656
|
+
)
|
|
657
|
+
if story.deleted or story.skipped:
|
|
658
|
+
return story
|
|
659
|
+
|
|
660
|
+
story.caption = str(getattr(raw, "caption", "") or "")
|
|
661
|
+
story.entities = message_entities(raw)
|
|
662
|
+
story.media = media_summary(getattr(raw, "media", None))
|
|
663
|
+
story.media_areas = [media_area_model(a) for a in (getattr(raw, "media_areas", None) or [])]
|
|
664
|
+
story.privacy = privacy_model(getattr(raw, "privacy", None))
|
|
665
|
+
story.public = bool(getattr(raw, "public", False))
|
|
666
|
+
story.contacts = bool(getattr(raw, "contacts", False))
|
|
667
|
+
story.selected_contacts = bool(getattr(raw, "selected_contacts", False))
|
|
668
|
+
story.pinned = bool(getattr(raw, "pinned", False))
|
|
669
|
+
story.noforwards = bool(getattr(raw, "noforwards", False))
|
|
670
|
+
story.edited = bool(getattr(raw, "edited", False))
|
|
671
|
+
story.out = bool(getattr(raw, "out", False))
|
|
672
|
+
story.min = bool(getattr(raw, "min", False))
|
|
673
|
+
story.fwd_from = _fwd(getattr(raw, "fwd_from", None))
|
|
674
|
+
reaction = getattr(raw, "sent_reaction", None)
|
|
675
|
+
story.sent_reaction = name_of(reaction) if reaction is not None else None
|
|
676
|
+
story.albums = [int(a) for a in (getattr(raw, "albums", None) or [])]
|
|
677
|
+
music = getattr(raw, "music", None)
|
|
678
|
+
if music is not None:
|
|
679
|
+
from telethon.tl import types as tl
|
|
680
|
+
|
|
681
|
+
story.music = media_summary(tl.MessageMediaDocument(document=music))
|
|
682
|
+
story.views = views_model(getattr(raw, "views", None))
|
|
683
|
+
return story
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def album_model(raw: Any, *, stories: list[int] | None = None) -> StoryAlbum:
|
|
687
|
+
from tlgr.ops._serialize import photo_summary
|
|
688
|
+
|
|
689
|
+
return StoryAlbum(
|
|
690
|
+
id=int(getattr(raw, "album_id", 0) or 0),
|
|
691
|
+
title=str(getattr(raw, "title", "") or ""),
|
|
692
|
+
icon=photo_summary(getattr(raw, "icon_photo", None)),
|
|
693
|
+
stories=list(stories or []),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def stealth_model(raw: Any, *, past: bool = False, future: bool = False) -> StealthMode:
|
|
698
|
+
import time
|
|
699
|
+
|
|
700
|
+
active = getattr(raw, "active_until_date", None)
|
|
701
|
+
cooldown = getattr(raw, "cooldown_until_date", None)
|
|
702
|
+
active_unix = to_unix(active)
|
|
703
|
+
return StealthMode(
|
|
704
|
+
active_until_date=fmt_dt(active),
|
|
705
|
+
active_until_unix=active_unix,
|
|
706
|
+
cooldown_until_date=fmt_dt(cooldown),
|
|
707
|
+
cooldown_until_unix=to_unix(cooldown),
|
|
708
|
+
past=past,
|
|
709
|
+
future=future,
|
|
710
|
+
active=bool(active_unix and active_unix > int(time.time())),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# ---------------------------------------------------------------------------
|
|
715
|
+
# Peers
|
|
716
|
+
# ---------------------------------------------------------------------------
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
async def resolve_or_self(ctx: Any, ref: PeerRef | None) -> Any:
|
|
720
|
+
"""The peer, or this account. Story commands default to your own stories."""
|
|
721
|
+
from telethon.tl import types
|
|
722
|
+
|
|
723
|
+
from tlgr.ops import _send
|
|
724
|
+
|
|
725
|
+
if ref is None:
|
|
726
|
+
return types.InputPeerSelf()
|
|
727
|
+
return await _send.resolve(ctx, ref)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
async def peer_id_for(ctx: Any, peer: Any) -> int:
|
|
731
|
+
"""The marked id of a resolved peer, resolving `InputPeerSelf` to my own.
|
|
732
|
+
|
|
733
|
+
`utils.get_peer_id` has no answer for `inputPeerSelf` — it is "whoever is
|
|
734
|
+
logged in" — and reporting the story of `peer: 0` would make `story post`
|
|
735
|
+
and `story export` disagree with every other command about what "me" is.
|
|
736
|
+
"""
|
|
737
|
+
from tlgr.ops import _send
|
|
738
|
+
|
|
739
|
+
marked = _send.peer_id_of(peer)
|
|
740
|
+
if marked:
|
|
741
|
+
return marked
|
|
742
|
+
me = await client(ctx).get_me()
|
|
743
|
+
return int(getattr(me, "id", 0) or 0)
|