diff --git a/examples/README.md b/examples/README.md
index 6f640ef4..545516fa 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -8,6 +8,7 @@ you have to change are the target chats (username, id) and file paths for sendin
- [**hello_world.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py)
- [**get_history.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_history.py)
- [**get_participants.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants.py)
+- [**get_participants2.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants2.py)
- [**inline_bots.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/inline_bots.py)
- [**updates.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/updates.py)
- [**simple_echo.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/simple_echo.py)
diff --git a/examples/get_participants2.py b/examples/get_participants2.py
new file mode 100644
index 00000000..23ed328f
--- /dev/null
+++ b/examples/get_participants2.py
@@ -0,0 +1,63 @@
+import time
+from string import ascii_lowercase
+
+from pyrogram import Client
+from pyrogram.api import functions, types
+from pyrogram.api.errors import FloodWait
+
+"""
+This is an improved version of get_participants.py
+
+Since Telegram will return at most 10.000 users for a single query, this script
+repeats the search using numbers ("0" to "9") and all the available ascii letters ("a" to "z").
+
+This can be further improved by also searching for non-ascii characters (e.g.: Japanese script),
+as some user names may not contain ascii letters at all.
+"""
+
+client = Client("example")
+client.start()
+
+target = "username" # Target channel/supergroup username or id
+users = {} # To ensure uniqueness, users will be stored in a dictionary with user_id as key
+limit = 200 # Amount of users to retrieve for each API call (200 is the maximum)
+
+# "" + "0123456789" + "abcdefghijklmnopqrstuvwxyz" (as list)
+queries = [""] + [str(i) for i in range(10)] + list(ascii_lowercase)
+
+for q in queries:
+ print("Searching for '{}'".format(q))
+ offset = 0 # For each query, offset restarts from 0
+
+ while True:
+ try:
+ participants = client.send(
+ functions.channels.GetParticipants(
+ channel=client.resolve_peer(target),
+ filter=types.ChannelParticipantsSearch(q),
+ offset=offset,
+ limit=limit,
+ hash=0
+ )
+ )
+ except FloodWait as e:
+ # Very large chats could trigger FloodWait.
+ # When happens, wait X seconds before continuing
+ print("Flood wait: {} seconds".format(e.x))
+ time.sleep(e.x)
+ continue
+
+ if not participants.participants:
+ print("Done searching for '{}'".format(q))
+ print()
+ break # No more participants left
+
+ # User information are stored in the participants.users list.
+ # Add those users to the dictionary
+ users.update({i.id: i for i in participants.users})
+
+ offset += len(participants.participants)
+
+ print("Total users: {}".format(len(users)))
+
+client.stop()
diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py
index b2b85d0c..084cce6e 100644
--- a/pyrogram/__init__.py
+++ b/pyrogram/__init__.py
@@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès .
import base64
+import binascii
import json
import logging
import math
import mimetypes
import os
import re
+import struct
import threading
import time
from collections import namedtuple
@@ -41,10 +43,9 @@ from pyrogram.api.errors import (
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned,
- VolumeLocNotFound)
+ VolumeLocNotFound, UserMigrate)
from pyrogram.api.types import (
User, Chat, Channel,
- PeerUser, PeerChannel,
InputPeerEmpty, InputPeerSelf,
InputPeerUser, InputPeerChat, InputPeerChannel
)
@@ -88,6 +89,10 @@ class Client:
Only applicable for new sessions and will be ignored in case previously
created sessions are loaded.
+ token (:obj:`str`, optional):
+ Pass your Bot API token to log-in as Bot.
+ E.g.: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
+
phone_number (:obj:`str`, optional):
Pass your phone number (with your Country Code prefix included) to avoid
entering it manually. Only applicable for new sessions.
@@ -113,7 +118,7 @@ class Client:
Thread pool size for handling incoming updates. Defaults to 4.
"""
- INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$")
+ INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:t\.me/joinchat/)?([\w-]+)$")
DIALOGS_AT_ONCE = 100
UPDATES_WORKERS = 2
DOWNLOAD_WORKERS = 1
@@ -123,6 +128,7 @@ class Client:
api_key: tuple or ApiKey = None,
proxy: dict or Proxy = None,
test_mode: bool = False,
+ token: str = None,
phone_number: str = None,
phone_code: str or callable = None,
password: str = None,
@@ -134,6 +140,7 @@ class Client:
self.proxy = proxy
self.test_mode = test_mode
+ self.token = token
self.phone_number = phone_number
self.password = password
self.phone_code = phone_code
@@ -146,7 +153,7 @@ class Client:
self.auth_key = None
self.user_id = None
- self.rnd_id = None
+ self.rnd_id = MsgId
self.peers_by_id = {}
self.peers_by_username = {}
@@ -159,6 +166,7 @@ class Client:
self.session = None
+ self.is_started = None
self.is_idle = None
self.updates_queue = Queue()
@@ -186,18 +194,22 @@ class Client:
client=self
)
- terms = self.session.start()
+ self.session.start()
+ self.is_started = True
if self.user_id is None:
- print("\n".join(terms.splitlines()), "\n")
+ if self.token is None:
+ self.authorize_user()
+ else:
+ self.authorize_bot()
- self.user_id = self.authorize()
- self.password = None
self.save_session()
- self.rnd_id = MsgId
- self.get_dialogs()
- self.get_contacts()
+ if self.token is None:
+ self.get_dialogs()
+ self.get_contacts()
+ else:
+ self.send(functions.updates.GetState())
for i in range(self.UPDATES_WORKERS):
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
@@ -214,6 +226,7 @@ class Client:
"""Use this method to manually stop the Client.
Requires no parameters.
"""
+ self.is_started = False
self.session.stop()
for _ in range(self.UPDATES_WORKERS):
@@ -225,340 +238,37 @@ class Client:
for _ in range(self.DOWNLOAD_WORKERS):
self.download_queue.put(None)
- def fetch_peers(self, entities: list):
- for entity in entities:
- if isinstance(entity, User):
- user_id = entity.id
-
- if user_id in self.peers_by_id:
- continue
-
- access_hash = entity.access_hash
-
- if access_hash is None:
- continue
-
- username = entity.username
- phone = entity.phone
-
- input_peer = InputPeerUser(
- user_id=user_id,
- access_hash=access_hash
+ def authorize_bot(self):
+ try:
+ r = self.send(
+ functions.auth.ImportBotAuthorization(
+ flags=0,
+ api_id=self.api_key.api_id,
+ api_hash=self.api_key.api_hash,
+ bot_auth_token=self.token
)
-
- self.peers_by_id[user_id] = input_peer
-
- if username is not None:
- self.peers_by_username[username] = input_peer
-
- if phone is not None:
- self.peers_by_phone[phone] = input_peer
-
- if isinstance(entity, Chat):
- chat_id = entity.id
- peer_id = -chat_id
-
- if peer_id in self.peers_by_id:
- continue
-
- input_peer = InputPeerChat(
- chat_id=chat_id
- )
-
- self.peers_by_id[peer_id] = input_peer
-
- if isinstance(entity, Channel):
- channel_id = entity.id
- peer_id = int("-100" + str(channel_id))
-
- if peer_id in self.peers_by_id:
- continue
-
- access_hash = entity.access_hash
-
- if access_hash is None:
- continue
-
- username = entity.username
-
- input_peer = InputPeerChannel(
- channel_id=channel_id,
- access_hash=access_hash
- )
-
- self.peers_by_id[peer_id] = input_peer
-
- if username is not None:
- self.peers_by_username[username] = input_peer
-
- def download_worker(self):
- name = threading.current_thread().name
- log.debug("{} started".format(name))
-
- while True:
- media = self.download_queue.get()
-
- if media is None:
- break
-
- try:
- media, file_name, done, progress, path = media
- tmp_file_name = None
-
- if isinstance(media, types.MessageMediaDocument):
- document = media.document
-
- if isinstance(document, types.Document):
- if not file_name:
- file_name = "doc_{}{}".format(
- datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"),
- ".txt" if document.mime_type == "text/plain" else
- mimetypes.guess_extension(document.mime_type) if document.mime_type else ".unknown"
- )
-
- for i in document.attributes:
- if isinstance(i, types.DocumentAttributeFilename):
- file_name = i.file_name
- break
- elif isinstance(i, types.DocumentAttributeSticker):
- file_name = file_name.replace("doc", "sticker")
- elif isinstance(i, types.DocumentAttributeAudio):
- file_name = file_name.replace("doc", "audio")
- elif isinstance(i, types.DocumentAttributeVideo):
- file_name = file_name.replace("doc", "video")
- elif isinstance(i, types.DocumentAttributeAnimated):
- file_name = file_name.replace("doc", "gif")
-
- tmp_file_name = self.get_file(
- dc_id=document.dc_id,
- id=document.id,
- access_hash=document.access_hash,
- version=document.version,
- size=document.size,
- progress=progress
- )
- elif isinstance(media, (types.MessageMediaPhoto, types.Photo)):
- if isinstance(media, types.MessageMediaPhoto):
- photo = media.photo
- else:
- photo = media
-
- if isinstance(photo, types.Photo):
- if not file_name:
- file_name = "photo_{}_{}.jpg".format(
- datetime.fromtimestamp(photo.date).strftime("%Y-%m-%d_%H-%M-%S"),
- self.rnd_id()
- )
-
- photo_loc = photo.sizes[-1].location
-
- tmp_file_name = self.get_file(
- dc_id=photo_loc.dc_id,
- volume_id=photo_loc.volume_id,
- local_id=photo_loc.local_id,
- secret=photo_loc.secret,
- size=photo.sizes[-1].size,
- progress=progress
- )
-
- if file_name is not None:
- path[0] = "downloads/{}".format(file_name)
-
- try:
- os.remove("downloads/{}".format(file_name))
- except OSError:
- pass
- finally:
- try:
- os.renames("{}".format(tmp_file_name), "downloads/{}".format(file_name))
- except OSError:
- pass
- except Exception as e:
- log.error(e, exc_info=True)
- finally:
- done.set()
-
- try:
- os.remove("{}".format(tmp_file_name))
- except OSError:
- pass
-
- log.debug("{} stopped".format(name))
-
- def updates_worker(self):
- name = threading.current_thread().name
- log.debug("{} started".format(name))
-
- while True:
- updates = self.updates_queue.get()
-
- if updates is None:
- break
-
- try:
- if isinstance(updates, (types.Update, types.UpdatesCombined)):
- self.fetch_peers(updates.users)
- self.fetch_peers(updates.chats)
-
- for update in updates.updates:
- channel_id = getattr(
- getattr(
- getattr(
- update, "message", None
- ), "to_id", None
- ), "channel_id", None
- ) or getattr(update, "channel_id", None)
-
- pts = getattr(update, "pts", None)
-
- if channel_id and pts:
- if channel_id not in self.channels_pts:
- self.channels_pts[channel_id] = []
-
- if pts in self.channels_pts[channel_id]:
- continue
-
- self.channels_pts[channel_id].append(pts)
-
- if len(self.channels_pts[channel_id]) > 50:
- self.channels_pts[channel_id] = self.channels_pts[channel_id][25:]
-
- self.update_queue.put((update, updates.users, updates.chats))
- elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)):
- diff = self.send(
- functions.updates.GetDifference(
- pts=updates.pts - updates.pts_count,
- date=updates.date,
- qts=-1
- )
- )
-
- self.fetch_peers(diff.users)
- self.fetch_peers(diff.chats)
-
- self.update_queue.put((
- types.UpdateNewMessage(
- message=diff.new_messages[0],
- pts=updates.pts,
- pts_count=updates.pts_count
- ),
- diff.users,
- diff.chats
- ))
- elif isinstance(updates, types.UpdateShort):
- self.update_queue.put((updates.update, [], []))
- except Exception as e:
- log.error(e, exc_info=True)
-
- log.debug("{} stopped".format(name))
-
- def update_worker(self):
- name = threading.current_thread().name
- log.debug("{} started".format(name))
-
- while True:
- update = self.update_queue.get()
-
- if update is None:
- break
-
- try:
- if self.update_handler:
- self.update_handler(
- self,
- update[0],
- {i.id: i for i in update[1]},
- {i.id: i for i in update[2]}
- )
- except Exception as e:
- log.error(e, exc_info=True)
-
- log.debug("{} stopped".format(name))
-
- def signal_handler(self, *args):
- self.stop()
- self.is_idle = False
-
- def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
- """Blocks the program execution until one of the signals are received,
- then gently stop the Client by closing the underlying connection.
-
- Args:
- stop_signals (:obj:`tuple`, optional):
- Iterable containing signals the signal handler will listen to.
- Defaults to (SIGINT, SIGTERM, SIGABRT).
- """
- for s in stop_signals:
- signal(s, self.signal_handler)
-
- self.is_idle = True
-
- while self.is_idle:
- time.sleep(1)
-
- def set_update_handler(self, callback: callable):
- """Use this method to set the update handler.
-
- You must call this method *before* you *start()* the Client.
-
- Args:
- callback (:obj:`callable`):
- A function that will be called when a new update is received from the server. It takes
- :obj:`(client, update, users, chats)` as positional arguments (Look at the section below for
- a detailed description).
-
- Other Parameters:
- client (:obj:`pyrogram.Client`):
- The Client itself, useful when you want to call other API methods inside the update handler.
-
- update (:obj:`Update`):
- The received update, which can be one of the many single Updates listed in the *updates*
- field you see in the :obj:`Update ` type.
-
- users (:obj:`dict`):
- Dictionary of all :obj:`User ` mentioned in the update.
- You can access extra info about the user (such as *first_name*, *last_name*, etc...) by using
- the IDs you find in the *update* argument (e.g.: *users[1768841572]*).
-
- chats (:obj:`dict`):
- Dictionary of all :obj:`Chat ` and
- :obj:`Channel ` mentioned in the update.
- You can access extra info about the chat (such as *title*, *participants_count*, etc...)
- by using the IDs you find in the *update* argument (e.g.: *chats[1701277281]*).
-
- Note:
- The following Empty or Forbidden types may exist inside the *users* and *chats* dictionaries.
- They mean you have been blocked by the user or banned from the group/channel.
-
- - :obj:`UserEmpty `
- - :obj:`ChatEmpty `
- - :obj:`ChatForbidden `
- - :obj:`ChannelForbidden `
- """
- self.update_handler = callback
-
- def send(self, data: Object):
- """Use this method to send Raw Function queries.
-
- This method makes possible to manually call every single Telegram API method in a low-level manner.
- Available functions are listed in the :obj:`pyrogram.api.functions` package and may accept compound
- data types from :obj:`pyrogram.api.types` as well as bare types such as :obj:`int`, :obj:`str`, etc...
-
- Args:
- data (:obj:`Object`):
- The API Scheme function filled with proper arguments.
-
- Raises:
- :class:`pyrogram.Error`
- """
- r = self.session.send(data)
-
- self.fetch_peers(getattr(r, "users", []))
- self.fetch_peers(getattr(r, "chats", []))
-
- return r
-
- def authorize(self):
+ )
+ except UserMigrate as e:
+ self.session.stop()
+
+ self.dc_id = e.x
+ self.auth_key = Auth(self.dc_id, self.test_mode, self.proxy).create()
+
+ self.session = Session(
+ self.dc_id,
+ self.test_mode,
+ self.proxy,
+ self.auth_key,
+ self.api_key.api_id,
+ client=self
+ )
+
+ self.session.start()
+ self.authorize_bot()
+ else:
+ self.user_id = r.user.id
+
+ def authorize_user(self):
phone_number_invalid_raises = self.phone_number is not None
phone_code_invalid_raises = self.phone_code is not None
password_hash_invalid_raises = self.password is not None
@@ -718,7 +428,341 @@ class Client:
else:
break
- return r.user.id
+ self.password = None
+ self.user_id = r.user.id
+
+ def fetch_peers(self, entities: list):
+ for entity in entities:
+ if isinstance(entity, User):
+ user_id = entity.id
+
+ if user_id in self.peers_by_id:
+ continue
+
+ access_hash = entity.access_hash
+
+ if access_hash is None:
+ continue
+
+ username = entity.username
+ phone = entity.phone
+
+ input_peer = InputPeerUser(
+ user_id=user_id,
+ access_hash=access_hash
+ )
+
+ self.peers_by_id[user_id] = input_peer
+
+ if username is not None:
+ self.peers_by_username[username.lower()] = input_peer
+
+ if phone is not None:
+ self.peers_by_phone[phone] = input_peer
+
+ if isinstance(entity, Chat):
+ chat_id = entity.id
+ peer_id = -chat_id
+
+ if peer_id in self.peers_by_id:
+ continue
+
+ input_peer = InputPeerChat(
+ chat_id=chat_id
+ )
+
+ self.peers_by_id[peer_id] = input_peer
+
+ if isinstance(entity, Channel):
+ channel_id = entity.id
+ peer_id = int("-100" + str(channel_id))
+
+ if peer_id in self.peers_by_id:
+ continue
+
+ access_hash = entity.access_hash
+
+ if access_hash is None:
+ continue
+
+ username = entity.username
+
+ input_peer = InputPeerChannel(
+ channel_id=channel_id,
+ access_hash=access_hash
+ )
+
+ self.peers_by_id[peer_id] = input_peer
+
+ if username is not None:
+ self.peers_by_username[username.lower()] = input_peer
+
+ def download_worker(self):
+ name = threading.current_thread().name
+ log.debug("{} started".format(name))
+
+ while True:
+ media = self.download_queue.get()
+
+ if media is None:
+ break
+
+ try:
+ media, file_name, done, progress, path = media
+ tmp_file_name = None
+
+ if isinstance(media, types.MessageMediaDocument):
+ document = media.document
+
+ if isinstance(document, types.Document):
+ if not file_name:
+ file_name = "doc_{}{}".format(
+ datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"),
+ ".txt" if document.mime_type == "text/plain" else
+ mimetypes.guess_extension(document.mime_type) if document.mime_type else ".unknown"
+ )
+
+ for i in document.attributes:
+ if isinstance(i, types.DocumentAttributeFilename):
+ file_name = i.file_name
+ break
+ elif isinstance(i, types.DocumentAttributeSticker):
+ file_name = file_name.replace("doc", "sticker")
+ elif isinstance(i, types.DocumentAttributeAudio):
+ file_name = file_name.replace("doc", "audio")
+ elif isinstance(i, types.DocumentAttributeVideo):
+ file_name = file_name.replace("doc", "video")
+ elif isinstance(i, types.DocumentAttributeAnimated):
+ file_name = file_name.replace("doc", "gif")
+
+ tmp_file_name = self.get_file(
+ dc_id=document.dc_id,
+ id=document.id,
+ access_hash=document.access_hash,
+ version=document.version,
+ size=document.size,
+ progress=progress
+ )
+ elif isinstance(media, (types.MessageMediaPhoto, types.Photo)):
+ if isinstance(media, types.MessageMediaPhoto):
+ photo = media.photo
+ else:
+ photo = media
+
+ if isinstance(photo, types.Photo):
+ if not file_name:
+ file_name = "photo_{}_{}.jpg".format(
+ datetime.fromtimestamp(photo.date).strftime("%Y-%m-%d_%H-%M-%S"),
+ self.rnd_id()
+ )
+
+ photo_loc = photo.sizes[-1].location
+
+ tmp_file_name = self.get_file(
+ dc_id=photo_loc.dc_id,
+ volume_id=photo_loc.volume_id,
+ local_id=photo_loc.local_id,
+ secret=photo_loc.secret,
+ size=photo.sizes[-1].size,
+ progress=progress
+ )
+
+ if file_name is not None:
+ path[0] = "downloads/{}".format(file_name)
+
+ try:
+ os.remove("downloads/{}".format(file_name))
+ except OSError:
+ pass
+ finally:
+ try:
+ os.renames("{}".format(tmp_file_name), "downloads/{}".format(file_name))
+ except OSError:
+ pass
+ except Exception as e:
+ log.error(e, exc_info=True)
+ finally:
+ done.set()
+
+ try:
+ os.remove("{}".format(tmp_file_name))
+ except OSError:
+ pass
+
+ log.debug("{} stopped".format(name))
+
+ def updates_worker(self):
+ name = threading.current_thread().name
+ log.debug("{} started".format(name))
+
+ while True:
+ updates = self.updates_queue.get()
+
+ if updates is None:
+ break
+
+ try:
+ if isinstance(updates, (types.Update, types.UpdatesCombined)):
+ self.fetch_peers(updates.users)
+ self.fetch_peers(updates.chats)
+
+ for update in updates.updates:
+ channel_id = getattr(
+ getattr(
+ getattr(
+ update, "message", None
+ ), "to_id", None
+ ), "channel_id", None
+ ) or getattr(update, "channel_id", None)
+
+ pts = getattr(update, "pts", None)
+
+ if channel_id and pts:
+ if channel_id not in self.channels_pts:
+ self.channels_pts[channel_id] = []
+
+ if pts in self.channels_pts[channel_id]:
+ continue
+
+ self.channels_pts[channel_id].append(pts)
+
+ if len(self.channels_pts[channel_id]) > 50:
+ self.channels_pts[channel_id] = self.channels_pts[channel_id][25:]
+
+ self.update_queue.put((update, updates.users, updates.chats))
+ elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)):
+ diff = self.send(
+ functions.updates.GetDifference(
+ pts=updates.pts - updates.pts_count,
+ date=updates.date,
+ qts=-1
+ )
+ )
+
+ self.update_queue.put((
+ types.UpdateNewMessage(
+ message=diff.new_messages[0],
+ pts=updates.pts,
+ pts_count=updates.pts_count
+ ),
+ diff.users,
+ diff.chats
+ ))
+ elif isinstance(updates, types.UpdateShort):
+ self.update_queue.put((updates.update, [], []))
+ except Exception as e:
+ log.error(e, exc_info=True)
+
+ log.debug("{} stopped".format(name))
+
+ def update_worker(self):
+ name = threading.current_thread().name
+ log.debug("{} started".format(name))
+
+ while True:
+ update = self.update_queue.get()
+
+ if update is None:
+ break
+
+ try:
+ if self.update_handler:
+ self.update_handler(
+ self,
+ update[0],
+ {i.id: i for i in update[1]},
+ {i.id: i for i in update[2]}
+ )
+ except Exception as e:
+ log.error(e, exc_info=True)
+
+ log.debug("{} stopped".format(name))
+
+ def signal_handler(self, *args):
+ self.stop()
+ self.is_idle = False
+
+ def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
+ """Blocks the program execution until one of the signals are received,
+ then gently stop the Client by closing the underlying connection.
+
+ Args:
+ stop_signals (:obj:`tuple`, optional):
+ Iterable containing signals the signal handler will listen to.
+ Defaults to (SIGINT, SIGTERM, SIGABRT).
+ """
+ for s in stop_signals:
+ signal(s, self.signal_handler)
+
+ self.is_idle = True
+
+ while self.is_idle:
+ time.sleep(1)
+
+ def set_update_handler(self, callback: callable):
+ """Use this method to set the update handler.
+
+ You must call this method *before* you *start()* the Client.
+
+ Args:
+ callback (:obj:`callable`):
+ A function that will be called when a new update is received from the server. It takes
+ :obj:`(client, update, users, chats)` as positional arguments (Look at the section below for
+ a detailed description).
+
+ Other Parameters:
+ client (:obj:`pyrogram.Client`):
+ The Client itself, useful when you want to call other API methods inside the update handler.
+
+ update (:obj:`Update`):
+ The received update, which can be one of the many single Updates listed in the *updates*
+ field you see in the :obj:`Update ` type.
+
+ users (:obj:`dict`):
+ Dictionary of all :obj:`User ` mentioned in the update.
+ You can access extra info about the user (such as *first_name*, *last_name*, etc...) by using
+ the IDs you find in the *update* argument (e.g.: *users[1768841572]*).
+
+ chats (:obj:`dict`):
+ Dictionary of all :obj:`Chat ` and
+ :obj:`Channel ` mentioned in the update.
+ You can access extra info about the chat (such as *title*, *participants_count*, etc...)
+ by using the IDs you find in the *update* argument (e.g.: *chats[1701277281]*).
+
+ Note:
+ The following Empty or Forbidden types may exist inside the *users* and *chats* dictionaries.
+ They mean you have been blocked by the user or banned from the group/channel.
+
+ - :obj:`UserEmpty `
+ - :obj:`ChatEmpty `
+ - :obj:`ChatForbidden `
+ - :obj:`ChannelForbidden `
+ """
+ self.update_handler = callback
+
+ def send(self, data: Object):
+ """Use this method to send Raw Function queries.
+
+ This method makes possible to manually call every single Telegram API method in a low-level manner.
+ Available functions are listed in the :obj:`pyrogram.api.functions` package and may accept compound
+ data types from :obj:`pyrogram.api.types` as well as bare types such as :obj:`int`, :obj:`str`, etc...
+
+ Args:
+ data (:obj:`Object`):
+ The API Scheme function filled with proper arguments.
+
+ Raises:
+ :class:`pyrogram.Error`
+ """
+ if self.is_started:
+ r = self.session.send(data)
+
+ self.fetch_peers(getattr(r, "users", []))
+ self.fetch_peers(getattr(r, "chats", []))
+
+ return r
+ else:
+ raise ConnectionError("client '{}' is not started".format(self.session_name))
def load_config(self):
parser = ConfigParser()
@@ -821,35 +865,6 @@ class Client:
offset_date = parse_dialogs(dialogs)
log.info("Entities count: {}".format(len(self.peers_by_id)))
- def resolve_username(self, username: str):
- username = username.lower().strip("@")
-
- resolved_peer = self.send(
- functions.contacts.ResolveUsername(
- username=username
- )
- ) # type: types.contacts.ResolvedPeer
-
- if type(resolved_peer.peer) is PeerUser:
- input_peer = InputPeerUser(
- user_id=resolved_peer.users[0].id,
- access_hash=resolved_peer.users[0].access_hash
- )
- peer_id = input_peer.user_id
- elif type(resolved_peer.peer) is PeerChannel:
- input_peer = InputPeerChannel(
- channel_id=resolved_peer.chats[0].id,
- access_hash=resolved_peer.chats[0].access_hash
- )
- peer_id = int("-100" + str(input_peer.channel_id))
- else:
- raise PeerIdInvalid
-
- self.peers_by_username[username] = input_peer
- self.peers_by_id[peer_id] = input_peer
-
- return input_peer
-
def resolve_peer(self, peer_id: int or str):
"""Use this method to get the *InputPeer* of a known *peer_id*.
@@ -874,6 +889,14 @@ class Client:
if peer_id in ("self", "me"):
return InputPeerSelf()
+ match = self.INVITE_LINK_RE.match(peer_id)
+
+ try:
+ decoded = base64.b64decode(match.group(1) + "=" * (-len(match.group(1)) % 4), "-_")
+ return self.resolve_peer(struct.unpack(">2iq", decoded)[1])
+ except (AttributeError, binascii.Error, struct.error):
+ pass
+
peer_id = peer_id.lower().strip("@+")
try:
@@ -882,7 +905,8 @@ class Client:
try:
return self.peers_by_username[peer_id]
except KeyError:
- return self.resolve_username(peer_id)
+ self.send(functions.contacts.ResolveUsername(peer_id))
+ return self.peers_by_username[peer_id]
else:
try:
return self.peers_by_phone[peer_id]
@@ -934,9 +958,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
text (:obj:`str`):
Text of the message to be sent.
@@ -984,15 +1009,16 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
from_chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the chat where the original message was sent
- (or channel/supergroup username in the format @username). For your personal cloud
- storage (Saved Messages) you can simply use "me" or "self".
- Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the source chat where the original message was sent.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
message_ids (:obj:`list`):
A list of Message identifiers in the chat specified in *from_chat_id*.
@@ -1030,9 +1056,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
photo (:obj:`str`):
Photo to send.
@@ -1115,9 +1142,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
audio (:obj:`str`):
Audio file to send.
@@ -1207,9 +1235,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
document (:obj:`str`):
File to send.
@@ -1283,9 +1312,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
sticker (:obj:`str`):
Sticker to send.
@@ -1357,9 +1387,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
video (:obj:`str`):
Video to send.
@@ -1461,9 +1492,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
voice (:obj:`str`):
Audio file to send.
@@ -1545,9 +1577,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
video_note (:obj:`str`):
Video note to send.
@@ -1624,9 +1657,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
media (:obj:`list`):
A list containing either :obj:`pyrogram.InputMedia.Photo` or :obj:`pyrogram.InputMedia.Video` objects
@@ -1722,9 +1756,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
latitude (:obj:`float`):
Latitude of the location.
@@ -1774,9 +1809,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
latitude (:obj:`float`):
Latitude of the venue.
@@ -1838,9 +1874,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
phone_number (:obj:`str`):
Contact's phone number.
@@ -1887,9 +1924,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
action (:obj:`callable`):
Type of action to broadcast.
@@ -1949,9 +1987,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
message_id (:obj:`int`):
Message identifier in the chat specified in chat_id.
@@ -1990,9 +2029,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
message_id (:obj:`int`):
Message identifier in the chat specified in chat_id.
@@ -2032,9 +2072,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
message_ids (:obj:`list`):
List of identifiers of the messages to delete.
@@ -2771,9 +2812,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
query_id (:obj:`int`):
Unique identifier for the answered query.
@@ -2813,9 +2855,10 @@ class Client:
Args:
chat_id (:obj:`int` | :obj:`str`):
- Unique identifier for the target chat or username of the target channel/supergroup
- (in the format @username). For your personal cloud storage (Saved Messages) you can
- simply use "me" or "self". Phone numbers that exist in your Telegram address book are also supported.
+ Unique identifier (int) or username (str) of the target chat.
+ For your personal cloud (Saved Messages) you can simply use "me" or "self".
+ For a contact that exists in your Telegram address book you can use his phone number (str).
+ For a private channel/supergroup you can use its *t.me/joinchat/* link.
message_ids (:obj:`list`):
A list of Message identifiers in the chat specified in *chat_id*.
diff --git a/pyrogram/crypto/aes.py b/pyrogram/crypto/aes.py
index c571fb2d..05a01044 100644
--- a/pyrogram/crypto/aes.py
+++ b/pyrogram/crypto/aes.py
@@ -53,7 +53,7 @@ class AES:
@staticmethod
def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes:
- replace = int.to_bytes(offset // 16, byteorder="big", length=4)
+ replace = int.to_bytes(offset // 16, 4, "big")
iv = iv[:-4] + replace
if is_fast:
diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py
index 6225fe44..46d722fc 100644
--- a/pyrogram/session/session.py
+++ b/pyrogram/session/session.py
@@ -122,8 +122,6 @@ class Session:
self.is_connected = Event()
def start(self):
- terms = None
-
while True:
try:
self.connection.connect()
@@ -141,7 +139,7 @@ class Session:
self.next_salt_thread.start()
if not self.is_cdn:
- terms = self._send(
+ self._send(
functions.InvokeWithLayer(
layer,
functions.InitConnection(
@@ -150,10 +148,10 @@ class Session:
self.SYSTEM_VERSION,
self.APP_VERSION,
"en", "", "en",
- functions.help.GetTermsOfService(),
+ functions.help.GetConfig(),
)
)
- ).text
+ )
self.ping_thread = Thread(target=self.ping, name="PingThread")
self.ping_thread.start()
@@ -168,8 +166,6 @@ class Session:
log.debug("Session started")
- return terms
-
def stop(self):
self.is_connected.clear()
@@ -190,6 +186,9 @@ class Session:
for i in range(self.NET_WORKERS):
self.recv_queue.put(None)
+ for i in self.results.values():
+ i.event.set()
+
log.debug("Session stopped")
def restart(self):
@@ -401,7 +400,7 @@ class Session:
try:
return self._send(data)
except (OSError, TimeoutError):
- log.warning("Retrying {}".format(type(data)))
+ (log.warning if i > 0 else log.info)("{}: {} Retrying {}".format(i, datetime.now(), type(data)))
continue
else:
return None