diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index d5e26971..7d37fdf3 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -25,7 +25,7 @@ DESTINATION = "pyrogram/api" NOTICE_PATH = "NOTICE" SECTION_RE = re.compile(r"---(\w+)---") 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?!.<>]+)") FLAGS_RE = re.compile(r"flags\.(\d+)\?") FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)") @@ -38,7 +38,7 @@ types_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 == "long": return "``int`` ``64-bit``" @@ -60,11 +60,17 @@ def get_docstring_arg_type(t: str, is_list: bool = False): elif t.startswith("Vector"): return "List of " + get_docstring_arg_type(t.split("<")[1][:-1], is_list=True) else: + if is_pyrogram_type: + t = "pyrogram." + t + t = types_to_constructors.get(t, [t]) n = len(t) - 1 t = (("e" if is_list else "E") + "ither " if n else "") + ", ".join( - ":obj:`{0} `".format(i) + ":obj:`{1} `".format( + "pyrogram." if is_pyrogram_type else "", + i.lstrip("pyrogram.") + ) for i in t ) @@ -94,7 +100,15 @@ def get_references(t: str): 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.namespace = namespace self.name = name @@ -102,6 +116,7 @@ class Combinator: self.args = args self.has_flags = has_flags self.return_type = return_type + self.docs = docs def snek(s: str): @@ -131,11 +146,15 @@ def start(): 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/main_api.tl".format(HOME), encoding="utf-8") as api: - schema = (auth.read() + system.read() + api.read()).splitlines() + open("{}/source/main_api.tl".format(HOME), encoding="utf-8") as api, \ + 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: - template = f.read() + with open("{}/template/mtproto.txt".format(HOME), encoding="utf-8") as f: + 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: notice = [] @@ -165,9 +184,9 @@ def start(): combinator = COMBINATOR_RE.match(line) if combinator: - name, id, return_type = combinator.groups() + name, id, return_type, docs = combinator.groups() namespace, name = name.split(".") if "." in name else ("", name) - args = ARGS_RE.findall(line) + args = ARGS_RE.findall(line.split(" //")[0]) # Pingu! has_flags = not not FLAGS_RE_3.findall(line) @@ -195,7 +214,8 @@ def start(): ".".join( return_type.split(".")[:-1] + [capit(return_type.split(".")[-1])] - ) + ), + docs ) ) @@ -254,6 +274,7 @@ def start(): ) if c.args else "pass" docstring_args = [] + docs = c.docs.split("|")[1:] if c.docs else None for i, arg in enumerate(sorted_args): arg_name, arg_type = arg @@ -261,13 +282,23 @@ def start(): flag_number = is_optional.group(1) if is_optional else -1 arg_type = arg_type.split("?")[-1] - docstring_args.append( - "{}{}: {}".format( - arg_name, - " (optional)".format(flag_number) if is_optional else "", - get_docstring_arg_type(arg_type) + if docs: + docstring_args.append( + "{} ({}{}):\n {}\n".format( + arg_name, + get_docstring_arg_type(arg_type, is_pyrogram_type=True), + ", 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") + ) ) - ) if docstring_args: docstring_args = "Args:\n " + "\n ".join(docstring_args) @@ -370,22 +401,38 @@ def start(): read_types += "\n " 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: - f.write( - template.format( - notice=notice, - class_name=capit(c.name), - docstring_args=docstring_args, - object_id=c.id, - arguments=arguments, - fields=fields, - read_flags=read_flags, - read_types=read_types, - write_flags=write_flags, - write_types=write_types, - return_arguments=", ".join([i[0] for i in sorted_args]) + if c.docs: + f.write( + 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, + class_name=capit(c.name), + docstring_args=docstring_args, + object_id=c.id, + arguments=arguments, + fields=fields, + read_flags=read_flags, + read_types=read_types, + write_flags=write_flags, + write_types=write_types, + return_arguments=", ".join([i[0] for i in sorted_args]) + ) ) - ) with open("{}/all.py".format(DESTINATION), "w", encoding="utf-8") as f: f.write(notice + "\n\n") diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl new file mode 100644 index 00000000..a2716ade --- /dev/null +++ b/compiler/api/source/pyrogram.tl @@ -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 caption_entities:flags.12?Vector audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector 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 left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector 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> = 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; diff --git a/compiler/api/template/class.txt b/compiler/api/template/mtproto.txt similarity index 94% rename from compiler/api/template/class.txt rename to compiler/api/template/mtproto.txt index d29caf05..81c99062 100644 --- a/compiler/api/template/class.txt +++ b/compiler/api/template/mtproto.txt @@ -6,8 +6,7 @@ from pyrogram.api.core import * class {class_name}(Object): - """ - {docstring_args} + """{docstring_args} """ ID = {object_id} diff --git a/compiler/api/template/pyrogram.txt b/compiler/api/template/pyrogram.txt new file mode 100644 index 00000000..adbe4151 --- /dev/null +++ b/compiler/api/template/pyrogram.txt @@ -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} diff --git a/docs/source/pyrogram/Filters.rst b/docs/source/pyrogram/Filters.rst new file mode 100644 index 00000000..083bd64a --- /dev/null +++ b/docs/source/pyrogram/Filters.rst @@ -0,0 +1,6 @@ +Filters +======= + +.. autoclass:: pyrogram.Filters + :members: + :undoc-members: diff --git a/docs/source/pyrogram/InputMedia.rst b/docs/source/pyrogram/InputMedia.rst deleted file mode 100644 index c637bdc0..00000000 --- a/docs/source/pyrogram/InputMedia.rst +++ /dev/null @@ -1,6 +0,0 @@ -InputMedia -========== - -.. autoclass:: pyrogram.InputMedia - :members: - :undoc-members: diff --git a/docs/source/pyrogram/InputMediaPhoto.rst b/docs/source/pyrogram/InputMediaPhoto.rst new file mode 100644 index 00000000..abc3f456 --- /dev/null +++ b/docs/source/pyrogram/InputMediaPhoto.rst @@ -0,0 +1,6 @@ +InputMediaPhoto +=============== + +.. autoclass:: pyrogram.InputMediaPhoto + :members: + :undoc-members: diff --git a/docs/source/pyrogram/InputMediaVideo.rst b/docs/source/pyrogram/InputMediaVideo.rst new file mode 100644 index 00000000..de9c480b --- /dev/null +++ b/docs/source/pyrogram/InputMediaVideo.rst @@ -0,0 +1,6 @@ +InputMediaVideo +=============== + +.. autoclass:: pyrogram.InputMediaVideo + :members: + :undoc-members: diff --git a/docs/source/pyrogram/MessageHandler.rst b/docs/source/pyrogram/MessageHandler.rst new file mode 100644 index 00000000..de908bd3 --- /dev/null +++ b/docs/source/pyrogram/MessageHandler.rst @@ -0,0 +1,6 @@ +MessageHandler +============== + +.. autoclass:: pyrogram.MessageHandler + :members: + :undoc-members: diff --git a/docs/source/pyrogram/RawUpdateHandler.rst b/docs/source/pyrogram/RawUpdateHandler.rst new file mode 100644 index 00000000..3d74a34b --- /dev/null +++ b/docs/source/pyrogram/RawUpdateHandler.rst @@ -0,0 +1,6 @@ +\RawUpdateHandler +================ + +.. autoclass:: pyrogram.RawUpdateHandler + :members: + :undoc-members: diff --git a/docs/source/pyrogram/index.rst b/docs/source/pyrogram/index.rst index d8ff3487..d1084a29 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -9,11 +9,38 @@ the same parameters as well, thus offering a familiar look to Bot developers. .. toctree:: Client + MessageHandler + RawUpdateHandler + Filters ChatAction ParseMode Emoji - InputMedia - InputPhoneContact 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 diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 96ce0e30..d5e26b43 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -26,9 +26,13 @@ __license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)" __version__ = "0.6.5" from .api.errors import Error +from .api.types.pyrogram import * from .client import ChatAction from .client import Client 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 import Emoji +from .client.handlers import MessageHandler, RawUpdateHandler +from .client.filters import Filters diff --git a/pyrogram/api/core/object.py b/pyrogram/api/core/object.py index bec9d015..24c1dcf1 100644 --- a/pyrogram/api/core/object.py +++ b/pyrogram/api/core/object.py @@ -37,6 +37,9 @@ class Object: def __str__(self) -> str: return dumps(self, cls=Encoder, indent=4) + def __bool__(self) -> bool: + return True + def __eq__(self, other) -> bool: return self.__dict__ == other.__dict__ @@ -47,6 +50,15 @@ class Object: 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): def default(self, o: Object): try: @@ -57,7 +69,10 @@ class Encoder(JSONEncoder): else: return repr(o) - return OrderedDict( - [("_", objects.get(getattr(o, "ID", None), None))] - + [i for i in content.items()] - ) + if "pyrogram" in objects.get(getattr(o, "ID", "")): + return remove_none(OrderedDict([i for i in content.items()])) + else: + return OrderedDict( + [("_", objects.get(getattr(o, "ID", None), None))] + + [i for i in content.items()] + ) diff --git a/pyrogram/client/__init__.py b/pyrogram/client/__init__.py index abda4464..b2935dad 100644 --- a/pyrogram/client/__init__.py +++ b/pyrogram/client/__init__.py @@ -18,5 +18,5 @@ from .chat_action import ChatAction from .client import Client -from .parse_mode import ParseMode from .emoji import Emoji +from .parse_mode import ParseMode diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 4d885e92..7e8eb791 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -36,6 +36,7 @@ from queue import Queue from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Event, Thread +import pyrogram from pyrogram.api import functions, types from pyrogram.api.core import Object from pyrogram.api.errors import ( @@ -44,12 +45,15 @@ from pyrogram.api.errors import ( PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded, PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, - VolumeLocNotFound, UserMigrate) + VolumeLocNotFound, UserMigrate, FileIdInvalid) from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId +from . import message_parser +from .dispatcher import Dispatcher from .input_media import InputMedia from .style import Markdown, HTML +from .utils import decode log = logging.getLogger(__name__) @@ -130,6 +134,18 @@ class Client: UPDATES_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, session_name: str, api_id: int or str = None, @@ -181,10 +197,62 @@ class Client: self.is_idle = None self.updates_queue = Queue() - self.update_queue = Queue() + self.download_queue = Queue() + + self.dispatcher = Dispatcher(self, workers) 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 `): + 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 `): + The handler to be registered. + + group (``int``, optional): + The group identifier, defaults to 0. + """ + self.dispatcher.add_handler(handler, group) def start(self): """Use this method to start the Client after creating it. @@ -232,12 +300,11 @@ class Client: for i in range(self.UPDATES_WORKERS): 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): Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start() + self.dispatcher.start() + mimetypes.init() def stop(self): @@ -253,12 +320,11 @@ class Client: for _ in range(self.UPDATES_WORKERS): self.updates_queue.put(None) - for _ in range(self.workers): - self.update_queue.put(None) - for _ in range(self.DOWNLOAD_WORKERS): self.download_queue.put(None) + self.dispatcher.stop() + def authorize_bot(self): try: r = self.send( @@ -682,7 +748,7 @@ class Client: 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)) + self.dispatcher.updates.put((update, updates.users, updates.chats)) elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)): diff = self.send( functions.updates.GetDifference( @@ -692,7 +758,7 @@ class Client: ) ) - self.update_queue.put(( + self.dispatcher.updates.put(( types.UpdateNewMessage( message=diff.new_messages[0], pts=updates.pts, @@ -702,30 +768,7 @@ class Client: 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]} - ) + self.dispatcher.updates.put((updates.update, [], [])) except Exception as e: log.error(e, exc_info=True) @@ -752,47 +795,6 @@ class Client: 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 (``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 `): - 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 ` type. - - users (``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 (``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. @@ -1122,7 +1124,9 @@ class Client: photo (``str``): 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): Photo caption, 0-200 characters. @@ -1156,23 +1160,55 @@ class Client: The size of the file. Returns: - On success, the sent Message is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` """ + file = None style = self.html if parse_mode.lower() == "html" else self.markdown - file = self.save_file(photo, progress=progress) + + if os.path.exists(photo): + 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 = " 24 else "` is returned. Raises: :class:`Error ` """ + file = None style = self.html if parse_mode.lower() == "html" else self.markdown - file = self.save_file(audio, progress=progress) + + if os.path.exists(audio): + file = self.save_file(audio, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"), + file=file, + attributes=[ + types.DocumentAttributeAudio( + duration=duration, + performer=performer, + title=title + ), + types.DocumentAttributeFilename(os.path.basename(audio)) + ] + ) + elif audio.startswith("http"): + media = types.InputMediaDocumentExternal( + url=audio + ) + else: + try: + decoded = decode(audio) + fmt = " 24 else " +# +# 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 . + +from .dispatcher import Dispatcher diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py new file mode 100644 index 00000000..2710c4c0 --- /dev/null +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -0,0 +1,148 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + +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)) diff --git a/pyrogram/client/filters/__init__.py b/pyrogram/client/filters/__init__.py new file mode 100644 index 00000000..88ae14e3 --- /dev/null +++ b/pyrogram/client/filters/__init__.py @@ -0,0 +1,19 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + +from .filters import Filters diff --git a/pyrogram/client/filters/filter.py b/pyrogram/client/filters/filter.py new file mode 100644 index 00000000..feec51df --- /dev/null +++ b/pyrogram/client/filters/filter.py @@ -0,0 +1,57 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + + +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) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py new file mode 100644 index 00000000..ac2ecf67 --- /dev/null +++ b/pyrogram/client/filters/filters.py @@ -0,0 +1,218 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + +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 `.""" + + document = build("Document", lambda _, m: bool(m.document)) + """Filter messages that contain a :obj:`Document `.""" + + photo = build("Photo", lambda _, m: bool(m.photo)) + """Filter messages that contain a :obj:`Photo `.""" + + sticker = build("Sticker", lambda _, m: bool(m.sticker)) + """Filter messages that contain a :obj:`Sticker `.""" + + video = build("Video", lambda _, m: bool(m.video)) + """Filter messages that contain a :obj:`Video `.""" + + voice = build("Voice", lambda _, m: bool(m.voice)) + """Filter messages that contain a :obj:`Voice ` note.""" + + video_note = build("Voice", lambda _, m: bool(m.video_note)) + """Filter messages that contain a :obj:`VideoNote `.""" + + contact = build("Contact", lambda _, m: bool(m.contact)) + """Filter messages that contain a :obj:`Contact `.""" + + location = build("Location", lambda _, m: bool(m.location)) + """Filter messages that contain a :obj:`Location `.""" + + venue = build("Venue", lambda _, m: bool(m.venue)) + """Filter messages that contain a :obj:`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""" diff --git a/pyrogram/client/handlers/__init__.py b/pyrogram/client/handlers/__init__.py new file mode 100644 index 00000000..d9c48359 --- /dev/null +++ b/pyrogram/client/handlers/__init__.py @@ -0,0 +1,19 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + +from .handlers import MessageHandler, RawUpdateHandler diff --git a/pyrogram/client/handlers/handler.py b/pyrogram/client/handlers/handler.py new file mode 100644 index 00000000..0e46a205 --- /dev/null +++ b/pyrogram/client/handlers/handler.py @@ -0,0 +1,23 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + + +class Handler: + def __init__(self, callback: callable, filters=None): + self.callback = callback + self.filters = filters diff --git a/pyrogram/client/handlers/handlers.py b/pyrogram/client/handlers/handlers.py new file mode 100644 index 00000000..ca43282f --- /dev/null +++ b/pyrogram/client/handlers/handlers.py @@ -0,0 +1,93 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + +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 `): + Pass one or more filters to allow only a subset of messages to be passed + in your callback function. + + Other parameters: + client (:obj:`Client `): + The Client itself, useful when you want to call other API methods inside the message handler. + + message (:obj:`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 `): + 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 ` type. + + users (``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 (``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 ` + """ + + def __init__(self, callback: callable): + super().__init__(callback) diff --git a/pyrogram/client/input_media_photo.py b/pyrogram/client/input_media_photo.py new file mode 100644 index 00000000..f1fe6acb --- /dev/null +++ b/pyrogram/client/input_media_photo.py @@ -0,0 +1,44 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + + +class InputMediaPhoto: + """This object represents a photo to be sent inside an album. + It is intended to be used with :obj:`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 ` or :obj:`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 diff --git a/pyrogram/client/input_media_video.py b/pyrogram/client/input_media_video.py new file mode 100644 index 00000000..c14767e5 --- /dev/null +++ b/pyrogram/client/input_media_video.py @@ -0,0 +1,64 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + + +class InputMediaVideo: + """This object represents a video to be sent inside an album. + It is intended to be used with :obj:`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 ` or :obj:`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 diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py new file mode 100644 index 00000000..427b98b1 --- /dev/null +++ b/pyrogram/client/message_parser.py @@ -0,0 +1,539 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# 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 . + +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( + " 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( + " 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( + " +# +# 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 . + +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("=")