python-telegram 2.0.0__py3-none-macosx_11_0_arm64.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.
telegram/client.py ADDED
@@ -0,0 +1,1101 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import enum
5
+ import getpass
6
+ import hashlib
7
+ import logging
8
+ import queue
9
+ import signal
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ import typing
14
+ from collections import defaultdict
15
+ from collections.abc import Callable
16
+ from pathlib import Path
17
+ from types import FrameType
18
+ from typing import (
19
+ Any,
20
+ Literal,
21
+ )
22
+
23
+ from telegram import VERSION
24
+ from telegram.tdjson import ClientDestroyedError, TDJson
25
+ from telegram.text import Element
26
+ from telegram.utils import AsyncResult
27
+ from telegram.worker import BaseWorker, SimpleWorker
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ MESSAGE_HANDLER_TYPE: str = "updateNewMessage"
33
+
34
+ # how long `stop` waits for tdlib to report the CLOSED authorization state
35
+ DEFAULT_CLOSE_TIMEOUT: float = 5.0
36
+
37
+
38
+ class AuthorizationState(enum.Enum):
39
+ NONE = None
40
+ WAIT_CODE = "authorizationStateWaitCode"
41
+ WAIT_PASSWORD = "authorizationStateWaitPassword"
42
+ WAIT_TDLIB_PARAMETERS = "authorizationStateWaitTdlibParameters"
43
+ # only tdlib 1.8.5 and older emit this state, newer versions take the
44
+ # encryption key in setTdlibParameters
45
+ WAIT_ENCRYPTION_KEY = "authorizationStateWaitEncryptionKey"
46
+ WAIT_PHONE_NUMBER = "authorizationStateWaitPhoneNumber"
47
+ WAIT_PREMIUM_PURCHASE = "authorizationStateWaitPremiumPurchase"
48
+ WAIT_EMAIL_ADDRESS = "authorizationStateWaitEmailAddress"
49
+ WAIT_EMAIL_CODE = "authorizationStateWaitEmailCode"
50
+ WAIT_OTHER_DEVICE_CONFIRMATION = "authorizationStateWaitOtherDeviceConfirmation"
51
+ WAIT_REGISTRATION = "authorizationStateWaitRegistration"
52
+ READY = "authorizationStateReady"
53
+ LOGGING_OUT = "authorizationStateLoggingOut"
54
+ CLOSING = "authorizationStateClosing"
55
+ CLOSED = "authorizationStateClosed"
56
+ UNKNOWN = "unknown"
57
+
58
+ @classmethod
59
+ def _missing_(cls, value: object) -> AuthorizationState:
60
+ """
61
+ tdlib adds authorization states over time, and a state this library has
62
+ never heard of must not turn into a ValueError halfway through login.
63
+ """
64
+ logger.warning("Unknown authorization state %r, treating it as UNKNOWN", value)
65
+
66
+ return cls.UNKNOWN
67
+
68
+
69
+ class Telegram:
70
+ def __init__(
71
+ self,
72
+ api_id: int,
73
+ api_hash: str,
74
+ database_encryption_key: str | bytes,
75
+ phone: str | None = None,
76
+ bot_token: str | None = None,
77
+ library_path: str | None = None,
78
+ worker: type[BaseWorker] | None = None,
79
+ files_directory: str | Path | None = None,
80
+ use_test_dc: bool = False,
81
+ use_message_database: bool = True,
82
+ device_model: str = "python-telegram",
83
+ application_version: str = VERSION,
84
+ system_version: str = "unknown",
85
+ system_language_code: str = "en",
86
+ login: bool = False,
87
+ default_workers_queue_size: int = 1000,
88
+ tdlib_verbosity: int = 2,
89
+ proxy_server: str = "",
90
+ proxy_port: int = 0,
91
+ proxy_type: dict[str, str] | None = None,
92
+ use_secret_chats: bool = True,
93
+ ) -> None:
94
+ """
95
+ Args:
96
+ api_id - ID of your app (https://my.telegram.org/apps/)
97
+ api_hash - api_hash of your app (https://my.telegram.org/apps/)
98
+ phone - your phone number
99
+ library_path - you can change path to the compiled libtdjson library
100
+ worker - worker to process updates
101
+ files_directory - directory for the tdlib's files (database, images, etc.)
102
+ use_test_dc - use test datacenter
103
+ use_message_database
104
+ use_secret_chats
105
+ device_model
106
+ application_version
107
+ system_version
108
+ system_language_code
109
+ """
110
+ self.api_id = api_id
111
+ self.api_hash = api_hash
112
+ self.library_path = library_path
113
+ self.phone = phone
114
+ self.bot_token = bot_token
115
+ self.use_test_dc = use_test_dc
116
+ self.device_model = device_model
117
+ self.system_version = system_version
118
+ self.system_language_code = system_language_code
119
+ self.application_version = application_version
120
+ self.use_message_database = use_message_database
121
+ self._queue_put_timeout = 10
122
+ self.proxy_server = proxy_server
123
+ self.proxy_port = proxy_port
124
+ self.proxy_type = proxy_type
125
+ self.use_secret_chats = use_secret_chats
126
+ self.authorization_state = AuthorizationState.NONE
127
+
128
+ if not self.bot_token and not self.phone:
129
+ raise ValueError("You must provide bot_token or phone")
130
+
131
+ self._database_encryption_key = database_encryption_key
132
+ if isinstance(self._database_encryption_key, str):
133
+ self._database_encryption_key = self._database_encryption_key.encode()
134
+
135
+ self._database_encryption_key = base64.b64encode(self._database_encryption_key).decode()
136
+
137
+ if not files_directory:
138
+ hasher = hashlib.md5()
139
+ str_to_encode: str = self.phone or self.bot_token # type: ignore
140
+ hasher.update(str_to_encode.encode("utf-8"))
141
+ directory_name = hasher.hexdigest()
142
+ files_directory = Path(tempfile.gettempdir()) / ".tdlib_files" / directory_name
143
+
144
+ self.files_directory = Path(files_directory)
145
+
146
+ self._authorized = False
147
+ self._stopped = threading.Event()
148
+
149
+ # todo: move to worker
150
+ self._workers_queue: queue.Queue = queue.Queue(maxsize=default_workers_queue_size)
151
+
152
+ if not worker:
153
+ worker = SimpleWorker
154
+ self.worker: BaseWorker = worker(queue=self._workers_queue)
155
+
156
+ self._results: dict[str, AsyncResult] = {}
157
+ self._update_handlers: defaultdict[str, list[Callable]] = defaultdict(list)
158
+
159
+ self._tdjson = TDJson(library_path=library_path, verbosity=tdlib_verbosity)
160
+ self._run()
161
+
162
+ if login:
163
+ self.login()
164
+
165
+ def stop(self, close_timeout: float = DEFAULT_CLOSE_TIMEOUT) -> None:
166
+ """
167
+ Stops the client.
168
+
169
+ Args:
170
+ close_timeout: how long to wait for tdlib to close the session
171
+ before shutting down anyway
172
+ """
173
+
174
+ if self._stopped.is_set():
175
+ return
176
+
177
+ logger.info("Stopping telegram client...")
178
+
179
+ try:
180
+ self._close(timeout=close_timeout)
181
+ except Exception:
182
+ # a broken or unresponsive tdlib must not prevent the shutdown:
183
+ # everything below has to run, otherwise `idle` never returns,
184
+ # the listener thread keeps going and the tdlib client leaks
185
+ logger.exception("Could not close the tdlib session cleanly, stopping anyway")
186
+
187
+ self._stopped.set()
188
+ self.worker.stop()
189
+
190
+ # wait for the tdjson listener to stop
191
+ self._td_listener.join()
192
+
193
+ if hasattr(self, "_tdjson"):
194
+ self._tdjson.stop()
195
+
196
+ def _close(self, timeout: float = DEFAULT_CLOSE_TIMEOUT) -> None:
197
+ """
198
+ Calls `close` tdlib method and waits until authorization_state becomes CLOSED.
199
+
200
+ Blocking, but gives up after `timeout` seconds.
201
+ """
202
+ self.call_method("close")
203
+
204
+ deadline = time.monotonic() + timeout
205
+
206
+ while self.authorization_state != AuthorizationState.CLOSED:
207
+ time_left = deadline - time.monotonic()
208
+
209
+ if time_left <= 0:
210
+ logger.warning(
211
+ "tdlib has not reached the CLOSED state in %s seconds, last known state: %s",
212
+ timeout,
213
+ self.authorization_state,
214
+ )
215
+ return
216
+
217
+ result = self.get_authorization_state()
218
+ self.authorization_state = self._wait_authorization_result(result, timeout=time_left)
219
+ logger.info("Authorization state: %s", self.authorization_state)
220
+ time.sleep(0.5)
221
+
222
+ def parse_text_entities(self, text: str, parse_mode: Literal["HTML", "Markdown"]) -> AsyncResult:
223
+ """
224
+ Parses text from 'HTML' and 'Markdown' (not MarkdownV2) into plain
225
+ text and internal telegram style description.
226
+
227
+ Args:
228
+ text
229
+ parse_mode
230
+
231
+ Returns:
232
+ AsyncResult
233
+
234
+ The update will be::
235
+
236
+ {
237
+ '@type': 'formattedText',
238
+ 'text': 'Hello world!',
239
+ 'entities': [
240
+ {
241
+ '@type': 'textEntity',
242
+ 'offset': 0,
243
+ 'length': 12,
244
+ 'type': {
245
+ '@type': 'textEntityTypeSpoiler'
246
+ }
247
+ }
248
+ ...
249
+ ]
250
+ }
251
+ """
252
+
253
+ parse_mode_types = {
254
+ "HTML": "textParseModeHTML",
255
+ "Markdown": "textParseModeMarkdown",
256
+ }
257
+ data = {
258
+ "@type": "parseTextEntities",
259
+ "text": text,
260
+ "parse_mode": {
261
+ "@type": parse_mode_types[parse_mode],
262
+ },
263
+ }
264
+
265
+ return self._send_data(data)
266
+
267
+ def send_message(
268
+ self,
269
+ chat_id: int,
270
+ text: str | Element,
271
+ entities: list[dict] | None = None,
272
+ ) -> AsyncResult:
273
+ """
274
+ Sends a message to a chat. The chat must be in the tdlib's database.
275
+ If there is no chat in the DB, tdlib returns an error.
276
+ Chat is being saved to the database when the client receives a message or when you call the `get_chats` method.
277
+
278
+ Args:
279
+ chat_id
280
+ text
281
+
282
+ Returns:
283
+ AsyncResult
284
+
285
+ The update will be::
286
+
287
+ {
288
+ '@type': 'message',
289
+ 'id': 1,
290
+ 'sender_user_id': 2,
291
+ 'chat_id': 3,
292
+ ...
293
+ }
294
+ """
295
+
296
+ if entities is None:
297
+ entities = []
298
+
299
+ updated_text: str
300
+ if isinstance(text, Element):
301
+ result = self.parse_text_entities(text.to_html(), parse_mode="HTML")
302
+ result.wait(raise_exc=True)
303
+ if result.update is None:
304
+ raise RuntimeError(f"Failed to parse text entities: {result.error_info}")
305
+ update: dict = result.update
306
+ entities = update["entities"]
307
+ updated_text = update["text"]
308
+ else:
309
+ updated_text = text
310
+
311
+ data = {
312
+ "@type": "sendMessage",
313
+ "chat_id": chat_id,
314
+ "input_message_content": {
315
+ "@type": "inputMessageText",
316
+ "text": {
317
+ "@type": "formattedText",
318
+ "text": updated_text,
319
+ "entities": entities,
320
+ },
321
+ },
322
+ }
323
+
324
+ return self._send_data(data)
325
+
326
+ def import_contacts(self, contacts: list[dict[str, str]]) -> AsyncResult:
327
+ """
328
+ Adds new contacts or edits existing contacts by their phone numbers.
329
+ https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1import_contacts.html
330
+
331
+ Args:
332
+ contacts
333
+
334
+ ``contacts`` is a list of the form::
335
+
336
+ [
337
+ {
338
+ "phone_number": "+380 12 345 67 89",
339
+ "first_name": "Name",
340
+ "last_name": "Surname"
341
+ },
342
+ {
343
+ "phone_number": "+380 09 876 54 32",
344
+ "first_name": "Name",
345
+ "last_name": "Surname"
346
+ },
347
+ ...
348
+ ]
349
+
350
+ The phone number format is country-specific.
351
+
352
+ Returns:
353
+ AsyncResult
354
+
355
+ The update will be::
356
+
357
+ {
358
+ '@type': 'importedContacts',
359
+ 'user_ids': [1, 2],
360
+ 'importer_count': [3, 4],
361
+ ...
362
+ }
363
+ """
364
+
365
+ for contact in contacts:
366
+ contact["@type"] = "importedContact"
367
+
368
+ data = {
369
+ "@type": "importContacts",
370
+ "contacts": contacts,
371
+ }
372
+
373
+ return self._send_data(data)
374
+
375
+ def get_chat(self, chat_id: int) -> AsyncResult:
376
+ """
377
+ This is offline request, if there is no chat in your database it will not be found
378
+ tdlib saves chat to the database when it receives a new message or when you call `get_chats` method.
379
+ """
380
+ data = {"@type": "getChat", "chat_id": chat_id}
381
+
382
+ return self._send_data(data)
383
+
384
+ def get_me(self) -> AsyncResult:
385
+ """
386
+ Requests information of the current user (getMe method)
387
+
388
+ https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_me.html
389
+ """
390
+
391
+ return self.call_method("getMe")
392
+
393
+ def get_user(self, user_id: int) -> AsyncResult:
394
+ """
395
+ Requests information about a user with id = user_id.
396
+
397
+ https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_user.html
398
+ """
399
+
400
+ return self.call_method("getUser", params={"user_id": user_id})
401
+
402
+ def get_user_full_info(self, user_id: int) -> AsyncResult:
403
+ """
404
+ Requests the full information about a user with id = user_id.
405
+
406
+ https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_user_full_info.html
407
+ """
408
+
409
+ return self.call_method("getUserFullInfo", params={"user_id": user_id})
410
+
411
+ def get_chats(self, limit: int = 100, chat_list: dict | None = None) -> AsyncResult:
412
+ """
413
+ Returns an ordered list of chats from the beginning of a chat list.
414
+
415
+ tdlib loads chats from the server until ``limit`` chats are available
416
+ or the end of the list is reached, so this method also saves those
417
+ chats to the database.
418
+
419
+ https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_chats.html
420
+
421
+ Args:
422
+ limit: the maximum number of chats to return
423
+ chat_list: the chat list to return chats from, the main chat list
424
+ if not set. For example: ``{'@type': 'chatListArchive'}``
425
+
426
+ Returns:
427
+ AsyncResult
428
+
429
+ The update will be::
430
+
431
+ {
432
+ '@type': 'chats',
433
+ 'total_count': 10,
434
+ 'chat_ids': [...],
435
+ '@extra': {
436
+ 'request_id': '...'
437
+ }
438
+ }
439
+ """
440
+ data = {
441
+ "@type": "getChats",
442
+ "chat_list": chat_list,
443
+ "limit": limit,
444
+ }
445
+
446
+ return self._send_data(data)
447
+
448
+ def load_chats(self, limit: int = 100, chat_list: dict | None = None) -> AsyncResult:
449
+ """
450
+ Loads more chats from a chat list.
451
+
452
+ The chats are not returned by this method, they are sent through
453
+ updates. tdlib chooses how many chats to load and can load fewer
454
+ than ``limit``.
455
+
456
+ https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1load_chats.html
457
+
458
+ Args:
459
+ limit: the maximum number of chats to load
460
+ chat_list: the chat list to load chats from, the main chat list
461
+ if not set. For example: ``{'@type': 'chatListArchive'}``
462
+
463
+ Returns:
464
+ AsyncResult
465
+
466
+ The update will be ``{'@type': 'ok'}``, or an error with the code
467
+ 404 when all the chats in the list have already been loaded.
468
+ """
469
+ data = {
470
+ "@type": "loadChats",
471
+ "chat_list": chat_list,
472
+ "limit": limit,
473
+ }
474
+
475
+ return self._send_data(data)
476
+
477
+ def get_chat_history(
478
+ self,
479
+ chat_id: int,
480
+ limit: int = 100,
481
+ from_message_id: int = 0,
482
+ offset: int = 0,
483
+ only_local: bool = False,
484
+ ) -> AsyncResult:
485
+ """
486
+ Returns history of a chat
487
+
488
+ Args:
489
+ chat_id
490
+ limit
491
+ from_message_id
492
+ offset
493
+ only_local
494
+ """
495
+ data = {
496
+ "@type": "getChatHistory",
497
+ "chat_id": chat_id,
498
+ "limit": limit,
499
+ "from_message_id": from_message_id,
500
+ "offset": offset,
501
+ "only_local": only_local,
502
+ }
503
+
504
+ return self._send_data(data)
505
+
506
+ def get_message(
507
+ self,
508
+ chat_id: int,
509
+ message_id: int,
510
+ ) -> AsyncResult:
511
+ """
512
+ Return a message via its message_id
513
+
514
+ Args:
515
+ chat_id
516
+ message_id
517
+
518
+ Returns:
519
+ AsyncResult
520
+
521
+ The update will be::
522
+
523
+ {
524
+ '@type': 'message',
525
+ 'id': 1,
526
+ 'sender_user_id': 2,
527
+ 'chat_id': 3,
528
+ 'content': {...},
529
+ ...
530
+ }
531
+ """
532
+ data = {
533
+ "@type": "getMessage",
534
+ "chat_id": chat_id,
535
+ "message_id": message_id,
536
+ }
537
+
538
+ return self._send_data(data)
539
+
540
+ def delete_messages(self, chat_id: int, message_ids: list[int], revoke: bool = True) -> AsyncResult:
541
+ """
542
+ Delete a list of messages in a chat
543
+
544
+ Args:
545
+ chat_id
546
+ message_ids
547
+ revoke
548
+ """
549
+
550
+ return self._send_data(
551
+ {
552
+ "@type": "deleteMessages",
553
+ "chat_id": chat_id,
554
+ "message_ids": message_ids,
555
+ "revoke": revoke,
556
+ }
557
+ )
558
+
559
+ def get_supergroup_full_info(self, supergroup_id: int) -> AsyncResult:
560
+ """
561
+ Get the full info of a supergroup
562
+
563
+ Args:
564
+ supergroup_id
565
+ """
566
+
567
+ return self._send_data({"@type": "getSupergroupFullInfo", "supergroup_id": supergroup_id})
568
+
569
+ def create_basic_group_chat(self, basic_group_id: int) -> AsyncResult:
570
+ """
571
+ Create a chat from a basic group
572
+
573
+ Args:
574
+ basic_group_id
575
+ """
576
+
577
+ return self._send_data({"@type": "createBasicGroupChat", "basic_group_id": basic_group_id})
578
+
579
+ def get_web_page_instant_view(self, url: str, only_local: bool = False) -> AsyncResult:
580
+ """
581
+ Use this method to request instant preview of a webpage.
582
+ Returns error with 404 if there is no preview for this webpage.
583
+
584
+ Args:
585
+ url: URL of a webpage
586
+ only_local: If true, the instant view is built from locally available
587
+ information only, without any network requests
588
+ """
589
+ data = {"@type": "getWebPageInstantView", "url": url, "only_local": only_local}
590
+
591
+ return self._send_data(data)
592
+
593
+ def call_method(
594
+ self,
595
+ method_name: str,
596
+ params: dict[str, Any] | None = None,
597
+ block: bool = False,
598
+ ) -> AsyncResult:
599
+ """
600
+ Use this method to call any other method of the tdlib
601
+
602
+ Args:
603
+ method_name: Name of the method
604
+ params: parameters
605
+ """
606
+ data = {"@type": method_name}
607
+
608
+ if params:
609
+ data.update(params)
610
+
611
+ return self._send_data(data, block=block)
612
+
613
+ def _run(self) -> None:
614
+ self._td_listener = threading.Thread(target=self._listen_to_td)
615
+ self._td_listener.daemon = True
616
+ self._td_listener.start()
617
+
618
+ self.worker.run()
619
+
620
+ def _listen_to_td(self) -> None:
621
+ logger.info("[Telegram.td_listener] started")
622
+
623
+ while not self._stopped.is_set():
624
+ try:
625
+ update = self._tdjson.receive()
626
+
627
+ if update:
628
+ self._update_async_result(update)
629
+ self._run_handlers(update)
630
+ except ClientDestroyedError:
631
+ # nothing left to listen to, and retrying would spin
632
+ logger.info("[Telegram.td_listener] the tdlib client is gone, stopping")
633
+ break
634
+ except Exception:
635
+ if self._stopped.is_set():
636
+ break
637
+ logger.exception("[Telegram.td_listener] error processing update")
638
+
639
+ def _update_async_result(self, update: dict[Any, Any]) -> AsyncResult | None:
640
+ async_result = None
641
+
642
+ _special_types = ("updateAuthorizationState",) # for authorizationProcess @extra.request_id doesn't work
643
+
644
+ if update.get("@type") in _special_types:
645
+ request_id = update["@type"]
646
+ else:
647
+ request_id = update.get("@extra", {}).get("request_id")
648
+
649
+ if not request_id:
650
+ logger.debug("request_id has not been found in the update")
651
+ else:
652
+ async_result = self._results.get(request_id)
653
+
654
+ if not async_result:
655
+ logger.debug("async_result has not been found in by request_id=%s", request_id)
656
+ else:
657
+ done = async_result.parse_update(update)
658
+
659
+ if done:
660
+ self._results.pop(request_id, None)
661
+
662
+ return async_result
663
+
664
+ def _run_handlers(self, update: dict[Any, Any]) -> None:
665
+ update_type: str = update.get("@type", "unknown")
666
+
667
+ for handler in self._update_handlers[update_type]:
668
+ try:
669
+ self._workers_queue.put((handler, update), timeout=self._queue_put_timeout)
670
+ except queue.Full:
671
+ logger.error("Handler queue full, dropping update %s for handler %s", update_type, handler)
672
+
673
+ def remove_update_handler(self, handler_type: str, func: Callable) -> None:
674
+ """
675
+ Remove a handler with the specified type
676
+ """
677
+ try:
678
+ self._update_handlers[handler_type].remove(func)
679
+ except (ValueError, KeyError):
680
+ # not in the list
681
+ pass
682
+
683
+ def add_message_handler(self, func: Callable) -> None:
684
+ self.add_update_handler(MESSAGE_HANDLER_TYPE, func)
685
+
686
+ def add_update_handler(self, handler_type: str, func: Callable) -> None:
687
+ if func not in self._update_handlers[handler_type]:
688
+ self._update_handlers[handler_type].append(func)
689
+
690
+ def _send_data(
691
+ self,
692
+ data: dict[Any, Any],
693
+ result_id: str | None = None,
694
+ block: bool = False,
695
+ ) -> AsyncResult:
696
+ """
697
+ Sends data to tdlib.
698
+
699
+ If `block`is True, waits for the result
700
+ """
701
+
702
+ if "@extra" not in data:
703
+ data["@extra"] = {}
704
+
705
+ if not result_id and "request_id" in data["@extra"]:
706
+ result_id = data["@extra"]["request_id"]
707
+
708
+ if result_id:
709
+ pending = self._results.get(result_id)
710
+
711
+ if pending is not None and not pending._ready.is_set():
712
+ # Overwriting the entry would leave `pending` unreachable from
713
+ # `_update_async_result`, so nothing would ever resolve it and
714
+ # anyone waiting on it would block forever.
715
+ raise RuntimeError(
716
+ f"A request with id={result_id} is already in flight. "
717
+ "Authorization calls share a fixed request id, so they cannot be made concurrently."
718
+ )
719
+
720
+ async_result = AsyncResult(client=self, result_id=result_id)
721
+ data["@extra"]["request_id"] = async_result.id
722
+ self._results[async_result.id] = async_result
723
+ self._tdjson.send(data)
724
+ async_result.request = data
725
+
726
+ if block:
727
+ async_result.wait(raise_exc=True)
728
+
729
+ return async_result
730
+
731
+ def idle(
732
+ self,
733
+ stop_signals: tuple = (
734
+ signal.SIGINT,
735
+ signal.SIGTERM,
736
+ signal.SIGABRT,
737
+ ),
738
+ ) -> None:
739
+ """
740
+ Blocks until one of the exit signals is received.
741
+ When a signal is received, calls `stop`.
742
+ """
743
+
744
+ for sig in stop_signals:
745
+ signal.signal(sig, self._stop_signal_handler)
746
+
747
+ self._stopped.wait()
748
+
749
+ def _stop_signal_handler(self, signum: int, frame: FrameType | None = None) -> None:
750
+ logger.info("Signal %s received!", signum)
751
+ self.stop()
752
+
753
+ def get_authorization_state(self) -> AsyncResult:
754
+ logger.debug("Getting authorization state")
755
+ data = {"@type": "getAuthorizationState"}
756
+
757
+ return self._send_data(data, result_id="getAuthorizationState")
758
+
759
+ def _wait_authorization_result(self, result: AsyncResult, timeout: float | None = None) -> AuthorizationState:
760
+ authorization_state = None
761
+
762
+ if result:
763
+ result.wait(timeout=timeout, raise_exc=True)
764
+
765
+ if result.update is None:
766
+ raise RuntimeError("Something wrong, the result update is None")
767
+
768
+ if result.id == "getAuthorizationState":
769
+ authorization_state = result.update["@type"]
770
+ else:
771
+ authorization_state = result.update["authorization_state"]["@type"]
772
+
773
+ return AuthorizationState(authorization_state)
774
+
775
+ def login(self, blocking: bool = True) -> AuthorizationState:
776
+ """
777
+ Login process.
778
+
779
+ Must be called before any other call.
780
+ It sends initial params to the tdlib, sets database encryption key, etc.
781
+
782
+ args:
783
+ blocking [bool]: If True, the process is blocking and the client
784
+ expects password and code from stdin.
785
+ If False, `login` call returns next AuthorizationState and
786
+ the login process can be continued (with calling login(blocking=False) again)
787
+ after the necessary action is completed.
788
+
789
+ Returns:
790
+ - AuthorizationState.WAIT_CODE if a telegram code is required.
791
+ The caller should ask the telegram code
792
+ to the end user then call send_code(code)
793
+ - AuthorizationState.WAIT_EMAIL_ADDRESS if an email address is required.
794
+ The caller should ask the email address
795
+ to the end user and then call send_email_address(email_address)
796
+ - AuthorizationState.WAIT_EMAIL_CODE if an email code is required.
797
+ The caller should ask the code sent to the email address
798
+ to the end user and then call send_email_code(code)
799
+ - AuthorizationState.WAIT_PASSWORD if a telegram password is required.
800
+ The caller should ask the telegram password
801
+ to the end user and then call send_password(password)
802
+ - AuthorizationState.WAIT_REGISTRATION if a the user must finish registration
803
+ The caller should ask the first and last names
804
+ to the end user and then call register_user(first, last)
805
+ - AuthorizationState.READY if the login process succeeded.
806
+ """
807
+
808
+ if self.proxy_server:
809
+ self._send_add_proxy()
810
+
811
+ actions: dict[AuthorizationState, Callable[[], AsyncResult]] = {
812
+ AuthorizationState.NONE: self.get_authorization_state,
813
+ AuthorizationState.WAIT_TDLIB_PARAMETERS: self._set_initial_params,
814
+ AuthorizationState.WAIT_ENCRYPTION_KEY: self._send_encryption_key,
815
+ AuthorizationState.WAIT_PHONE_NUMBER: self._send_phone_number_or_bot_token,
816
+ AuthorizationState.WAIT_EMAIL_ADDRESS: self._send_email_address,
817
+ AuthorizationState.WAIT_EMAIL_CODE: self._send_email_code,
818
+ AuthorizationState.WAIT_CODE: self._send_telegram_code,
819
+ AuthorizationState.WAIT_PASSWORD: self._send_password,
820
+ AuthorizationState.WAIT_REGISTRATION: self._register_user,
821
+ }
822
+
823
+ blocking_actions = (
824
+ AuthorizationState.WAIT_CODE,
825
+ AuthorizationState.WAIT_EMAIL_ADDRESS,
826
+ AuthorizationState.WAIT_EMAIL_CODE,
827
+ AuthorizationState.WAIT_PASSWORD,
828
+ AuthorizationState.WAIT_REGISTRATION,
829
+ )
830
+
831
+ if self.phone:
832
+ logger.info("[login] Login process has been started with phone")
833
+ else:
834
+ logger.info("[login] Login process has been started with bot token")
835
+
836
+ while self.authorization_state != AuthorizationState.READY:
837
+ logger.info("[login] current authorization state: %s", self.authorization_state)
838
+
839
+ if not blocking and self.authorization_state in blocking_actions:
840
+ return self.authorization_state
841
+
842
+ action = actions.get(self.authorization_state)
843
+
844
+ if action is None:
845
+ raise RuntimeError(
846
+ f"Can not continue the login process from the authorization state {self.authorization_state}"
847
+ )
848
+
849
+ result = action()
850
+
851
+ if not isinstance(result, AuthorizationState):
852
+ self.authorization_state = self._wait_authorization_result(result)
853
+ else:
854
+ self.authorization_state = result
855
+
856
+ return self.authorization_state
857
+
858
+ def _set_initial_params(self) -> AsyncResult:
859
+ logger.info(
860
+ "Setting tdlib initial params: files_dir=%s, test_dc=%s",
861
+ self.files_directory,
862
+ self.use_test_dc,
863
+ )
864
+
865
+ parameters = {
866
+ "use_test_dc": self.use_test_dc,
867
+ "api_id": self.api_id,
868
+ "api_hash": self.api_hash,
869
+ "device_model": self.device_model,
870
+ "system_version": self.system_version,
871
+ "application_version": self.application_version,
872
+ "system_language_code": self.system_language_code,
873
+ "database_directory": str(self.files_directory / "database"),
874
+ "use_message_database": self.use_message_database,
875
+ "files_directory": str(self.files_directory / "files"),
876
+ "use_secret_chats": self.use_secret_chats,
877
+ }
878
+ data: dict[str, typing.Any] = {
879
+ "@type": "setTdlibParameters",
880
+ # since tdlib 1.8.6
881
+ "database_encryption_key": self._database_encryption_key,
882
+ **parameters,
883
+ }
884
+
885
+ return self._send_data(data, result_id="updateAuthorizationState")
886
+
887
+ def _send_encryption_key(self) -> AsyncResult:
888
+ logger.info("Sending encryption key")
889
+
890
+ data = {
891
+ "@type": "checkDatabaseEncryptionKey",
892
+ "encryption_key": self._database_encryption_key,
893
+ }
894
+
895
+ return self._send_data(data, result_id="updateAuthorizationState")
896
+
897
+ def _send_phone_number_or_bot_token(self) -> AsyncResult:
898
+ """Sends phone number or a bot_token"""
899
+
900
+ if self.phone:
901
+ return self._send_phone_number()
902
+ elif self.bot_token:
903
+ return self._send_bot_token()
904
+ else:
905
+ raise RuntimeError("Unknown mode: both bot_token and phone are None")
906
+
907
+ def _send_phone_number(self) -> AsyncResult:
908
+ logger.info("Sending phone number")
909
+ data = {
910
+ "@type": "setAuthenticationPhoneNumber",
911
+ "phone_number": self.phone,
912
+ "settings": {
913
+ "@type": "phoneNumberAuthenticationSettings",
914
+ "allow_flash_call": False,
915
+ "is_current_phone_number": True,
916
+ },
917
+ }
918
+
919
+ return self._send_data(data, result_id="updateAuthorizationState")
920
+
921
+ def _send_add_proxy(self) -> AsyncResult:
922
+ logger.info("Sending addProxy")
923
+ data = {
924
+ "@type": "addProxy",
925
+ "proxy": {
926
+ "@type": "proxy",
927
+ "server": self.proxy_server,
928
+ "port": self.proxy_port,
929
+ "type": self.proxy_type,
930
+ },
931
+ "enable": True,
932
+ }
933
+
934
+ # no fixed result_id: the result is never awaited, and `login` may send
935
+ # this more than once while a previous addProxy is still in flight
936
+ return self._send_data(data)
937
+
938
+ def _send_bot_token(self) -> AsyncResult:
939
+ logger.info("Sending bot token")
940
+ data = {"@type": "checkAuthenticationBotToken", "token": self.bot_token}
941
+
942
+ return self._send_data(data, result_id="updateAuthorizationState")
943
+
944
+ def _send_email_address(self, email_address: str | None = None) -> AsyncResult:
945
+ logger.info("Sending email address")
946
+
947
+ if email_address is None:
948
+ email_address = input("Enter email address:")
949
+ data = {
950
+ "@type": "setAuthenticationEmailAddress",
951
+ "email_address": email_address,
952
+ }
953
+
954
+ return self._send_data(data, result_id="updateAuthorizationState")
955
+
956
+ def send_email_address(self, email_address: str) -> AuthorizationState:
957
+ """
958
+ Sets the email address of the user and continues the authorization process
959
+
960
+ Args:
961
+ email_address: the email address of the user.
962
+ If email_address is None, it will be asked to the user using the input() function
963
+
964
+ Returns
965
+ - AuthorizationState. The caller has to call `login` to continue the login process.
966
+
967
+ Raises:
968
+ - RuntimeError if the login failed
969
+ """
970
+ result = self._send_email_address(email_address)
971
+ self.authorization_state = self._wait_authorization_result(result)
972
+
973
+ return self.authorization_state
974
+
975
+ def _send_email_code(self, code: str | None = None) -> AsyncResult:
976
+ logger.info("Sending email code")
977
+
978
+ if code is None:
979
+ code = input("Enter email code:")
980
+ data = {
981
+ "@type": "checkAuthenticationEmailCode",
982
+ "code": {
983
+ "@type": "emailAddressAuthenticationCode",
984
+ "code": str(code),
985
+ },
986
+ }
987
+
988
+ return self._send_data(data, result_id="updateAuthorizationState")
989
+
990
+ def send_email_code(self, code: str) -> AuthorizationState:
991
+ """
992
+ Verifies the code sent to the email address and continues the authorization process
993
+
994
+ Args:
995
+ code: the code to be verified.
996
+ If code is None, it will be asked to the user using the input() function
997
+
998
+ Returns
999
+ - AuthorizationState. The caller has to call `login` to continue the login process.
1000
+
1001
+ Raises:
1002
+ - RuntimeError if the login failed
1003
+ """
1004
+ result = self._send_email_code(code)
1005
+ self.authorization_state = self._wait_authorization_result(result)
1006
+
1007
+ return self.authorization_state
1008
+
1009
+ def _send_telegram_code(self, code: str | None = None) -> AsyncResult:
1010
+ logger.info("Sending code")
1011
+
1012
+ if code is None:
1013
+ code = input("Enter code:")
1014
+ data = {"@type": "checkAuthenticationCode", "code": str(code)}
1015
+
1016
+ return self._send_data(data, result_id="updateAuthorizationState")
1017
+
1018
+ def send_code(self, code: str) -> AuthorizationState:
1019
+ """
1020
+ Verifies a telegram code and continues the authorization process
1021
+
1022
+ Args:
1023
+ code: the code to be verified. If code is None, it will be asked to the user using the input() function
1024
+
1025
+ Returns
1026
+ - AuthorizationState. The called have to call `login` to continue the login process.
1027
+
1028
+ Raises:
1029
+ - RuntimeError if the login failed
1030
+ """
1031
+ result = self._send_telegram_code(code)
1032
+ self.authorization_state = self._wait_authorization_result(result)
1033
+
1034
+ return self.authorization_state
1035
+
1036
+ def _send_password(self, password: str | None = None) -> AsyncResult:
1037
+ logger.info("Sending password")
1038
+
1039
+ if password is None:
1040
+ password = getpass.getpass("Password:")
1041
+ data = {"@type": "checkAuthenticationPassword", "password": password}
1042
+
1043
+ return self._send_data(data, result_id="updateAuthorizationState")
1044
+
1045
+ def send_password(self, password: str) -> AuthorizationState:
1046
+ """
1047
+ Verifies a telegram password and continues the authorization process
1048
+
1049
+ Args:
1050
+ password the password to be verified.
1051
+ If password is None, it will be asked to the user using the getpass.getpass() function
1052
+
1053
+ Returns
1054
+ - AuthorizationState. The called have to call `login` to continue the login process.
1055
+
1056
+ Raises:
1057
+ - RuntimeError if the login failed
1058
+
1059
+ """
1060
+ result = self._send_password(password)
1061
+ self.authorization_state = self._wait_authorization_result(result)
1062
+
1063
+ return self.authorization_state
1064
+
1065
+ def _register_user(self, first: str | None = None, last: str | None = None) -> AsyncResult:
1066
+ logger.info("Registering user")
1067
+
1068
+ if first is None:
1069
+ first = input("Enter first name: ")
1070
+
1071
+ if last is None:
1072
+ last = input("Enter last name: ")
1073
+
1074
+ data = {
1075
+ "@type": "registerUser",
1076
+ "first_name": first,
1077
+ "last_name": last,
1078
+ }
1079
+
1080
+ return self._send_data(data, result_id="updateAuthorizationState")
1081
+
1082
+ def register_user(self, first: str, last: str) -> AuthorizationState:
1083
+ """
1084
+ Finishes the new user registration process
1085
+
1086
+ Args:
1087
+ first the user's first name
1088
+ last the user's last name
1089
+ If either argument is None, it will be asked to the user using the input() function
1090
+
1091
+ Returns
1092
+ - AuthorizationState. The called have to call `login` to continue the login process.
1093
+
1094
+ Raises:
1095
+ - RuntimeError if the login failed
1096
+
1097
+ """
1098
+ result = self._register_user(first, last)
1099
+ self.authorization_state = self._wait_authorization_result(result)
1100
+
1101
+ return self.authorization_state