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.
Files changed (192) hide show
  1. tlgr/__init__.py +3 -0
  2. tlgr/__main__.py +6 -0
  3. tlgr/actions/__init__.py +45 -0
  4. tlgr/actions/forward.py +74 -0
  5. tlgr/actions/reply.py +32 -0
  6. tlgr/cli/__init__.py +259 -0
  7. tlgr/cli/confirm.py +55 -0
  8. tlgr/cli/errors.py +84 -0
  9. tlgr/cli/gen.py +690 -0
  10. tlgr/cli/globals.py +273 -0
  11. tlgr/cli/introspect.py +170 -0
  12. tlgr/cli/params.py +189 -0
  13. tlgr/cli/render.py +418 -0
  14. tlgr/core/__init__.py +0 -0
  15. tlgr/core/accounts.py +384 -0
  16. tlgr/core/config.py +358 -0
  17. tlgr/core/custom_tl.py +170 -0
  18. tlgr/core/errors.py +687 -0
  19. tlgr/core/eventtypes.py +1170 -0
  20. tlgr/core/identity.py +127 -0
  21. tlgr/core/launchd.py +122 -0
  22. tlgr/core/logging.py +194 -0
  23. tlgr/core/media.py +134 -0
  24. tlgr/core/output.py +251 -0
  25. tlgr/core/pagination.py +227 -0
  26. tlgr/core/paths.py +360 -0
  27. tlgr/core/peers.py +427 -0
  28. tlgr/core/process.py +138 -0
  29. tlgr/core/signing.py +38 -0
  30. tlgr/core/systemd.py +96 -0
  31. tlgr/core/telethon_compat.py +295 -0
  32. tlgr/core/text.py +211 -0
  33. tlgr/core/timefmt.py +199 -0
  34. tlgr/core/tl.py +98 -0
  35. tlgr/daemon/__init__.py +0 -0
  36. tlgr/daemon/app.py +869 -0
  37. tlgr/daemon/dispatch.py +446 -0
  38. tlgr/daemon/events.py +723 -0
  39. tlgr/daemon/files.py +431 -0
  40. tlgr/daemon/idle.py +119 -0
  41. tlgr/daemon/jobs.py +68 -0
  42. tlgr/daemon/main.py +161 -0
  43. tlgr/daemon/peercred.py +75 -0
  44. tlgr/daemon/policy.py +113 -0
  45. tlgr/daemon/preauth.py +366 -0
  46. tlgr/daemon/ratelimit.py +391 -0
  47. tlgr/daemon/server.py +24 -0
  48. tlgr/daemon/session.py +648 -0
  49. tlgr/daemon/sessions.py +274 -0
  50. tlgr/daemon/singleton.py +114 -0
  51. tlgr/daemon/stream.py +193 -0
  52. tlgr/daemon/transfers.py +219 -0
  53. tlgr/daemon/webhook.py +390 -0
  54. tlgr/data/catalog_index.json +1 -0
  55. tlgr/data/parity_waivers.toml +90 -0
  56. tlgr/filters/__init__.py +42 -0
  57. tlgr/filters/compose.py +121 -0
  58. tlgr/filters/content.py +85 -0
  59. tlgr/filters/context.py +114 -0
  60. tlgr/filters/message.py +161 -0
  61. tlgr/filters/temporal.py +87 -0
  62. tlgr/filters/user.py +36 -0
  63. tlgr/gateway/__init__.py +1 -0
  64. tlgr/gateway/config.py +161 -0
  65. tlgr/gateway/engine.py +215 -0
  66. tlgr/gateway/event.py +22 -0
  67. tlgr/jobs/__init__.py +0 -0
  68. tlgr/jobs/base.py +81 -0
  69. tlgr/jobs/client.py +37 -0
  70. tlgr/models/__init__.py +1220 -0
  71. tlgr/models/admin.py +744 -0
  72. tlgr/models/auth.py +510 -0
  73. tlgr/models/base.py +81 -0
  74. tlgr/models/bot.py +576 -0
  75. tlgr/models/business.py +265 -0
  76. tlgr/models/call.py +586 -0
  77. tlgr/models/config.py +101 -0
  78. tlgr/models/contact.py +481 -0
  79. tlgr/models/daemon.py +336 -0
  80. tlgr/models/dialog.py +626 -0
  81. tlgr/models/envelope.py +68 -0
  82. tlgr/models/error.py +30 -0
  83. tlgr/models/event.py +79 -0
  84. tlgr/models/export.py +66 -0
  85. tlgr/models/gift.py +275 -0
  86. tlgr/models/inline.py +84 -0
  87. tlgr/models/location.py +115 -0
  88. tlgr/models/media.py +507 -0
  89. tlgr/models/message.py +584 -0
  90. tlgr/models/net.py +232 -0
  91. tlgr/models/notify.py +105 -0
  92. tlgr/models/page.py +32 -0
  93. tlgr/models/payment.py +172 -0
  94. tlgr/models/peer.py +400 -0
  95. tlgr/models/poll.py +119 -0
  96. tlgr/models/premium.py +161 -0
  97. tlgr/models/privacy.py +93 -0
  98. tlgr/models/profile.py +217 -0
  99. tlgr/models/reaction.py +160 -0
  100. tlgr/models/resolve.py +175 -0
  101. tlgr/models/settings.py +103 -0
  102. tlgr/models/stars.py +101 -0
  103. tlgr/models/sticker.py +243 -0
  104. tlgr/models/story.py +467 -0
  105. tlgr/models/sync.py +105 -0
  106. tlgr/models/todo.py +36 -0
  107. tlgr/models/webapp.py +89 -0
  108. tlgr/ops/__init__.py +63 -0
  109. tlgr/ops/_admin.py +313 -0
  110. tlgr/ops/_auth.py +599 -0
  111. tlgr/ops/_bots.py +586 -0
  112. tlgr/ops/_calls.py +535 -0
  113. tlgr/ops/_common.py +160 -0
  114. tlgr/ops/_layer.py +46 -0
  115. tlgr/ops/_media.py +592 -0
  116. tlgr/ops/_params.py +212 -0
  117. tlgr/ops/_rights.py +402 -0
  118. tlgr/ops/_send.py +593 -0
  119. tlgr/ops/_serialize.py +667 -0
  120. tlgr/ops/_settings.py +306 -0
  121. tlgr/ops/_spec.py +167 -0
  122. tlgr/ops/_story.py +743 -0
  123. tlgr/ops/account.py +2604 -0
  124. tlgr/ops/agent.py +937 -0
  125. tlgr/ops/auth.py +1282 -0
  126. tlgr/ops/bot.py +4880 -0
  127. tlgr/ops/business.py +1520 -0
  128. tlgr/ops/call.py +1610 -0
  129. tlgr/ops/chat.py +4025 -0
  130. tlgr/ops/chat_admin.py +929 -0
  131. tlgr/ops/chat_extra.py +1061 -0
  132. tlgr/ops/chat_invite.py +716 -0
  133. tlgr/ops/chat_manage.py +1691 -0
  134. tlgr/ops/chat_member.py +1357 -0
  135. tlgr/ops/chat_stats.py +902 -0
  136. tlgr/ops/chat_topic.py +905 -0
  137. tlgr/ops/conference.py +791 -0
  138. tlgr/ops/config.py +1698 -0
  139. tlgr/ops/contact.py +2330 -0
  140. tlgr/ops/daemon.py +1397 -0
  141. tlgr/ops/draft.py +299 -0
  142. tlgr/ops/emoji.py +343 -0
  143. tlgr/ops/events.py +1327 -0
  144. tlgr/ops/export.py +596 -0
  145. tlgr/ops/folder.py +1322 -0
  146. tlgr/ops/gif.py +522 -0
  147. tlgr/ops/gift.py +1546 -0
  148. tlgr/ops/giveaway.py +541 -0
  149. tlgr/ops/inline.py +773 -0
  150. tlgr/ops/job.py +799 -0
  151. tlgr/ops/location.py +917 -0
  152. tlgr/ops/media.py +4495 -0
  153. tlgr/ops/message.py +3769 -0
  154. tlgr/ops/net.py +536 -0
  155. tlgr/ops/notify.py +840 -0
  156. tlgr/ops/passport.py +464 -0
  157. tlgr/ops/payment.py +907 -0
  158. tlgr/ops/poll.py +1078 -0
  159. tlgr/ops/premium.py +488 -0
  160. tlgr/ops/privacy.py +794 -0
  161. tlgr/ops/profile.py +1481 -0
  162. tlgr/ops/proxy.py +750 -0
  163. tlgr/ops/reaction.py +1475 -0
  164. tlgr/ops/resolve.py +1140 -0
  165. tlgr/ops/search.py +521 -0
  166. tlgr/ops/settings.py +1066 -0
  167. tlgr/ops/stars.py +594 -0
  168. tlgr/ops/sticker.py +1602 -0
  169. tlgr/ops/story.py +3216 -0
  170. tlgr/ops/sync.py +788 -0
  171. tlgr/ops/todo.py +514 -0
  172. tlgr/ops/user.py +1406 -0
  173. tlgr/ops/vc.py +2351 -0
  174. tlgr/ops/webapp.py +717 -0
  175. tlgr/ops/webhook.py +418 -0
  176. tlgr/parity.py +386 -0
  177. tlgr/processors/__init__.py +125 -0
  178. tlgr/processors/regex.py +26 -0
  179. tlgr/processors/text.py +56 -0
  180. tlgr/registry.py +519 -0
  181. tlgr/schema.py +173 -0
  182. tlgr/transport/__init__.py +30 -0
  183. tlgr/transport/autostart.py +293 -0
  184. tlgr/transport/client.py +805 -0
  185. tlgr/transport/ndjson.py +44 -0
  186. tlgr/version.py +31 -0
  187. tlgr_cli-2.0.1.dist-info/METADATA +957 -0
  188. tlgr_cli-2.0.1.dist-info/RECORD +192 -0
  189. tlgr_cli-2.0.1.dist-info/WHEEL +5 -0
  190. tlgr_cli-2.0.1.dist-info/entry_points.txt +2 -0
  191. tlgr_cli-2.0.1.dist-info/licenses/LICENSE +21 -0
  192. tlgr_cli-2.0.1.dist-info/top_level.txt +1 -0
tlgr/core/timefmt.py ADDED
@@ -0,0 +1,199 @@
1
+ """Every timestamp and duration tlgr reads or writes goes through here.
2
+
3
+ v1 called `str(datetime)` in a dozen places and produced
4
+ `"2025-03-06 12:00:00+00:00"` — a format nothing parses without a custom rule
5
+ (COR-35). v2 emits RFC-3339 UTC with a `Z`, ships a `*_unix` sibling for
6
+ anything an agent is likely to compare, and renders local time only in human
7
+ tables. There is one function per direction, and no other module formats a
8
+ date.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from datetime import date, datetime, timedelta, timezone
15
+
16
+ # `datetime.UTC` is 3.11+; we support 3.10.
17
+ UTC = timezone.utc
18
+
19
+ __all__ = [
20
+ "fmt_dt",
21
+ "fmt_local",
22
+ "fmt_unix",
23
+ "legacy_dates_enabled",
24
+ "parse_dt",
25
+ "parse_duration",
26
+ "to_unix",
27
+ ]
28
+
29
+ _DURATION_RE = re.compile(r"^(\d+(?:\.\d+)?)\s*([smhdw]?)$", re.IGNORECASE)
30
+ _RELATIVE_RE = re.compile(r"^([+-])(\d+(?:\.\d+)?)\s*([smhdw])$", re.IGNORECASE)
31
+ _UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
32
+
33
+ FOREVER = frozenset({"forever", "never", "none", "unlimited"})
34
+
35
+
36
+ class TimeFormatError(ValueError):
37
+ """A duration or timestamp that cannot be understood.
38
+
39
+ A plain ValueError subclass so `core.timefmt` keeps its dependencies to
40
+ the standard library; `core.errors.classify` maps it to USAGE.
41
+ """
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Out
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ def fmt_dt(value: datetime | None, *, legacy: bool = False) -> str | None:
50
+ """RFC-3339 UTC with a `Z` suffix, or None.
51
+
52
+ *legacy* restores v1's `str(datetime)` spelling for the one minor release
53
+ that `[defaults] legacy_dates` covers (§12.4).
54
+ """
55
+ if value is None:
56
+ return None
57
+ if value.tzinfo is None:
58
+ # Telethon hands back aware datetimes; anything naive reaching here is
59
+ # a local wall-clock value, and guessing UTC would silently shift it.
60
+ value = value.astimezone()
61
+ utc = value.astimezone(UTC)
62
+ if legacy:
63
+ return str(utc)
64
+ return utc.strftime("%Y-%m-%dT%H:%M:%SZ")
65
+
66
+
67
+ def fmt_unix(value: int | float | None, *, legacy: bool = False) -> str | None:
68
+ """A Unix timestamp as RFC-3339 UTC.
69
+
70
+ The mirror of `to_unix`, for the values Telegram gives as integers
71
+ (`mute_until`, `freeze_until_date`, `slowmode_next_send_date`): they have
72
+ to come back out as a timestamp a human can check, because "muted: true"
73
+ with a deadline in 1970 looks exactly like a mute that worked (COR-01).
74
+ """
75
+ if value is None:
76
+ return None
77
+ return fmt_dt(datetime.fromtimestamp(float(value), UTC), legacy=legacy)
78
+
79
+
80
+ def to_unix(value: datetime | None) -> int | None:
81
+ """Seconds since the epoch, for the `*_unix` sibling of every timestamp."""
82
+ if value is None:
83
+ return None
84
+ if value.tzinfo is None:
85
+ value = value.astimezone()
86
+ return int(value.timestamp())
87
+
88
+
89
+ def legacy_dates_enabled() -> bool:
90
+ """`[defaults] legacy_dates` — v1 date spelling, for one minor release."""
91
+ try:
92
+ from tlgr.core.config import load_app_config
93
+
94
+ return bool(getattr(load_app_config().defaults, "legacy_dates", False))
95
+ except Exception:
96
+ return False
97
+
98
+
99
+ def fmt_local(value: str | datetime | None, *, now: datetime | None = None) -> str:
100
+ """Human rendering: local time, precision by recency.
101
+
102
+ `today 14:02` for today, `Mon 09:14` inside the last week, and the full
103
+ `2026-09-02 09:14` beyond that — a table of timestamps is read for "when
104
+ relative to me", not for the year.
105
+ """
106
+ if value is None:
107
+ return "-"
108
+ dt = parse_dt(value) if isinstance(value, str) else value
109
+ if dt is None:
110
+ return "-"
111
+ local = dt.astimezone()
112
+ reference = (now or datetime.now(UTC)).astimezone()
113
+ delta = reference.date() - local.date()
114
+ if delta.days == 0:
115
+ return f"today {local:%H:%M}"
116
+ if delta.days == 1:
117
+ return f"yesterday {local:%H:%M}"
118
+ if 0 < delta.days < 7:
119
+ return f"{local:%a %H:%M}"
120
+ return f"{local:%Y-%m-%d %H:%M}"
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # In
125
+ # ---------------------------------------------------------------------------
126
+
127
+
128
+ def parse_dt(text: str | datetime | None, *, now: datetime | None = None) -> datetime | None:
129
+ """Parse RFC-3339, `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM` or a relative offset.
130
+
131
+ Naive values are read in the **local** zone and converted to UTC: a user
132
+ typing `2026-09-02 09:00` means nine in their own morning, and reading it
133
+ as UTC silently moves the appointment (COR-23).
134
+ """
135
+ if text is None:
136
+ return None
137
+ if isinstance(text, datetime):
138
+ return text if text.tzinfo else text.astimezone()
139
+
140
+ raw = text.strip()
141
+ if not raw:
142
+ return None
143
+ reference = now or datetime.now(UTC)
144
+
145
+ low = raw.lower()
146
+ if low == "now":
147
+ return reference
148
+ if low in ("today", "midnight"):
149
+ local = reference.astimezone()
150
+ return local.replace(hour=0, minute=0, second=0, microsecond=0)
151
+
152
+ relative = _RELATIVE_RE.match(raw)
153
+ if relative:
154
+ sign, amount, unit = relative.groups()
155
+ seconds = float(amount) * _UNIT_SECONDS[unit.lower()]
156
+ return reference + timedelta(seconds=seconds if sign == "+" else -seconds)
157
+
158
+ if raw.isdigit() and len(raw) >= 9:
159
+ # A bare unix timestamp; 9 digits keeps `20260902` out of this branch.
160
+ return datetime.fromtimestamp(int(raw), tz=UTC)
161
+
162
+ candidate = raw.replace("Z", "+00:00").replace("z", "+00:00")
163
+ try:
164
+ parsed = datetime.fromisoformat(candidate)
165
+ except ValueError:
166
+ try:
167
+ parsed = datetime.combine(date.fromisoformat(raw), datetime.min.time())
168
+ except ValueError as exc:
169
+ raise TimeFormatError(
170
+ f"{text!r} is not a timestamp: expected RFC-3339, YYYY-MM-DD, "
171
+ "or a relative offset such as -90m or +3d"
172
+ ) from exc
173
+ if parsed.tzinfo is None:
174
+ parsed = parsed.astimezone()
175
+ return parsed
176
+
177
+
178
+ def parse_duration(text: str | int | None) -> int | None:
179
+ """`30s 5m 2h 7d 1w forever` → seconds, or None for "forever".
180
+
181
+ `0` is legal and means "now/immediately"; None means "no end", which is
182
+ why the return type cannot simply be an int.
183
+ """
184
+ if text is None:
185
+ return None
186
+ if isinstance(text, int):
187
+ return text
188
+ raw = text.strip().lower()
189
+ if not raw:
190
+ return None
191
+ if raw in FOREVER:
192
+ return None
193
+ match = _DURATION_RE.match(raw)
194
+ if not match:
195
+ raise TimeFormatError(
196
+ f"{text!r} is not a duration: expected 30s, 5m, 2h, 7d, 1w or 'forever'"
197
+ )
198
+ amount, unit = match.groups()
199
+ return int(float(amount) * _UNIT_SECONDS.get(unit or "s", 1))
tlgr/core/tl.py ADDED
@@ -0,0 +1,98 @@
1
+ """TL object → JSON-safe builtins, and peer arithmetic.
2
+
3
+ Two functions, in `core/` because three layers need them and none may import
4
+ another: the bus normalises updates with them, `ops/` reads `help.*` replies
5
+ with them, and neither is allowed to reach into the other (§2.2).
6
+
7
+ `tl_to_builtins` is the COR-07 fix stated once. v1 delivered a raw `to_dict()`
8
+ through `json.dumps(default=str)`, so a `datetime` became a string in one
9
+ place and a `bytes` blew up in another — and a message with media could fail
10
+ to serialise *at delivery time*, far from the cause, counted as a delivery
11
+ failure rather than as the bug it was.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from datetime import datetime, timezone
17
+ from typing import Any
18
+
19
+ __all__ = ["CHANNEL_MARK", "peer_marked_id", "tl_to_builtins"]
20
+
21
+ #: Telegram's channel id offset. A channel's marked id is this minus its id,
22
+ #: which is what makes `-100…` recognisable at a glance.
23
+ CHANNEL_MARK = -1000000000000
24
+
25
+ #: How deep `tl_to_builtins` walks before it stops. A `Message` inside a
26
+ #: `Story` inside a `WebPage` is real; anything past this is a cycle, or a
27
+ #: payload nobody wanted in a stream frame.
28
+ _MAX_DEPTH = 8
29
+
30
+
31
+ def tl_to_builtins(value: Any, *, depth: int = 0) -> Any:
32
+ """A TL object tree → JSON-safe builtins, with the class name kept.
33
+
34
+ Datetimes become RFC-3339, bytes become hex, and the constructor name
35
+ survives as `_` so a consumer can still branch on it.
36
+ """
37
+ if value is None or isinstance(value, (bool, int, float, str)):
38
+ return value
39
+ if isinstance(value, (bytes, bytearray, memoryview)):
40
+ return bytes(value).hex()
41
+ if isinstance(value, datetime):
42
+ return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
43
+ if depth >= _MAX_DEPTH:
44
+ return type(value).__name__
45
+ if isinstance(value, (list, tuple, set, frozenset)):
46
+ return [tl_to_builtins(item, depth=depth + 1) for item in value]
47
+ if isinstance(value, dict):
48
+ return {str(k): tl_to_builtins(v, depth=depth + 1) for k, v in value.items()}
49
+
50
+ out: dict[str, Any] = {"_": type(value).__name__}
51
+ attributes = getattr(value, "__dict__", None)
52
+ names: list[str]
53
+ if isinstance(attributes, dict) and attributes:
54
+ names = [name for name in attributes if not name.startswith("_")]
55
+ else:
56
+ names = [
57
+ name
58
+ for klass in type(value).__mro__
59
+ for name in getattr(klass, "__slots__", ())
60
+ if not str(name).startswith("_")
61
+ ]
62
+ if not names:
63
+ # A TLObject with nothing set, or an object we have no handle on. Its
64
+ # class name is the honest answer; `str()` would call Telethon's
65
+ # pretty-printer, which re-enters `to_dict()` and can raise.
66
+ return out
67
+ for name in names:
68
+ out[str(name)] = tl_to_builtins(getattr(value, name, None), depth=depth + 1)
69
+ return out
70
+
71
+
72
+ def peer_marked_id(peer: Any) -> int | None:
73
+ """A TL `Peer*` → the marked id tlgr uses everywhere else.
74
+
75
+ Four lines of arithmetic rather than a call into Telethon's `utils`,
76
+ because the bus runs this on the update loop's hot path for every event.
77
+ """
78
+ if peer is None:
79
+ return None
80
+ if isinstance(peer, int):
81
+ return peer
82
+ name = type(peer).__name__
83
+ if name == "PeerUser":
84
+ return _int(getattr(peer, "user_id", None))
85
+ if name == "PeerChat":
86
+ chat_id = _int(getattr(peer, "chat_id", None))
87
+ return -chat_id if chat_id is not None else None
88
+ if name == "PeerChannel":
89
+ channel_id = _int(getattr(peer, "channel_id", None))
90
+ return CHANNEL_MARK - channel_id if channel_id is not None else None
91
+ return None
92
+
93
+
94
+ def _int(value: Any) -> int | None:
95
+ try:
96
+ return int(value)
97
+ except (TypeError, ValueError):
98
+ return None
File without changes