Merge branch 'new-api' into new-api-docs

# Conflicts:
#	compiler/api/compiler.py
#	docs/source/pyrogram/index.rst
This commit is contained in:
Dan 2018-04-11 03:29:47 +02:00
commit 5e5289596b
27 changed files with 1672 additions and 147 deletions

View File

@ -25,7 +25,7 @@ DESTINATION = "pyrogram/api"
NOTICE_PATH = "NOTICE" NOTICE_PATH = "NOTICE"
SECTION_RE = re.compile(r"---(\w+)---") SECTION_RE = re.compile(r"---(\w+)---")
LAYER_RE = re.compile(r"//\sLAYER\s(\d+)") LAYER_RE = re.compile(r"//\sLAYER\s(\d+)")
COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);$", re.MULTILINE) COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);(?: // Docs: (.+))?$", re.MULTILINE)
ARGS_RE = re.compile("[^{](\w+):([\w?!.<>]+)") ARGS_RE = re.compile("[^{](\w+):([\w?!.<>]+)")
FLAGS_RE = re.compile(r"flags\.(\d+)\?") FLAGS_RE = re.compile(r"flags\.(\d+)\?")
FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)") FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)")
@ -38,7 +38,7 @@ types_to_functions = {}
constructors_to_functions = {} constructors_to_functions = {}
def get_docstring_arg_type(t: str, is_list: bool = False): def get_docstring_arg_type(t: str, is_list: bool = False, is_pyrogram_type: bool = False):
if t in core_types: if t in core_types:
if t == "long": if t == "long":
return "``int`` ``64-bit``" return "``int`` ``64-bit``"
@ -60,11 +60,17 @@ def get_docstring_arg_type(t: str, is_list: bool = False):
elif t.startswith("Vector"): elif t.startswith("Vector"):
return "List of " + get_docstring_arg_type(t.split("<")[1][:-1], is_list=True) return "List of " + get_docstring_arg_type(t.split("<")[1][:-1], is_list=True)
else: else:
if is_pyrogram_type:
t = "pyrogram." + t
t = types_to_constructors.get(t, [t]) t = types_to_constructors.get(t, [t])
n = len(t) - 1 n = len(t) - 1
t = (("e" if is_list else "E") + "ither " if n else "") + ", ".join( t = (("e" if is_list else "E") + "ither " if n else "") + ", ".join(
":obj:`{0} <pyrogram.api.types.{0}>`".format(i) ":obj:`{1} <pyrogram.api.types.{0}{1}>`".format(
"pyrogram." if is_pyrogram_type else "",
i.lstrip("pyrogram.")
)
for i in t for i in t
) )
@ -94,7 +100,15 @@ def get_references(t: str):
class Combinator: class Combinator:
def __init__(self, section: str, namespace: str, name: str, id: str, args: list, has_flags: bool, return_type: str): def __init__(self,
section: str,
namespace: str,
name: str,
id: str,
args: list,
has_flags: bool,
return_type: str,
docs: str):
self.section = section self.section = section
self.namespace = namespace self.namespace = namespace
self.name = name self.name = name
@ -102,6 +116,7 @@ class Combinator:
self.args = args self.args = args
self.has_flags = has_flags self.has_flags = has_flags
self.return_type = return_type self.return_type = return_type
self.docs = docs
def snek(s: str): def snek(s: str):
@ -131,11 +146,15 @@ def start():
with open("{}/source/auth_key.tl".format(HOME), encoding="utf-8") as auth, \ with open("{}/source/auth_key.tl".format(HOME), encoding="utf-8") as auth, \
open("{}/source/sys_msgs.tl".format(HOME), encoding="utf-8") as system, \ open("{}/source/sys_msgs.tl".format(HOME), encoding="utf-8") as system, \
open("{}/source/main_api.tl".format(HOME), encoding="utf-8") as api: open("{}/source/main_api.tl".format(HOME), encoding="utf-8") as api, \
schema = (auth.read() + system.read() + api.read()).splitlines() open("{}/source/pyrogram.tl".format(HOME), encoding="utf-8") as pyrogram:
schema = (auth.read() + system.read() + api.read() + pyrogram.read()).splitlines()
with open("{}/template/class.txt".format(HOME), encoding="utf-8") as f: with open("{}/template/mtproto.txt".format(HOME), encoding="utf-8") as f:
template = f.read() mtproto_template = f.read()
with open("{}/template/pyrogram.txt".format(HOME), encoding="utf-8") as f:
pyrogram_template = f.read()
with open(NOTICE_PATH, encoding="utf-8") as f: with open(NOTICE_PATH, encoding="utf-8") as f:
notice = [] notice = []
@ -165,9 +184,9 @@ def start():
combinator = COMBINATOR_RE.match(line) combinator = COMBINATOR_RE.match(line)
if combinator: if combinator:
name, id, return_type = combinator.groups() name, id, return_type, docs = combinator.groups()
namespace, name = name.split(".") if "." in name else ("", name) namespace, name = name.split(".") if "." in name else ("", name)
args = ARGS_RE.findall(line) args = ARGS_RE.findall(line.split(" //")[0])
# Pingu! # Pingu!
has_flags = not not FLAGS_RE_3.findall(line) has_flags = not not FLAGS_RE_3.findall(line)
@ -195,7 +214,8 @@ def start():
".".join( ".".join(
return_type.split(".")[:-1] return_type.split(".")[:-1]
+ [capit(return_type.split(".")[-1])] + [capit(return_type.split(".")[-1])]
) ),
docs
) )
) )
@ -254,6 +274,7 @@ def start():
) if c.args else "pass" ) if c.args else "pass"
docstring_args = [] docstring_args = []
docs = c.docs.split("|")[1:] if c.docs else None
for i, arg in enumerate(sorted_args): for i, arg in enumerate(sorted_args):
arg_name, arg_type = arg arg_name, arg_type = arg
@ -261,11 +282,21 @@ def start():
flag_number = is_optional.group(1) if is_optional else -1 flag_number = is_optional.group(1) if is_optional else -1
arg_type = arg_type.split("?")[-1] arg_type = arg_type.split("?")[-1]
if docs:
docstring_args.append( docstring_args.append(
"{}{}: {}".format( "{} ({}{}):\n {}\n".format(
arg_name, arg_name,
" (optional)".format(flag_number) if is_optional else "", get_docstring_arg_type(arg_type, is_pyrogram_type=True),
get_docstring_arg_type(arg_type) ", optional" if "Optional" in docs[i] else "",
re.sub("Optional\. ", "", docs[i].split("§")[1].rstrip(".") + ".")
)
)
else:
docstring_args.append(
"{}: {}{}".format(
arg_name,
"``optional`` ".format(flag_number) if is_optional else "",
get_docstring_arg_type(arg_type, is_pyrogram_type=c.namespace == "pyrogram")
) )
) )
@ -370,9 +401,25 @@ def start():
read_types += "\n " read_types += "\n "
read_types += "{} = Object.read(b)\n ".format(arg_name) read_types += "{} = Object.read(b)\n ".format(arg_name)
if c.docs:
description = c.docs.split("|")[0].split("§")[1]
docstring_args = description + "\n\n " + docstring_args
with open("{}/{}.py".format(path, snek(c.name)), "w", encoding="utf-8") as f: with open("{}/{}.py".format(path, snek(c.name)), "w", encoding="utf-8") as f:
if c.docs:
f.write( f.write(
template.format( pyrogram_template.format(
notice=notice,
class_name=capit(c.name),
docstring_args=docstring_args,
object_id=c.id,
arguments=arguments,
fields=fields
)
)
else:
f.write(
mtproto_template.format(
notice=notice, notice=notice,
class_name=capit(c.name), class_name=capit(c.name),
docstring_args=docstring_args, docstring_args=docstring_args,

View File

@ -0,0 +1,22 @@
// Pyrogram
---types---
pyrogram.update#b0700000 flags:# update_id:int message:flags.0?Message edited_message:flags.1?Message channel_post:flags.2?Message edited_channel_post:flags.3?Message inline_query:flags.4?InlineQuery chosen_inline_result:flags.5?ChosenInlineResult callback_query:flags.6?CallbackQuery shipping_query:flags.7?ShippingQuery pre_checkout_query:flags.8?PreCheckoutQuery = pyrogram.Update;
pyrogram.user#b0700001 flags:# id:int is_bot:Bool first_name:string last_name:flags.0?string username:flags.1?string language_code:flags.2?string phone_number:flags.3?string = pyrogram.User;
pyrogram.chat#b0700002 flags:# id:int type:string title:flags.0?string username:flags.1?string first_name:flags.2?string last_name:flags.3?string all_members_are_administrators:flags.4?Bool photo:flags.5?ChatPhoto description:flags.6?string invite_link:flags.7?string pinned_message:flags.8?Message sticker_set_name:flags.9?string can_set_sticker_set:flags.10?Bool = pyrogram.Chat;
pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int chat:Chat forward_from:flags.1?User forward_from_chat:flags.2?Chat forward_from_message_id:flags.3?int forward_signature:flags.4?string forward_date:flags.5?int reply_to_message:flags.6?Message edit_date:flags.7?int media_group_id:flags.8?string author_signature:flags.9?string text:flags.10?string entities:flags.11?Vector<MessageEntity> caption_entities:flags.12?Vector<MessageEntity> audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector<PhotoSize> sticker:flags.17?Sticker video:flags.18?Video voice:flags.19?Voice video_note:flags.20?VideoNote caption:flags.21?string contact:flags.22?Contact location:flags.23?Location venue:flags.24?Venue new_chat_members:flags.25?Vector<User> left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector<PhotoSize> delete_chat_photo:flags.29?true group_chat_created:flags.30?true supergroup_chat_created:flags.31?true channel_chat_created:flags.32?true migrate_to_chat_id:flags.33?int migrate_from_chat_id:flags.34?int pinned_message:flags.35?Message invoice:flags.36?Invoice successful_payment:flags.37?SuccessfulPayment connected_website:flags.38?string views:flags.39?int via_bot:flags.40?User = pyrogram.Message;
pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity;
pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize;
pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio;
pyrogram.document#b0700007 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Document;
pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video;
pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice;
pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote;
pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact;
pyrogram.location#b0700012 longitude:double latitude:double = pyrogram.Location;
pyrogram.venue#b0700013 flags:# location:Location title:string address:string foursquare_id:flags.0?string = pyrogram.Venue;
pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector<Vector<PhotoSize>> = pyrogram.UserProfilePhotos;
pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto;
pyrogram.chatMember#b0700016 flags:# user:User status:string until_date:flags.0?int can_be_edited:flags.1?Bool can_change_info:flags.2?Bool can_post_messages:flags.3?Bool can_edit_messages:flags.4?Bool can_delete_messages:flags.5?Bool can_invite_users:flags.6?Bool can_restrict_members:flags.7?Bool can_pin_messages:flags.8?Bool can_promote_members:flags.9?Bool can_send_messages:flags.10?Bool can_send_media_messages:flags.11?Bool can_send_other_messages:flags.12?Bool can_add_web_page_previews:flags.13?Bool = pyrogram.ChatMember;
pyrogram.sticker#b0700017 flags:# file_id:string width:int height:int thumb:flags.0?PhotoSize emoji:flags.1?string set_name:flags.2?string mask_position:flags.3?MaskPosition file_size:flags.4?int = pyrogram.Sticker;

View File

@ -6,8 +6,7 @@ from pyrogram.api.core import *
class {class_name}(Object): class {class_name}(Object):
""" """{docstring_args}
{docstring_args}
""" """
ID = {object_id} ID = {object_id}

View File

@ -0,0 +1,11 @@
{notice}
from pyrogram.api.core import Object
class {class_name}(Object):
"""{docstring_args}
"""
ID = {object_id}
def __init__(self{arguments}):
{fields}

View File

@ -0,0 +1,6 @@
Filters
=======
.. autoclass:: pyrogram.Filters
:members:
:undoc-members:

View File

@ -1,6 +0,0 @@
InputMedia
==========
.. autoclass:: pyrogram.InputMedia
:members:
:undoc-members:

View File

@ -0,0 +1,6 @@
InputMediaPhoto
===============
.. autoclass:: pyrogram.InputMediaPhoto
:members:
:undoc-members:

View File

@ -0,0 +1,6 @@
InputMediaVideo
===============
.. autoclass:: pyrogram.InputMediaVideo
:members:
:undoc-members:

View File

@ -0,0 +1,6 @@
MessageHandler
==============
.. autoclass:: pyrogram.MessageHandler
:members:
:undoc-members:

View File

@ -0,0 +1,6 @@
\RawUpdateHandler
================
.. autoclass:: pyrogram.RawUpdateHandler
:members:
:undoc-members:

View File

@ -9,11 +9,38 @@ the same parameters as well, thus offering a familiar look to Bot developers.
.. toctree:: .. toctree::
Client Client
MessageHandler
RawUpdateHandler
Filters
ChatAction ChatAction
ParseMode ParseMode
Emoji Emoji
InputMedia
InputPhoneContact
Error Error
Types
-----
.. toctree::
../types/pyrogram/User
../types/pyrogram/Chat
../types/pyrogram/Message
../types/pyrogram/MessageEntity
../types/pyrogram/PhotoSize
../types/pyrogram/Audio
../types/pyrogram/Document
../types/pyrogram/Video
../types/pyrogram/Voice
../types/pyrogram/VideoNote
../types/pyrogram/Contact
../types/pyrogram/Location
../types/pyrogram/Venue
../types/pyrogram/UserProfilePhotos
../types/pyrogram/ChatPhoto
../types/pyrogram/ChatMember
InputMediaPhoto
InputMediaVideo
InputPhoneContact
../types/pyrogram/Sticker
.. _Telegram Bot API: https://core.telegram.org/bots/api#available-methods .. _Telegram Bot API: https://core.telegram.org/bots/api#available-methods

View File

@ -26,9 +26,13 @@ __license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__version__ = "0.6.5" __version__ = "0.6.5"
from .api.errors import Error from .api.errors import Error
from .api.types.pyrogram import *
from .client import ChatAction from .client import ChatAction
from .client import Client from .client import Client
from .client import ParseMode from .client import ParseMode
from .client.input_media import InputMedia from .client.input_media_photo import InputMediaPhoto
from .client.input_media_video import InputMediaVideo
from .client.input_phone_contact import InputPhoneContact from .client.input_phone_contact import InputPhoneContact
from .client import Emoji from .client import Emoji
from .client.handlers import MessageHandler, RawUpdateHandler
from .client.filters import Filters

View File

@ -37,6 +37,9 @@ class Object:
def __str__(self) -> str: def __str__(self) -> str:
return dumps(self, cls=Encoder, indent=4) return dumps(self, cls=Encoder, indent=4)
def __bool__(self) -> bool:
return True
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
@ -47,6 +50,15 @@ class Object:
pass pass
def remove_none(obj):
if isinstance(obj, (list, tuple, set)):
return type(obj)(remove_none(x) for x in obj if x is not None)
elif isinstance(obj, dict):
return type(obj)((remove_none(k), remove_none(v)) for k, v in obj.items() if k is not None and v is not None)
else:
return obj
class Encoder(JSONEncoder): class Encoder(JSONEncoder):
def default(self, o: Object): def default(self, o: Object):
try: try:
@ -57,6 +69,9 @@ class Encoder(JSONEncoder):
else: else:
return repr(o) return repr(o)
if "pyrogram" in objects.get(getattr(o, "ID", "")):
return remove_none(OrderedDict([i for i in content.items()]))
else:
return OrderedDict( return OrderedDict(
[("_", objects.get(getattr(o, "ID", None), None))] [("_", objects.get(getattr(o, "ID", None), None))]
+ [i for i in content.items()] + [i for i in content.items()]

View File

@ -18,5 +18,5 @@
from .chat_action import ChatAction from .chat_action import ChatAction
from .client import Client from .client import Client
from .parse_mode import ParseMode
from .emoji import Emoji from .emoji import Emoji
from .parse_mode import ParseMode

View File

@ -36,6 +36,7 @@ from queue import Queue
from signal import signal, SIGINT, SIGTERM, SIGABRT from signal import signal, SIGINT, SIGTERM, SIGABRT
from threading import Event, Thread from threading import Event, Thread
import pyrogram
from pyrogram.api import functions, types from pyrogram.api import functions, types
from pyrogram.api.core import Object from pyrogram.api.core import Object
from pyrogram.api.errors import ( from pyrogram.api.errors import (
@ -44,12 +45,15 @@ from pyrogram.api.errors import (
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded, PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned,
VolumeLocNotFound, UserMigrate) VolumeLocNotFound, UserMigrate, FileIdInvalid)
from pyrogram.crypto import AES from pyrogram.crypto import AES
from pyrogram.session import Auth, Session from pyrogram.session import Auth, Session
from pyrogram.session.internals import MsgId from pyrogram.session.internals import MsgId
from . import message_parser
from .dispatcher import Dispatcher
from .input_media import InputMedia from .input_media import InputMedia
from .style import Markdown, HTML from .style import Markdown, HTML
from .utils import decode
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -130,6 +134,18 @@ class Client:
UPDATES_WORKERS = 1 UPDATES_WORKERS = 1
DOWNLOAD_WORKERS = 1 DOWNLOAD_WORKERS = 1
MEDIA_TYPE_ID = {
0: "Thumbnail",
2: "Photo",
3: "Voice",
4: "Video",
5: "Document",
8: "Sticker",
9: "Audio",
10: "GIF",
13: "VideoNote"
}
def __init__(self, def __init__(self,
session_name: str, session_name: str,
api_id: int or str = None, api_id: int or str = None,
@ -181,10 +197,62 @@ class Client:
self.is_idle = None self.is_idle = None
self.updates_queue = Queue() self.updates_queue = Queue()
self.update_queue = Queue() self.download_queue = Queue()
self.dispatcher = Dispatcher(self, workers)
self.update_handler = None self.update_handler = None
self.download_queue = Queue() def on_message(self, filters=None, group: int = 0):
"""Use this decorator to automatically register a function for handling
messages. This does the same thing as :meth:`add_handler` using the
MessageHandler.
Args:
filters (:obj:`Filters <pyrogram.Filters>`):
Pass one or more filters to allow only a subset of messages to be passed
in your function.
group (``int``, optional):
The group identifier, defaults to 0.
"""
def decorator(func):
self.add_handler(pyrogram.MessageHandler(func, filters), group)
return func
return decorator
def on_raw_update(self, group: int = 0):
"""Use this decorator to automatically register a function for handling
raw updates. This does the same thing as :meth:`add_handler` using the
RawUpdateHandler.
Args:
group (``int``, optional):
The group identifier, defaults to 0.
"""
def decorator(func):
self.add_handler(pyrogram.RawUpdateHandler(func), group)
return func
return decorator
def add_handler(self, handler, group: int = 0):
"""Use this method to register an event handler.
You can register multiple handlers, but at most one handler within a group
will be used for a single event. To handle the same event more than once, register
your handler using a different group id (lower group id == higher priority).
Args:
handler (:obj:`Handler <pyrogram.handler.Handler>`):
The handler to be registered.
group (``int``, optional):
The group identifier, defaults to 0.
"""
self.dispatcher.add_handler(handler, group)
def start(self): def start(self):
"""Use this method to start the Client after creating it. """Use this method to start the Client after creating it.
@ -232,12 +300,11 @@ class Client:
for i in range(self.UPDATES_WORKERS): for i in range(self.UPDATES_WORKERS):
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start() Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
for i in range(self.workers):
Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start()
for i in range(self.DOWNLOAD_WORKERS): for i in range(self.DOWNLOAD_WORKERS):
Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start() Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start()
self.dispatcher.start()
mimetypes.init() mimetypes.init()
def stop(self): def stop(self):
@ -253,12 +320,11 @@ class Client:
for _ in range(self.UPDATES_WORKERS): for _ in range(self.UPDATES_WORKERS):
self.updates_queue.put(None) self.updates_queue.put(None)
for _ in range(self.workers):
self.update_queue.put(None)
for _ in range(self.DOWNLOAD_WORKERS): for _ in range(self.DOWNLOAD_WORKERS):
self.download_queue.put(None) self.download_queue.put(None)
self.dispatcher.stop()
def authorize_bot(self): def authorize_bot(self):
try: try:
r = self.send( r = self.send(
@ -682,7 +748,7 @@ class Client:
if len(self.channels_pts[channel_id]) > 50: if len(self.channels_pts[channel_id]) > 50:
self.channels_pts[channel_id] = self.channels_pts[channel_id][25:] self.channels_pts[channel_id] = self.channels_pts[channel_id][25:]
self.update_queue.put((update, updates.users, updates.chats)) self.dispatcher.updates.put((update, updates.users, updates.chats))
elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)): elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)):
diff = self.send( diff = self.send(
functions.updates.GetDifference( functions.updates.GetDifference(
@ -692,7 +758,7 @@ class Client:
) )
) )
self.update_queue.put(( self.dispatcher.updates.put((
types.UpdateNewMessage( types.UpdateNewMessage(
message=diff.new_messages[0], message=diff.new_messages[0],
pts=updates.pts, pts=updates.pts,
@ -702,30 +768,7 @@ class Client:
diff.chats diff.chats
)) ))
elif isinstance(updates, types.UpdateShort): elif isinstance(updates, types.UpdateShort):
self.update_queue.put((updates.update, [], [])) self.dispatcher.updates.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: except Exception as e:
log.error(e, exc_info=True) log.error(e, exc_info=True)
@ -752,47 +795,6 @@ class Client:
while self.is_idle: while self.is_idle:
time.sleep(1) 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 (``callable``):
A function that will be called when a new update is received from the server. It takes
*(client, update, users, chats)* as positional arguments (Look at the section below for
a detailed description).
Other Parameters:
client (:class:`Client <pyrogram.Client>`):
The Client itself, useful when you want to call other API methods inside the update handler.
update (``Update``):
The received update, which can be one of the many single Updates listed in the *updates*
field you see in the :obj:`Update <pyrogram.api.types.Update>` type.
users (``dict``):
Dictionary of all :obj:`User <pyrogram.api.types.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 (``dict``):
Dictionary of all :obj:`Chat <pyrogram.api.types.Chat>` and
:obj:`Channel <pyrogram.api.types.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 <pyrogram.api.types.UserEmpty>`
- :obj:`ChatEmpty <pyrogram.api.types.ChatEmpty>`
- :obj:`ChatForbidden <pyrogram.api.types.ChatForbidden>`
- :obj:`ChannelForbidden <pyrogram.api.types.ChannelForbidden>`
"""
self.update_handler = callback
def send(self, data: Object): def send(self, data: Object):
"""Use this method to send Raw Function queries. """Use this method to send Raw Function queries.
@ -1122,7 +1124,9 @@ class Client:
photo (``str``): photo (``str``):
Photo to send. Photo to send.
Pass a file path as string to send a photo that exists on your local machine. Pass a file_id as string to send a photo that exists on the Telegram servers,
pass an HTTP URL as a string for Telegram to get a photo from the Internet, or
pass a file path as string to upload a new photo that exists on your local machine.
caption (``bool``, optional): caption (``bool``, optional):
Photo caption, 0-200 characters. Photo caption, 0-200 characters.
@ -1156,23 +1160,55 @@ class Client:
The size of the file. The size of the file.
Returns: Returns:
On success, the sent Message is returned. On success, the sent :obj:`Message <pyrogram.Message>` is returned.
Raises: Raises:
:class:`Error <pyrogram.Error>` :class:`Error <pyrogram.Error>`
""" """
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(photo):
file = self.save_file(photo, progress=progress) file = self.save_file(photo, progress=progress)
media = types.InputMediaUploadedPhoto(
file=file,
ttl_seconds=ttl_seconds
)
elif photo.startswith("http"):
media = types.InputMediaPhotoExternal(
url=photo,
ttl_seconds=ttl_seconds
)
else:
try:
decoded = decode(photo)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 2:
media_type = Client.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaPhoto(
id=types.InputPhoto(
id=unpacked[2],
access_hash=unpacked[3]
),
ttl_seconds=ttl_seconds
)
while True: while True:
try: try:
r = self.send( r = self.send(
functions.messages.SendMedia( functions.messages.SendMedia(
peer=self.resolve_peer(chat_id), peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedPhoto( media=media,
file=file,
ttl_seconds=ttl_seconds
),
silent=disable_notification or None, silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id, reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(), random_id=self.rnd_id(),
@ -1182,7 +1218,12 @@ class Client:
except FilePartMissing as e: except FilePartMissing as e:
self.save_file(photo, file_id=file.id, file_part=e.x) self.save_file(photo, file_id=file.id, file_part=e.x)
else: else:
return r for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
users = {i.id: i for i in r.users}
chats = {i.id: i for i in r.chats}
return message_parser.parse_message(self, i.message, users, chats)
def send_audio(self, def send_audio(self,
chat_id: int or str, chat_id: int or str,
@ -1208,7 +1249,9 @@ class Client:
audio (``str``): audio (``str``):
Audio file to send. Audio file to send.
Pass a file path as string to send an audio file that exists on your local machine. Pass a file_id as string to send an audio file that exists on the Telegram servers,
pass an HTTP URL as a string for Telegram to get an audio file from the Internet, or
pass a file path as string to upload a new audio file that exists on your local machine.
caption (``str``, optional): caption (``str``, optional):
Audio caption, 0-200 characters. Audio caption, 0-200 characters.
@ -1246,20 +1289,17 @@ class Client:
The size of the file. The size of the file.
Returns: Returns:
On success, the sent Message is returned. On success, the sent :obj:`Message <pyrogram.Message>` is returned.
Raises: Raises:
:class:`Error <pyrogram.Error>` :class:`Error <pyrogram.Error>`
""" """
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown style = self.html if parse_mode.lower() == "html" else self.markdown
file = self.save_file(audio, progress=progress)
while True: if os.path.exists(audio):
try: file = self.save_file(audio, progress=progress)
r = self.send( media = types.InputMediaUploadedDocument(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"), mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"),
file=file, file=file,
attributes=[ attributes=[
@ -1270,7 +1310,40 @@ class Client:
), ),
types.DocumentAttributeFilename(os.path.basename(audio)) types.DocumentAttributeFilename(os.path.basename(audio))
] ]
), )
elif audio.startswith("http"):
media = types.InputMediaDocumentExternal(
url=audio
)
else:
try:
decoded = decode(audio)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 9:
media_type = Client.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3]
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None, silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id, reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(), random_id=self.rnd_id(),
@ -1280,7 +1353,12 @@ class Client:
except FilePartMissing as e: except FilePartMissing as e:
self.save_file(audio, file_id=file.id, file_part=e.x) self.save_file(audio, file_id=file.id, file_part=e.x)
else: else:
return r for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
users = {i.id: i for i in r.users}
chats = {i.id: i for i in r.chats}
return message_parser.parse_message(self, i.message, users, chats)
def send_document(self, def send_document(self,
chat_id: int or str, chat_id: int or str,

View File

@ -0,0 +1,19 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .dispatcher import Dispatcher

View File

@ -0,0 +1,148 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import threading
from collections import OrderedDict
from queue import Queue
from threading import Thread
import pyrogram
from pyrogram.api import types
from .. import message_parser
from ..handlers import RawUpdateHandler
log = logging.getLogger(__name__)
class Dispatcher:
MESSAGE_UPDATES = (
types.UpdateNewMessage,
types.UpdateNewChannelMessage
)
EDIT_UPDATES = (
types.UpdateEditMessage,
types.UpdateEditChannelMessage
)
ALLOWED_UPDATES = MESSAGE_UPDATES + EDIT_UPDATES
def __init__(self, client, workers):
self.client = client
self.workers = workers
self.updates = Queue()
self.groups = OrderedDict()
def start(self):
for i in range(self.workers):
Thread(
target=self.update_worker,
name="UpdateWorker#{}".format(i + 1)
).start()
def stop(self):
for _ in range(self.workers):
self.updates.put(None)
def add_handler(self, handler, group: int):
if group not in self.groups:
self.groups[group] = []
self.groups = OrderedDict(sorted(self.groups.items()))
self.groups[group].append(handler)
def dispatch(self, update, users: dict = None, chats: dict = None, is_raw: bool = False):
for group in self.groups.values():
for handler in group:
if is_raw:
if not isinstance(handler, RawUpdateHandler):
continue
args = (self.client, update, users, chats)
else:
message = (update.message
or update.channel_post
or update.edited_message
or update.edited_channel_post)
if not handler.check(message):
continue
args = (self.client, message)
handler.callback(*args)
break
def update_worker(self):
name = threading.current_thread().name
log.debug("{} started".format(name))
while True:
update = self.updates.get()
if update is None:
break
try:
users = {i.id: i for i in update[1]}
chats = {i.id: i for i in update[2]}
update = update[0]
self.dispatch(update, users=users, chats=chats, is_raw=True)
if isinstance(update, Dispatcher.ALLOWED_UPDATES):
if isinstance(update.message, types.Message):
parser = message_parser.parse_message
elif isinstance(update.message, types.MessageService):
parser = message_parser.parse_message_service
else:
continue
message = parser(
self.client,
update.message,
users,
chats
)
else:
continue
is_edited_message = isinstance(update, Dispatcher.EDIT_UPDATES)
self.dispatch(
pyrogram.Update(
update_id=0,
message=((message if message.chat.type != "channel"
else None) if not is_edited_message
else None),
edited_message=((message if message.chat.type != "channel"
else None) if is_edited_message
else None),
channel_post=((message if message.chat.type == "channel"
else None) if not is_edited_message
else None),
edited_channel_post=((message if message.chat.type == "channel"
else None) if is_edited_message
else None)
)
)
except Exception as e:
log.error(e, exc_info=True)
log.debug("{} stopped".format(name))

View File

@ -0,0 +1,19 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .filters import Filters

View File

@ -0,0 +1,57 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
class Filter:
def __call__(self, message):
raise NotImplementedError
def __invert__(self):
return InvertFilter(self)
def __and__(self, other):
return AndFilter(self, other)
def __or__(self, other):
return OrFilter(self, other)
class InvertFilter(Filter):
def __init__(self, base):
self.base = base
def __call__(self, message):
return not self.base(message)
class AndFilter(Filter):
def __init__(self, base, other):
self.base = base
self.other = other
def __call__(self, message):
return self.base(message) and self.other(message)
class OrFilter(Filter):
def __init__(self, base, other):
self.base = base
self.other = other
def __call__(self, message):
return self.base(message) or self.other(message)

View File

@ -0,0 +1,218 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import re
from .filter import Filter
def build(name: str, func: callable, **kwargs) -> type:
d = {"__call__": func}
d.update(kwargs)
return type(name, (Filter,), d)()
class Filters:
"""This class provides access to all the Filters available in Pyrogram.
It is intended to be used when adding an handler."""
text = build("Text", lambda _, m: bool(m.text and not m.text.startswith("/")))
"""Filter text messages."""
reply = build("Reply", lambda _, m: bool(m.reply_to_message))
"""Filter messages that are replies to other messages."""
forwarded = build("Forwarded", lambda _, m: bool(m.forward_date))
"""Filter messages that are forwarded."""
caption = build("Caption", lambda _, m: bool(m.caption))
"""Filter media messages that contain captions."""
edited = build("Edited", lambda _, m: bool(m.edit_date))
"""Filter edited messages."""
audio = build("Audio", lambda _, m: bool(m.audio))
"""Filter messages that contain an :obj:`Audio <pyrogram.Audio>`."""
document = build("Document", lambda _, m: bool(m.document))
"""Filter messages that contain a :obj:`Document <pyrogram.Document>`."""
photo = build("Photo", lambda _, m: bool(m.photo))
"""Filter messages that contain a :obj:`Photo <pyrogram.Photo>`."""
sticker = build("Sticker", lambda _, m: bool(m.sticker))
"""Filter messages that contain a :obj:`Sticker <pyrogram.Sticker>`."""
video = build("Video", lambda _, m: bool(m.video))
"""Filter messages that contain a :obj:`Video <pyrogram.Video>`."""
voice = build("Voice", lambda _, m: bool(m.voice))
"""Filter messages that contain a :obj:`Voice <pyrogram.Voice>` note."""
video_note = build("Voice", lambda _, m: bool(m.video_note))
"""Filter messages that contain a :obj:`VideoNote <pyrogram.VideoNote>`."""
contact = build("Contact", lambda _, m: bool(m.contact))
"""Filter messages that contain a :obj:`Contact <pyrogram.Contact>`."""
location = build("Location", lambda _, m: bool(m.location))
"""Filter messages that contain a :obj:`Location <pyrogram.Location>`."""
venue = build("Venue", lambda _, m: bool(m.venue))
"""Filter messages that contain a :obj:`Venue <pyrogram.Venue>`."""
private = build("Private", lambda _, m: bool(m.chat.type == "private"))
"""Filter messages sent in private chats."""
group = build("Group", lambda _, m: bool(m.chat.type in {"group", "supergroup"}))
"""Filter messages sent in group or supergroup chats."""
channel = build("Channel", lambda _, m: bool(m.chat.type == "channel"))
"""Filter messages sent in channels."""
@staticmethod
def command(command: str or list):
"""Filter commands, i.e.: text messages starting with '/'.
Args:
command (``str`` | ``list``):
The command or list of commands as strings the filter should look for.
"""
return build(
"Command",
lambda _, m: bool(
m.text
and m.text.startswith("/")
and (m.text[1:].split()[0] in _.c)
),
c=(
{command}
if not isinstance(command, list)
else {c for c in command}
)
)
@staticmethod
def regex(pattern, flags: int = 0):
"""Filter messages that match a given RegEx pattern.
Args:
pattern (``str``):
The RegEx pattern.
flags (``int``, optional):
RegEx flags.
"""
return build(
"Regex", lambda _, m: bool(_.p.search(m.text or "")),
p=re.compile(pattern, flags)
)
@staticmethod
def user(user: int or str or list):
"""Filter messages coming from specific users.
Args:
user (``int`` | ``str`` | ``list``):
The user or list of user IDs (int) or usernames (str) the filter should look for.
"""
return build(
"User",
lambda _, m: bool(m.from_user
and (m.from_user.id in _.u
or (m.from_user.username
and m.from_user.username.lower() in _.u))),
u=(
{user.lower().strip("@") if type(user) is str else user}
if not isinstance(user, list)
else {i.lower().strip("@") if type(i) is str else i for i in user}
)
)
@staticmethod
def chat(chat: int or str or list):
"""Filter messages coming from specific chats.
Args:
chat (``int`` | ``str`` | ``list``):
The chat or list of chat IDs (int) or usernames (str) the filter should look for.
"""
return build(
"Chat",
lambda _, m: bool(m.chat
and (m.chat.id in _.c
or (m.chat.username
and m.chat.username.lower() in _.c))),
c=(
{chat.lower().strip("@") if type(chat) is str else chat}
if not isinstance(chat, list)
else {i.lower().strip("@") if type(i) is str else i for i in chat}
)
)
new_chat_members = build("NewChatMembers", lambda _, m: bool(m.new_chat_members))
"""Filter service messages for new chat members."""
left_chat_member = build("LeftChatMember", lambda _, m: bool(m.left_chat_member))
"""Filter service messages for members that left the chat."""
new_chat_title = build("NewChatTitle", lambda _, m: bool(m.new_chat_title))
"""Filter service messages for new chat titles."""
new_chat_photo = build("NewChatPhoto", lambda _, m: bool(m.new_chat_photo))
"""Filter service messages for new chat photos."""
delete_chat_photo = build("DeleteChatPhoto", lambda _, m: bool(m.delete_chat_photo))
"""Filter service messages for deleted photos."""
group_chat_created = build("GroupChatCreated", lambda _, m: bool(m.group_chat_created))
"""Filter service messages for group chat creations."""
supergroup_chat_created = build("SupergroupChatCreated", lambda _, m: bool(m.supergroup_chat_created))
"""Filter service messages for supergroup chat creations."""
channel_chat_created = build("ChannelChatCreated", lambda _, m: bool(m.channel_chat_created))
"""Filter service messages for channel chat creations."""
migrate_to_chat_id = build("MigrateToChatId", lambda _, m: bool(m.migrate_to_chat_id))
"""Filter service messages that contain migrate_to_chat_id."""
migrate_from_chat_id = build("MigrateFromChatId", lambda _, m: bool(m.migrate_from_chat_id))
"""Filter service messages that contain migrate_from_chat_id."""
pinned_message = build("PinnedMessage", lambda _, m: bool(m.pinned_message))
"""Filter service messages for pinned messages."""
service = build(
"Service",
lambda _, m: bool(
_.new_chat_members(m)
or _.left_chat_member(m)
or _.new_chat_title(m)
or _.new_chat_photo(m)
or _.delete_chat_photo(m)
or _.group_chat_created(m)
or _.supergroup_chat_created(m)
or _.channel_chat_created(m)
or _.migrate_to_chat_id(m)
or _.migrate_from_chat_id(m)
or _.pinned_m(m)
)
)
"""Filter all service messages"""

View File

@ -0,0 +1,19 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .handlers import MessageHandler, RawUpdateHandler

View File

@ -0,0 +1,23 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
class Handler:
def __init__(self, callback: callable, filters=None):
self.callback = callback
self.filters = filters

View File

@ -0,0 +1,93 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .handler import Handler
class MessageHandler(Handler):
"""The Message handler class. It is used to handle text, media and service messages coming from
any chat (private, group, channel).
Args:
callback (``callable``):
Pass a function that will be called when a new Message arrives. It takes *(client, message)*
as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters <pyrogram.Filters>`):
Pass one or more filters to allow only a subset of messages to be passed
in your callback function.
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
The Client itself, useful when you want to call other API methods inside the message handler.
message (:obj:`Message <pyrogram.Message>`):
The received message.
"""
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, message):
return (
self.filters(message)
if self.filters
else True
)
class RawUpdateHandler(Handler):
"""The Raw Update handler class. It is used to handle raw updates.
Args:
callback (``callable``):
A function that will be called when a new update is received from the server. It takes
*(client, update, users, chats)* as positional arguments (look at the section below for
a detailed description).
Other Parameters:
client (:class:`Client <pyrogram.Client>`):
The Client itself, useful when you want to call other API methods inside the update handler.
update (``Update``):
The received update, which can be one of the many single Updates listed in the *updates*
field you see in the :obj:`Update <pyrogram.api.types.Update>` type.
users (``dict``):
Dictionary of all :obj:`User <pyrogram.api.types.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 (``dict``):
Dictionary of all :obj:`Chat <pyrogram.api.types.Chat>` and
:obj:`Channel <pyrogram.api.types.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 <pyrogram.api.types.UserEmpty>`
- :obj:`ChatEmpty <pyrogram.api.types.ChatEmpty>`
- :obj:`ChatForbidden <pyrogram.api.types.ChatForbidden>`
- :obj:`ChannelForbidden <pyrogram.api.types.ChannelForbidden>`
"""
def __init__(self, callback: callable):
super().__init__(callback)

View File

@ -0,0 +1,44 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
class InputMediaPhoto:
"""This object represents a photo to be sent inside an album.
It is intended to be used with :obj:`send_media_group <pyrogram.Client.send_media_group>`.
Args:
media (:obj:`str`):
Photo file to send.
Pass a file path as string to send a photo that exists on your local machine.
caption (:obj:`str`):
Caption of the photo to be sent, 0-200 characters
parse_mode (:obj:`str`):
Use :obj:`MARKDOWN <pyrogram.ParseMode.MARKDOWN>` or :obj:`HTML <pyrogram.ParseMode.HTML>`
if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
"""
def __init__(self,
media: str,
caption: str = "",
parse_mode: str = ""):
self.media = media
self.caption = caption
self.parse_mode = parse_mode

View File

@ -0,0 +1,64 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
class InputMediaVideo:
"""This object represents a video to be sent inside an album.
It is intended to be used with :obj:`send_media_group <pyrogram.Client.send_media_group>`.
Args:
media (:obj:`str`):
Video file to send.
Pass a file path as string to send a video that exists on your local machine.
caption (:obj:`str`, optional):
Caption of the video to be sent, 0-200 characters
parse_mode (:obj:`str`, optional):
Use :obj:`MARKDOWN <pyrogram.ParseMode.MARKDOWN>` or :obj:`HTML <pyrogram.ParseMode.HTML>`
if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
width (:obj:`int`, optional):
Video width.
height (:obj:`int`, optional):
Video height.
duration (:obj:`int`, optional):
Video duration.
supports_streaming (:obj:`bool`, optional):
Pass True, if the uploaded video is suitable for streaming.
"""
def __init__(self,
media: str,
caption: str = "",
parse_mode: str = "",
width: int = 0,
height: int = 0,
duration: int = 0,
supports_streaming: bool = True):
self.media = media
self.caption = caption
self.parse_mode = parse_mode
self.width = width
self.height = height
self.duration = duration
self.supports_streaming = supports_streaming

View File

@ -0,0 +1,539 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from struct import pack
import pyrogram
from pyrogram.api import types
from .utils import encode
# TODO: Organize the code better?
ENTITIES = {
types.MessageEntityMention.ID: "mention",
types.MessageEntityHashtag.ID: "hashtag",
types.MessageEntityBotCommand.ID: "bot_command",
types.MessageEntityUrl.ID: "url",
types.MessageEntityEmail.ID: "email",
types.MessageEntityBold.ID: "bold",
types.MessageEntityItalic.ID: "italic",
types.MessageEntityCode.ID: "code",
types.MessageEntityPre.ID: "pre",
types.MessageEntityTextUrl.ID: "text_link",
types.MessageEntityMentionName.ID: "text_mention"
}
def parse_entities(entities: list, users: dict) -> list:
output_entities = []
for entity in entities:
entity_type = ENTITIES.get(entity.ID, None)
if entity_type:
output_entities.append(pyrogram.MessageEntity(
type=entity_type,
offset=entity.offset,
length=entity.length,
url=getattr(entity, "url", None),
user=parse_user(
users.get(
getattr(entity, "user_id", None),
None
)
)
))
return output_entities
def parse_user(user: types.User) -> pyrogram.User or None:
return pyrogram.User(
id=user.id,
is_bot=user.bot,
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
language_code=user.lang_code,
phone_number=user.phone
) if user else None
def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat:
if isinstance(message.to_id, types.PeerUser):
return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id])
elif isinstance(message.to_id, types.PeerChat):
return parse_chat_chat(chats[message.to_id.chat_id])
else:
return parse_channel_chat(chats[message.to_id.channel_id])
def parse_user_chat(user: types.User) -> pyrogram.Chat:
return pyrogram.Chat(
id=user.id,
type="private",
username=user.username,
first_name=user.first_name,
last_name=user.last_name
)
def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat:
return pyrogram.Chat(
id=-chat.id,
type="group",
title=chat.title,
all_members_are_administrators=not chat.admins_enabled
)
def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat:
return pyrogram.Chat(
id=int("-100" + str(channel.id)),
type="supergroup" if channel.megagroup else "channel",
title=channel.title,
username=channel.username
)
def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None:
if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)):
loc = thumb.location
if isinstance(thumb, types.PhotoSize):
file_size = thumb.size
else:
file_size = len(thumb.bytes)
if isinstance(loc, types.FileLocation):
return pyrogram.PhotoSize(
file_id=encode(
pack(
"<iiqqqqi",
0,
loc.dc_id,
0,
0,
loc.volume_id,
loc.secret,
loc.local_id
)
),
width=thumb.w,
height=thumb.h,
file_size=file_size
)
# TODO: Reorganize code, maybe split parts as well
def parse_message(
client,
message: types.Message,
users: dict,
chats: dict,
replies: int = 1
) -> pyrogram.Message:
entities = parse_entities(message.entities, users)
forward_from = None
forward_from_chat = None
forward_from_message_id = None
forward_signature = None
forward_date = None
forward_header = message.fwd_from # type: types.MessageFwdHeader
if forward_header:
forward_date = forward_header.date
if forward_header.from_id:
forward_from = parse_user(users[forward_header.from_id])
else:
forward_from_chat = parse_channel_chat(chats[forward_header.channel_id])
forward_from_message_id = forward_header.channel_post
forward_signature = forward_header.post_author
photo = None
location = None
contact = None
venue = None
audio = None
voice = None
video = None
video_note = None
sticker = None
document = None
media = message.media
if media:
if isinstance(media, types.MessageMediaPhoto):
photo = media.photo
if isinstance(photo, types.Photo):
sizes = photo.sizes
photo_sizes = []
for size in sizes:
if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)):
loc = size.location
if isinstance(size, types.PhotoSize):
file_size = size.size
else:
file_size = len(size.bytes)
if isinstance(loc, types.FileLocation):
photo_size = pyrogram.PhotoSize(
file_id=encode(
pack(
"<iiqqqqi",
2,
loc.dc_id,
photo.id,
photo.access_hash,
loc.volume_id,
loc.secret,
loc.local_id
)
),
width=size.w,
height=size.h,
file_size=file_size
)
photo_sizes.append(photo_size)
photo = photo_sizes
elif isinstance(media, types.MessageMediaGeo):
geo_point = media.geo
if isinstance(geo_point, types.GeoPoint):
location = pyrogram.Location(
longitude=geo_point.long,
latitude=geo_point.lat
)
elif isinstance(media, types.MessageMediaContact):
contact = pyrogram.Contact(
phone_number=media.phone_number,
first_name=media.first_name,
last_name=media.last_name,
user_id=media.user_id
)
elif isinstance(media, types.MessageMediaVenue):
venue = pyrogram.Venue(
location=pyrogram.Location(
longitude=media.geo.long,
latitude=media.geo.lat
),
title=media.title,
address=media.address,
foursquare_id=media.venue_id
)
elif isinstance(media, types.MessageMediaDocument):
doc = media.document
if isinstance(doc, types.Document):
attributes = {type(i): i for i in doc.attributes}
if types.DocumentAttributeAudio in attributes:
audio_attributes = attributes[types.DocumentAttributeAudio]
if audio_attributes.voice:
voice = pyrogram.Voice(
file_id=encode(
pack(
"<iiqq",
3,
doc.dc_id,
doc.id,
doc.access_hash
)
),
duration=audio_attributes.duration,
mime_type=doc.mime_type,
file_size=doc.size
)
else:
audio = pyrogram.Audio(
file_id=encode(
pack(
"<iiqq",
9,
doc.dc_id,
doc.id,
doc.access_hash
)
),
duration=audio_attributes.duration,
performer=audio_attributes.performer,
title=audio_attributes.title,
mime_type=doc.mime_type,
file_size=doc.size
)
elif types.DocumentAttributeAnimated in attributes:
document = pyrogram.Document(
file_id=encode(
pack(
"<iiqq",
10,
doc.dc_id,
doc.id,
doc.access_hash
)
),
thumb=parse_thumb(doc.thumb),
file_name=getattr(
attributes.get(
types.DocumentAttributeFilename, None
), "file_name", None
),
mime_type=doc.mime_type,
file_size=doc.size
)
elif types.DocumentAttributeVideo in attributes:
video_attributes = attributes[types.DocumentAttributeVideo]
if video_attributes.round_message:
video_note = pyrogram.VideoNote(
file_id=encode(
pack(
"<iiqq",
13,
doc.dc_id,
doc.id,
doc.access_hash
)
),
length=video_attributes.w,
duration=video_attributes.duration,
thumb=parse_thumb(doc.thumb),
file_size=doc.size
)
else:
video = pyrogram.Video(
file_id=encode(
pack(
"<iiqq",
4,
doc.dc_id,
doc.id,
doc.access_hash
)
),
width=video_attributes.w,
height=video_attributes.h,
duration=video_attributes.duration,
thumb=parse_thumb(doc.thumb),
mime_type=doc.mime_type,
file_size=doc.size
)
elif types.DocumentAttributeSticker in attributes:
image_size_attributes = attributes[types.DocumentAttributeImageSize]
sticker = pyrogram.Sticker(
file_id=encode(
pack(
"<iiqq",
8,
doc.dc_id,
doc.id,
doc.access_hash
)
),
width=image_size_attributes.w,
height=image_size_attributes.h,
thumb=parse_thumb(doc.thumb),
# TODO: Emoji, set_name and mask_position
file_size=doc.size,
)
else:
document = pyrogram.Document(
file_id=encode(
pack(
"<iiqq",
5,
doc.dc_id,
doc.id,
doc.access_hash
)
),
thumb=parse_thumb(doc.thumb),
file_name=getattr(
attributes.get(
types.DocumentAttributeFilename, None
), "file_name", None
),
mime_type=doc.mime_type,
file_size=doc.size
)
else:
media = None
m = pyrogram.Message(
message_id=message.id,
date=message.date,
chat=parse_chat(message, users, chats),
from_user=parse_user(users.get(message.from_id, None)),
text=message.message or None if media is None else None,
caption=message.message or None if media is not None else None,
entities=entities or None if media is None else None,
caption_entities=entities or None if media is not None else None,
author_signature=message.post_author,
forward_from=forward_from,
forward_from_chat=forward_from_chat,
forward_from_message_id=forward_from_message_id,
forward_signature=forward_signature,
forward_date=forward_date,
edit_date=message.edit_date,
media_group_id=message.grouped_id,
photo=photo,
location=location,
contact=contact,
venue=venue,
audio=audio,
voice=voice,
video=video,
video_note=video_note,
sticker=sticker,
document=document,
views=message.views,
via_bot=parse_user(users.get(message.via_bot_id, None))
)
if message.reply_to_msg_id and replies:
reply_to_message = client.get_messages(m.chat.id, [message.reply_to_msg_id])
message = reply_to_message.messages[0]
users = {i.id: i for i in reply_to_message.users}
chats = {i.id: i for i in reply_to_message.chats}
if isinstance(message, types.Message):
m.reply_to_message = parse_message(client, message, users, chats, replies - 1)
elif isinstance(message, types.MessageService):
m.reply_to_message = parse_message_service(client, message, users, chats)
return m
def parse_message_service(
client,
message: types.MessageService,
users: dict,
chats: dict
) -> pyrogram.Message:
action = message.action
new_chat_members = None
left_chat_member = None
new_chat_title = None
delete_chat_photo = None
migrate_to_chat_id = None
migrate_from_chat_id = None
group_chat_created = None
channel_chat_created = None
new_chat_photo = None
if isinstance(action, types.MessageActionChatAddUser):
new_chat_members = [parse_user(users[i]) for i in action.users]
elif isinstance(action, types.MessageActionChatJoinedByLink):
new_chat_members = [parse_user(users[action.inviter_id])]
elif isinstance(action, types.MessageActionChatDeleteUser):
left_chat_member = parse_user(users[action.user_id])
elif isinstance(action, types.MessageActionChatEditTitle):
new_chat_title = action.title
elif isinstance(action, types.MessageActionChatDeletePhoto):
delete_chat_photo = True
elif isinstance(action, types.MessageActionChatMigrateTo):
migrate_to_chat_id = action.channel_id
elif isinstance(action, types.MessageActionChannelMigrateFrom):
migrate_from_chat_id = action.chat_id
elif isinstance(action, types.MessageActionChatCreate):
group_chat_created = True
elif isinstance(action, types.MessageActionChannelCreate):
channel_chat_created = True
elif isinstance(action, types.MessageActionChatEditPhoto):
photo = action.photo
if isinstance(photo, types.Photo):
sizes = photo.sizes
photo_sizes = []
for size in sizes:
if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)):
loc = size.location
if isinstance(size, types.PhotoSize):
file_size = size.size
else:
file_size = len(size.bytes)
if isinstance(loc, types.FileLocation):
photo_size = pyrogram.PhotoSize(
file_id=encode(
pack(
"<iiqqqqi",
2,
loc.dc_id,
photo.id,
photo.access_hash,
loc.volume_id,
loc.secret,
loc.local_id
)
),
width=size.w,
height=size.h,
file_size=file_size
)
photo_sizes.append(photo_size)
new_chat_photo = photo_sizes
m = pyrogram.Message(
message_id=message.id,
date=message.date,
chat=parse_chat(message, users, chats),
from_user=parse_user(users.get(message.from_id, None)),
new_chat_members=new_chat_members,
left_chat_member=left_chat_member,
new_chat_title=new_chat_title,
new_chat_photo=new_chat_photo,
delete_chat_photo=delete_chat_photo,
migrate_to_chat_id=int("-100" + str(migrate_to_chat_id)) if migrate_to_chat_id else None,
migrate_from_chat_id=-migrate_from_chat_id if migrate_from_chat_id else None,
group_chat_created=group_chat_created,
channel_chat_created=channel_chat_created
# TODO: supergroup_chat_created
)
if isinstance(action, types.MessageActionPinMessage):
pin_message = client.get_messages(m.chat.id, [message.reply_to_msg_id])
message = pin_message.messages[0]
users = {i.id: i for i in pin_message.users}
chats = {i.id: i for i in pin_message.chats}
if isinstance(message, types.Message):
m.pinned_message = parse_message(client, message, users, chats)
elif isinstance(message, types.MessageService):
# TODO: We can't pin a service message, can we?
m.pinned_message = parse_message_service(client, message, users, chats)
return m

55
pyrogram/client/utils.py Normal file
View File

@ -0,0 +1,55 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from base64 import b64decode, b64encode
def decode(s: str) -> bytes:
s = b64decode(s + "=" * (-len(s) % 4), "-_")
r = b""
assert s[-1] == 2
i = 0
while i < len(s) - 1:
if s[i] != 0:
r += bytes([s[i]])
else:
r += b"\x00" * s[i + 1]
i += 1
i += 1
return r
def encode(s: bytes) -> str:
r = b""
n = 0
for i in s + bytes([2]):
if i == 0:
n += 1
else:
if n:
r += b"\x00" + bytes([n])
n = 0
r += bytes([i])
return b64encode(r, b"-_").decode().rstrip("=")