tdelegram 0.1.0__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 (50) hide show
  1. tdelegram/__init__.py +42 -0
  2. tdelegram/api/__init__.py +47 -0
  3. tdelegram/api/account.py +69 -0
  4. tdelegram/api/admin.py +225 -0
  5. tdelegram/api/bots.py +134 -0
  6. tdelegram/api/chats.py +314 -0
  7. tdelegram/api/contacts.py +78 -0
  8. tdelegram/api/drafts.py +49 -0
  9. tdelegram/api/export.py +275 -0
  10. tdelegram/api/folders.py +77 -0
  11. tdelegram/api/inbox.py +124 -0
  12. tdelegram/api/media.py +202 -0
  13. tdelegram/api/messages.py +400 -0
  14. tdelegram/api/polls.py +51 -0
  15. tdelegram/api/proxies.py +222 -0
  16. tdelegram/api/reactions.py +43 -0
  17. tdelegram/api/search.py +103 -0
  18. tdelegram/api/secret.py +46 -0
  19. tdelegram/api/stories.py +42 -0
  20. tdelegram/api/topics.py +161 -0
  21. tdelegram/api/updates.py +106 -0
  22. tdelegram/api/users.py +82 -0
  23. tdelegram/auth.py +433 -0
  24. tdelegram/cli/__init__.py +0 -0
  25. tdelegram/cli/context.py +301 -0
  26. tdelegram/cli/main.py +1050 -0
  27. tdelegram/cli/output.py +107 -0
  28. tdelegram/client.py +198 -0
  29. tdelegram/config.py +113 -0
  30. tdelegram/credentials.py +227 -0
  31. tdelegram/dates.py +65 -0
  32. tdelegram/entities.py +91 -0
  33. tdelegram/errors.py +127 -0
  34. tdelegram/files.py +100 -0
  35. tdelegram/loop.py +197 -0
  36. tdelegram/methods.json +4090 -0
  37. tdelegram/normalize.py +378 -0
  38. tdelegram/paging.py +127 -0
  39. tdelegram/py.typed +0 -0
  40. tdelegram/safety.py +43 -0
  41. tdelegram/schema.json +3211 -0
  42. tdelegram/schema.py +233 -0
  43. tdelegram/tdjson.py +54 -0
  44. tdelegram/transport.py +196 -0
  45. tdelegram-0.1.0.dist-info/METADATA +200 -0
  46. tdelegram-0.1.0.dist-info/RECORD +50 -0
  47. tdelegram-0.1.0.dist-info/WHEEL +4 -0
  48. tdelegram-0.1.0.dist-info/entry_points.txt +2 -0
  49. tdelegram-0.1.0.dist-info/licenses/LICENSE +201 -0
  50. tdelegram-0.1.0.dist-info/licenses/NOTICE +5 -0
tdelegram/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """TDelegram: a full-featured Telegram client library + CLI backed by TDLib."""
2
+
3
+ from importlib import metadata as _metadata
4
+
5
+ from tdelegram.client import TelegramClient
6
+ from tdelegram.config import ClientConfig
7
+ from tdelegram.errors import (
8
+ AuthError,
9
+ FloodWait,
10
+ InvalidRequest,
11
+ NotFoundError,
12
+ TelegramError,
13
+ TelegramPermissionError,
14
+ TransportError,
15
+ )
16
+
17
+ __all__ = [
18
+ "TelegramClient",
19
+ "ClientConfig",
20
+ "TelegramError",
21
+ "AuthError",
22
+ "TelegramPermissionError",
23
+ "NotFoundError",
24
+ "InvalidRequest",
25
+ "FloodWait",
26
+ "TransportError",
27
+ ]
28
+
29
+
30
+ def _installed_version() -> str:
31
+ # pyproject.toml is the only place the version is written. A second copy
32
+ # here had to be bumped by hand and nothing checked that it was, so a
33
+ # release that bumped only pyproject.toml would have reported the version
34
+ # before it. Reinstall after a bump for a dev checkout to see it.
35
+ try:
36
+ return _metadata.version("tdelegram")
37
+ except _metadata.PackageNotFoundError:
38
+ # Imported from a source tree that was never installed.
39
+ return "0+unknown"
40
+
41
+
42
+ __version__ = _installed_version()
@@ -0,0 +1,47 @@
1
+ """Domain APIs over TelegramClient."""
2
+
3
+ from tdelegram.api import (
4
+ account,
5
+ admin,
6
+ bots,
7
+ chats,
8
+ contacts,
9
+ drafts,
10
+ export,
11
+ folders,
12
+ inbox,
13
+ media,
14
+ messages,
15
+ polls,
16
+ proxies,
17
+ reactions,
18
+ search,
19
+ secret,
20
+ stories,
21
+ topics,
22
+ updates,
23
+ users,
24
+ )
25
+
26
+ __all__ = [
27
+ "account",
28
+ "admin",
29
+ "bots",
30
+ "chats",
31
+ "contacts",
32
+ "drafts",
33
+ "export",
34
+ "folders",
35
+ "inbox",
36
+ "media",
37
+ "messages",
38
+ "polls",
39
+ "proxies",
40
+ "reactions",
41
+ "search",
42
+ "secret",
43
+ "stories",
44
+ "topics",
45
+ "updates",
46
+ "users",
47
+ ]
@@ -0,0 +1,69 @@
1
+ """Account: info, profile, privacy, sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from tdelegram.client import TelegramClient
8
+
9
+
10
+ def info(client: TelegramClient) -> dict[str, Any]:
11
+ return client.call("getMe", {})
12
+
13
+
14
+ def full_info(client: TelegramClient, user_id: int) -> dict[str, Any]:
15
+ return client.call("getUserFullInfo", {"user_id": user_id})
16
+
17
+
18
+ def set_profile(
19
+ client: TelegramClient,
20
+ *,
21
+ first_name: str | None = None,
22
+ last_name: str | None = None,
23
+ bio: str | None = None,
24
+ allow_write: bool = False,
25
+ ) -> dict[str, Any]:
26
+ results: dict[str, Any] = {}
27
+ if first_name is not None or last_name is not None:
28
+ results["name"] = client.call(
29
+ "setName",
30
+ {"first_name": first_name or "", "last_name": last_name or ""},
31
+ allow_write=allow_write,
32
+ )
33
+ if bio is not None:
34
+ results["bio"] = client.call("setBio", {"bio": bio}, allow_write=allow_write)
35
+ return results or {"@type": "ok"}
36
+
37
+
38
+ def sessions(client: TelegramClient) -> dict[str, Any]:
39
+ return client.call("getActiveSessions", {})
40
+
41
+
42
+ def terminate_session(
43
+ client: TelegramClient,
44
+ session_id: int,
45
+ *,
46
+ allow_write: bool = False,
47
+ allow_destructive: bool = False,
48
+ ) -> dict[str, Any]:
49
+ return client.call(
50
+ "terminateSession",
51
+ {"session_id": session_id},
52
+ allow_write=allow_write,
53
+ allow_destructive=allow_destructive,
54
+ )
55
+
56
+
57
+ def terminate_others(
58
+ client: TelegramClient, *, allow_write: bool = False, allow_destructive: bool = False
59
+ ) -> dict[str, Any]:
60
+ return client.call(
61
+ "terminateAllOtherSessions",
62
+ {},
63
+ allow_write=allow_write,
64
+ allow_destructive=allow_destructive,
65
+ )
66
+
67
+
68
+ def privacy(client: TelegramClient, setting: str) -> dict[str, Any]:
69
+ return client.call("getUserPrivacySettingRules", {"setting": {"@type": setting}})
tdelegram/api/admin.py ADDED
@@ -0,0 +1,225 @@
1
+ """Admin: promote/demote/ban/restrict/permissions/slowmode/invites."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ from tdelegram.api.chats import resolve, resolve_id
9
+ from tdelegram.client import TelegramClient
10
+
11
+ # The least an administrator can hold, and what `promote` grants unless told
12
+ # otherwise: every other right implies it.
13
+ DEFAULT_RIGHTS = ("manage_chat",)
14
+
15
+
16
+ def administrator_rights(rights: Iterable[str]) -> dict[str, Any]:
17
+ """chatAdministratorRights granting exactly `rights`, and nothing else.
18
+
19
+ Names are the schema's fields with or without the `can_` prefix, so
20
+ `delete_messages` and `can_delete_messages` both work; `is_anonymous` is
21
+ taken as it is.
22
+ """
23
+ from tdelegram import schema
24
+
25
+ entry = schema.constructor("chatAdministratorRights")
26
+ known = list(entry["fields"]) if entry else []
27
+ granted: dict[str, Any] = {"@type": "chatAdministratorRights"}
28
+ for right in rights:
29
+ name = right.strip().lower().replace("-", "_")
30
+ field = name if name in known else f"can_{name}"
31
+ if field not in known:
32
+ short = ", ".join(k.removeprefix("can_") for k in known)
33
+ raise ValueError(f"Unknown administrator right {right!r}; choose from: {short}.")
34
+ granted[field] = True
35
+ return granted
36
+
37
+
38
+ def _set_status(
39
+ client: TelegramClient,
40
+ chat_id: int,
41
+ user_id: int,
42
+ status: dict[str, Any],
43
+ *,
44
+ allow_write: bool,
45
+ allow_destructive: bool,
46
+ ) -> dict[str, Any]:
47
+ return client.call(
48
+ "setChatMemberStatus",
49
+ {
50
+ "chat_id": chat_id,
51
+ "member_id": {"@type": "messageSenderUser", "user_id": user_id},
52
+ "status": status,
53
+ },
54
+ allow_write=allow_write,
55
+ allow_destructive=allow_destructive,
56
+ )
57
+
58
+
59
+ def set_member_status(
60
+ client: TelegramClient,
61
+ chat_ref: str,
62
+ user_id: int,
63
+ status: dict[str, Any],
64
+ *,
65
+ allow_write: bool = False,
66
+ allow_destructive: bool = False,
67
+ ) -> dict[str, Any]:
68
+ """Set a member's status. Can ban, so it is gated like banChatMember."""
69
+ return _set_status(
70
+ client,
71
+ resolve_id(client, chat_ref),
72
+ user_id,
73
+ status,
74
+ allow_write=allow_write,
75
+ allow_destructive=allow_destructive,
76
+ )
77
+
78
+
79
+ def promote(
80
+ client: TelegramClient,
81
+ chat_ref: str,
82
+ user_id: int,
83
+ *,
84
+ rights: Iterable[str] = DEFAULT_RIGHTS,
85
+ title: str = "",
86
+ allow_write: bool = False,
87
+ allow_destructive: bool = False,
88
+ ) -> dict[str, Any]:
89
+ """Make a member an administrator with exactly `rights`.
90
+
91
+ The status used to carry `custom_title` and no `rights`, neither of which
92
+ TDLib's administrator status takes any more, so the promotion granted
93
+ nothing. A title is now the member's tag, set by a second call -- which
94
+ channels do not have, so a title there is refused before anything changes
95
+ rather than failing after the promotion went through.
96
+ """
97
+ chat = resolve(client, chat_ref)
98
+ if title and (chat.get("type") or {}).get("is_channel"):
99
+ raise ValueError("Channels have no member titles; promote without --title.")
100
+ chat_id = int(chat["id"])
101
+ result = _set_status(
102
+ client,
103
+ chat_id,
104
+ user_id,
105
+ {"@type": "chatMemberStatusAdministrator", "rights": administrator_rights(rights)},
106
+ allow_write=allow_write,
107
+ allow_destructive=allow_destructive,
108
+ )
109
+ if title:
110
+ client.call(
111
+ "setChatMemberTag",
112
+ {"chat_id": chat_id, "user_id": user_id, "tag": title},
113
+ allow_write=allow_write,
114
+ )
115
+ return result
116
+
117
+
118
+ def demote(
119
+ client: TelegramClient,
120
+ chat_ref: str,
121
+ user_id: int,
122
+ *,
123
+ allow_write: bool = False,
124
+ allow_destructive: bool = False,
125
+ ) -> dict[str, Any]:
126
+ return set_member_status(
127
+ client,
128
+ chat_ref,
129
+ user_id,
130
+ {"@type": "chatMemberStatusMember"},
131
+ allow_write=allow_write,
132
+ allow_destructive=allow_destructive,
133
+ )
134
+
135
+
136
+ def ban(
137
+ client: TelegramClient,
138
+ chat_ref: str,
139
+ user_id: int,
140
+ *,
141
+ until: int = 0,
142
+ allow_write: bool = False,
143
+ allow_destructive: bool = False,
144
+ ) -> dict[str, Any]:
145
+ return client.call(
146
+ "banChatMember",
147
+ {
148
+ "chat_id": resolve_id(client, chat_ref),
149
+ "member_id": {"@type": "messageSenderUser", "user_id": user_id},
150
+ "banned_until_date": until,
151
+ },
152
+ allow_write=allow_write,
153
+ allow_destructive=allow_destructive,
154
+ )
155
+
156
+
157
+ def restrict(
158
+ client: TelegramClient,
159
+ chat_ref: str,
160
+ user_id: int,
161
+ *,
162
+ permissions: dict[str, Any] | None = None,
163
+ allow_write: bool = False,
164
+ allow_destructive: bool = False,
165
+ ) -> dict[str, Any]:
166
+ return set_member_status(
167
+ client,
168
+ chat_ref,
169
+ user_id,
170
+ {
171
+ "@type": "chatMemberStatusRestricted",
172
+ "permissions": permissions
173
+ or {"@type": "chatPermissions", "can_send_basic_messages": False},
174
+ "is_member": True,
175
+ },
176
+ allow_write=allow_write,
177
+ allow_destructive=allow_destructive,
178
+ )
179
+
180
+
181
+ def unrestrict(
182
+ client: TelegramClient,
183
+ chat_ref: str,
184
+ user_id: int,
185
+ *,
186
+ allow_write: bool = False,
187
+ allow_destructive: bool = False,
188
+ ) -> dict[str, Any]:
189
+ return demote(
190
+ client, chat_ref, user_id, allow_write=allow_write, allow_destructive=allow_destructive
191
+ )
192
+
193
+
194
+ def slowmode(
195
+ client: TelegramClient, chat_ref: str, delay_seconds: int, *, allow_write: bool = False
196
+ ) -> dict[str, Any]:
197
+ return client.call(
198
+ "setChatSlowModeDelay",
199
+ {"chat_id": resolve_id(client, chat_ref), "slow_mode_delay": delay_seconds},
200
+ allow_write=allow_write,
201
+ )
202
+
203
+
204
+ def create_invite(
205
+ client: TelegramClient, chat_ref: str, *, allow_write: bool = False
206
+ ) -> dict[str, Any]:
207
+ return client.call(
208
+ "createChatInviteLink", {"chat_id": resolve_id(client, chat_ref)}, allow_write=allow_write
209
+ )
210
+
211
+
212
+ def revoke_invite(
213
+ client: TelegramClient,
214
+ chat_ref: str,
215
+ invite_link: str,
216
+ *,
217
+ allow_write: bool = False,
218
+ allow_destructive: bool = False,
219
+ ) -> dict[str, Any]:
220
+ return client.call(
221
+ "revokeChatInviteLink",
222
+ {"chat_id": resolve_id(client, chat_ref), "invite_link": invite_link},
223
+ allow_write=allow_write,
224
+ allow_destructive=allow_destructive,
225
+ )
tdelegram/api/bots.py ADDED
@@ -0,0 +1,134 @@
1
+ """Bots: inline queries, inline results, callback answers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from tdelegram.api.chats import resolve_id
8
+ from tdelegram.client import TelegramClient
9
+
10
+
11
+ def start_bot(
12
+ client: TelegramClient,
13
+ bot_ref: str,
14
+ chat_ref: str,
15
+ *,
16
+ parameter: str = "",
17
+ allow_write: bool = False,
18
+ ) -> dict[str, Any]:
19
+ bot_id = resolve_id(client, bot_ref)
20
+ chat_id = resolve_id(client, chat_ref)
21
+ return client.call(
22
+ "sendBotStartMessage",
23
+ {"bot_user_id": bot_id, "chat_id": chat_id, "parameter": parameter},
24
+ allow_write=allow_write,
25
+ )
26
+
27
+
28
+ def inline_results(
29
+ client: TelegramClient,
30
+ bot_user_id: int,
31
+ query: str,
32
+ *,
33
+ offset: str = "",
34
+ allow_write: bool = False,
35
+ ) -> dict[str, Any]:
36
+ """Send an inline query to a bot. The bot is notified, so this is a write."""
37
+ return client.call(
38
+ "getInlineQueryResults",
39
+ {"bot_user_id": bot_user_id, "chat_id": 0, "query": query, "offset": offset},
40
+ allow_write=allow_write,
41
+ )
42
+
43
+
44
+ def send_inline_result(
45
+ client: TelegramClient,
46
+ chat_ref: str,
47
+ query_id: int | str,
48
+ result_id: str,
49
+ *,
50
+ allow_write: bool = False,
51
+ ) -> dict[str, Any]:
52
+ """Send one result of an inline query. `query_id` comes from inline_results()."""
53
+ return client.call(
54
+ "sendInlineQueryResultMessage",
55
+ {"chat_id": resolve_id(client, chat_ref), "query_id": query_id, "result_id": result_id},
56
+ allow_write=allow_write,
57
+ )
58
+
59
+
60
+ def press_button(
61
+ client: TelegramClient,
62
+ chat_ref: str,
63
+ message_id: int,
64
+ label: str,
65
+ *,
66
+ allow_write: bool = False,
67
+ ) -> dict[str, Any]:
68
+ """Press the inline button labelled `label` under a bot's message.
69
+
70
+ The bot sees the press and acts on it, which is why getCallbackQueryAnswer
71
+ is a write. A URL button has nothing to press, so its link comes back
72
+ instead; a button that needs a password, a game or a payment is refused.
73
+ """
74
+ chat_id = resolve_id(client, chat_ref)
75
+ message = client.call("getMessage", {"chat_id": chat_id, "message_id": message_id})
76
+ markup = message.get("reply_markup") or {}
77
+ rows = markup.get("rows") if markup.get("@type") == "replyMarkupInlineKeyboard" else None
78
+ buttons = [button for row in rows or [] for button in row or []]
79
+ chosen = [b for b in buttons if b.get("text") == label] or [
80
+ b for b in buttons if str(b.get("text", "")).casefold() == label.casefold()
81
+ ]
82
+ if not chosen:
83
+ offered = ", ".join(repr(b.get("text")) for b in buttons) or "none"
84
+ raise ValueError(f"Message {message_id} has no button {label!r}; its buttons: {offered}.")
85
+ kind = chosen[0].get("type") or {}
86
+ found = {"chat_id": chat_id, "message_id": message_id, "button": chosen[0].get("text")}
87
+ if kind.get("@type") == "inlineKeyboardButtonTypeUrl":
88
+ return {**found, "pressed": False, "url": kind.get("url")}
89
+ if kind.get("@type") != "inlineKeyboardButtonTypeCallback":
90
+ raise ValueError(f"{label!r} is a {kind.get('@type')} button, which only an app can use.")
91
+ answer = client.call(
92
+ "getCallbackQueryAnswer",
93
+ {
94
+ "chat_id": chat_id,
95
+ "message_id": message_id,
96
+ "payload": {"@type": "callbackQueryPayloadData", "data": kind.get("data", "")},
97
+ },
98
+ allow_write=allow_write,
99
+ )
100
+ return {
101
+ **found,
102
+ "pressed": True,
103
+ "answer": answer.get("text") or None,
104
+ "show_alert": bool(answer.get("show_alert")),
105
+ "url": answer.get("url") or None,
106
+ }
107
+
108
+
109
+ def answer_callback(
110
+ client: TelegramClient, query_id: int, *, text: str = "", allow_write: bool = False
111
+ ) -> dict[str, Any]:
112
+ """For bot accounts only: a user account cannot answer callback queries."""
113
+ return client.call(
114
+ "answerCallbackQuery",
115
+ {"callback_query_id": query_id, "text": text},
116
+ allow_write=allow_write,
117
+ )
118
+
119
+
120
+ def answer_inline(
121
+ client: TelegramClient,
122
+ inline_query_id: int,
123
+ results: list[dict[str, Any]],
124
+ *,
125
+ allow_write: bool = False,
126
+ ) -> dict[str, Any]:
127
+ return client.call(
128
+ "answerInlineQuery",
129
+ {"inline_query_id": inline_query_id, "results": results},
130
+ allow_write=allow_write,
131
+ )
132
+
133
+
134
+ # -- stories ----------------------------------------------------------