diff --git a/README.rst b/README.rst index ae6f084f..67dbd3a5 100644 --- a/README.rst +++ b/README.rst @@ -1,158 +1,79 @@ |header| -Table of Contents -================= +Pyrogram |twitter| +================== -- `About`_ +.. code-block:: python - - `Features`_ + from pyrogram import Client, Filters - - `Requirements`_ - -- `Getting Started`_ - - - `Installation`_ - - - `Configuration`_ - - - `Usage`_ - -- `Documentation`_ - -- `Contribution`_ - -- `Feedback`_ - -- `License`_ + app = Client("my_account") -About -===== + @app.on_message(Filters.private) + def hello(client, message): + client.send_message( + message.chat.id, "Hello {}".format(message.from_user.first_name)) + + + app.start() + app.idle() **Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for building -custom Telegram applications in Python that interact with the MTProto API as both User and Bot. +custom Telegram applications that interact with the MTProto API as both User and Bot. Features -------- -- **Easy to setup**: Pyrogram can be easily installed using pip and requires very few lines of code to get started with. - -- **Easy to use**: Pyrogram provides idiomatic, clean and readable Python code making the Telegram API simple to use. - -- **High-level**: Pyrogram automatically handles all the low-level details of communication with Telegram servers. - -- **Updated**: Pyrogram makes use of the latest Telegram MTProto API version, currently Layer 76. - -- **Fast**: Pyrogram critical parts are boosted up by `TgCrypto`_, a high-performance Crypto Library written in pure C. - -- **Documented**: Pyrogram API methods are documented and resemble the well established Telegram Bot API, - thus offering a familiar look to Bot developers. - -- **Full API support**: Beside the simple Bot API-like methods, Pyrogram also provides an easy access to every single - Telegram MTProto API method allowing you to programmatically execute any action an official client is able to do, and more. - +- **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. +- **High-level**: The low-level details of MTProto are abstracted and automatically handled. +- **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. +- **Updated** to the latest Telegram API version, currently Layer 76 running on MTProto 2.0. +- **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. +- **Full API**, allowing to execute any advanced action an official client is able to do, and more. Requirements ------------ - Python 3.4 or higher. +- A `Telegram API key`_. -- A Telegram API key. - +Installing +---------- + +.. code:: shell + + pip3 install pyrogram Getting Started -=============== +--------------- -Installation +- The Docs contain lots of resources to help you getting started with Pyrogram: https://docs.pyrogram.ml. +- Reading Examples_ in this repository is also a good way for learning how things work. +- Seeking extra help? Don't be shy, come join and ask our Community_! +- For other requests you can send an Email_ or a Message_. + +Contributing ------------ -- You can install and upgrade Pyrogram using pip: - - .. code:: shell - - $ pip3 install --upgrade pyrogram - -Configuration -------------- - -- Create a new ``config.ini`` file at the root of your working directory, copy-paste - the following and replace the **api_id** and **api_hash** values with `your own`_: - - .. code:: ini - - [pyrogram] - api_id = 12345 - api_hash = 0123456789abcdef0123456789abcdef - -Usage ------ - -- And here is how Pyrogram looks like: - - .. code:: python - - from pyrogram import Client - - client = Client("example") - client.start() - - client.send_message("me", "Hi there! I'm using Pyrogram") - - client.stop() - -That's all you need for getting started with Pyrogram. For more detailed information, -please refer to the Documentation_ and the Examples_ folder. - - -Documentation -============= - -- The entire Pyrogram documentation resides at https://docs.pyrogram.ml. - - -Contribution -============ - Pyrogram is brand new! **You are welcome to try it and help make it better** by either submitting pull requests or reporting issues/bugs as well as suggesting best practices, ideas, enhancements on both code and documentation. Any help is appreciated! - -Feedback -======== - -Means for getting in touch: - -- `Community`_ -- `GitHub`_ -- `Email`_ - - -License -======= +Copyright & License +------------------- - Copyright (C) 2017-2018 Dan Tès - -- Licensed under the terms of the - `GNU Lesser General Public License v3 or later (LGPLv3+)`_ - +- Licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_ .. _`Telegram`: https://telegram.org/ - -.. _`your own`: https://docs.pyrogram.ml/start/ProjectSetup#api-keys - -.. _`Examples`: https://github.com/pyrogram/pyrogram/blob/master/examples/README.md - +.. _`Telegram API key`: https://docs.pyrogram.ml/start/ProjectSetup#api-keys .. _`Community`: https://t.me/PyrogramChat - -.. _`bot-like`: https://core.telegram.org/bots/api#available-methods - +.. _`Examples`: https://github.com/pyrogram/pyrogram/tree/master/examples .. _`GitHub`: https://github.com/pyrogram/pyrogram/issues - .. _`Email`: admin@pyrogram.ml - +.. _`Message`: https://t.me/haskell .. _TgCrypto: https://github.com/pyrogram/tgcrypto - .. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser .. |header| raw:: html @@ -190,6 +111,9 @@ License

+.. |twitter| image:: https://media.pyrogram.ml/images/twitter.svg + :target: https://twitter.com/intent/tweet?text=Build%20custom%20Telegram%20applications%20with%20Pyrogram&url=https://github.com/pyrogram/pyrogram&hashtags=Telegram,MTProto,Python + .. |logo| image:: https://pyrogram.ml/images/logo.png :target: https://pyrogram.ml :alt: Pyrogram diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index e1e95328..15b1ec76 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -22,20 +22,93 @@ import shutil HOME = "compiler/api" DESTINATION = "pyrogram/api" -notice_path = "NOTICE" +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<>.]+)") FLAGS_RE_3 = re.compile(r"flags:#") +INT_RE = re.compile(r"int(\d+)") core_types = ["int", "long", "int128", "int256", "double", "bytes", "string", "Bool"] +types_to_constructors = {} +types_to_functions = {} +constructors_to_functions = {} + + +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``" + elif "int" in t: + size = INT_RE.match(t) + return "``int`` ``{}-bit``".format(size.group(1)) if size else "``int`` ``32-bit``" + elif t == "double": + return "``float`` ``64-bit``" + elif t == "string": + return "``str``" + else: + return "``{}``".format(t.lower()) + elif t == "true": + return "``bool``" + elif t == "Object" or t == "X": + return "Any object from :obj:`pyrogram.api.types`" + elif t == "!X": + return "Any method from :obj:`pyrogram.api.functions`" + elif t.startswith("Vector"): + return "List of " + get_docstring_arg_type(t.split("<", 1)[1][:-1], True, is_pyrogram_type) + 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:`{1} `".format( + "pyrogram." if is_pyrogram_type else "", + i.lstrip("pyrogram.") + ) + for i in t + ) + + if n: + t = t.split(", ") + t = ", ".join(t[:-1]) + " or " + t[-1] + + return t + + +def get_references(t: str): + t = constructors_to_functions.get(t) + + if t: + n = len(t) - 1 + + t = ", ".join( + ":obj:`{0} `".format(i) + for i in t + ) + + if n: + t = t.split(", ") + t = ", ".join(t[:-1]) + " and " + t[-1] + + return t 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 @@ -43,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): @@ -72,13 +146,17 @@ 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(notice_path, encoding="utf-8") as f: + 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 = [] for line in f.readlines(): @@ -106,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) @@ -129,23 +207,37 @@ def start(): Combinator( section, namespace, - name, + capit(name), "0x{}".format(id.zfill(8)), args, has_flags, - return_type + ".".join( + return_type.split(".")[:-1] + + [capit(return_type.split(".")[-1])] + ), + docs ) ) - by_types = {} for c in combinators: - return_type = capit(c.return_type) + return_type = c.return_type - if c.section == "types": - if return_type not in by_types: - by_types[return_type] = [] + if return_type.startswith("Vector"): + return_type = return_type.split("<")[1][:-1] - by_types[return_type].append(".".join(filter(None, [c.namespace, capit(c.name)]))) + d = types_to_constructors if c.section == "types" else types_to_functions + + if return_type not in d: + d[return_type] = [] + + d[return_type].append(".".join(filter(None, [c.namespace, c.name]))) + + for k, v in types_to_constructors.items(): + for i in v: + try: + constructors_to_functions[i] = types_to_functions[k] + except KeyError: + pass total = len(combinators) current = 0 @@ -182,52 +274,30 @@ 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 - is_optional = arg_type.startswith("flags.") + is_optional = FLAGS_RE.match(arg_type) + flag_number = is_optional.group(1) if is_optional else -1 arg_type = arg_type.split("?")[-1] - if arg_type in core_types: - if "int" in arg_type or arg_type == "long": - arg_type = ":obj:`int`" - elif arg_type == "double": - arg_type = ":obj:`float`" - else: - arg_type = ":obj:`{}`".format(arg_type.lower()) - elif arg_type == "true": - arg_type = ":obj:`bool`" - else: - if arg_type.startswith("Vector"): - sub_type = arg_type.split("<")[1][:-1] - - if sub_type in core_types: - if "int" in sub_type or sub_type == "long": - arg_type = "List of :obj:`int`" - elif sub_type == "double": - arg_type = "List of :obj:`float`" - else: - arg_type = "List of :obj:`{}`".format(sub_type.lower()) - else: - arg_type = "List of :class:`pyrogram.api.types.{}`".format( - ".".join( - sub_type.split(".")[:-1] - + [capit(sub_type.split(".")[-1])] - ) - ) - else: - arg_type = ":class:`pyrogram.api.types.{}`".format( - ".".join( - arg_type.split(".")[:-1] - + [capit(arg_type.split(".")[-1])] - ) - ) + # if c.namespace == "pyrogram": + # 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( + "{}{}: {}".format( arg_name, - arg_type, - " (optional)" if is_optional else "" + " (optional)".format(flag_number) if is_optional else "", + get_docstring_arg_type(arg_type, is_pyrogram_type=c.namespace == "pyrogram") ) ) @@ -236,81 +306,16 @@ def start(): else: docstring_args = "No parameters required." - docstring_args = "Attributes:\n ID (:obj:`int`): ``{}``\n\n ".format(c.id) + docstring_args + docstring_args = "Attributes:\n ID: ``{}``\n\n ".format(c.id) + docstring_args if c.section == "functions": - docstring_args += "\n\n Returns:\n " - if c.return_type in core_types: - if "int" in c.return_type or c.return_type == "long": - return_type = ":obj:`int`" - elif c.return_type == "double": - return_type = ":obj:`float`" - else: - return_type = ":obj:`{}`".format(c.return_type.lower()) - else: - if c.return_type.startswith("Vector"): - sub_type = c.return_type.split("<")[1][:-1] + docstring_args += "\n\n Raises:\n :obj:`Error `" + docstring_args += "\n\n Returns:\n " + get_docstring_arg_type(c.return_type) + else: + references = get_references(".".join(filter(None, [c.namespace, c.name]))) - if sub_type in core_types: - if "int" in sub_type or sub_type == "long": - return_type = "List of :obj:`int`" - elif sub_type == "double": - return_type = "List of :obj:`float`" - else: - return_type = "List of :obj:`{}`".format(c.return_type.lower()) - else: - if c.section == "functions": - try: - constructors = by_types[capit(sub_type)] - except KeyError: - return_type = "List of :class:`pyrogram.api.types.{}`".format( - ".".join( - sub_type.split(".")[:-1] - + [capit(sub_type.split(".")[-1])] - ) - ) - else: - constructors = ["List of :class:`pyrogram.api.types.{}`".format( - ".".join( - i.split(".")[:-1] - + [capit(i.split(".")[-1])] - ) - ) for i in constructors] - - return_type = " | ".join(constructors) - else: - return_type = "List of :class:`pyrogram.api.types.{}`".format( - ".".join( - sub_type.split(".")[:-1] - + [capit(sub_type.split(".")[-1])] - ) - ) - else: - if c.section == "functions": - try: - constructors = by_types[capit(c.return_type)] - except KeyError: - return_type = ":class:`pyrogram.api.types.{}`".format( - ".".join(filter(None, [c.namespace, capit(c.name)])) - ) - else: - constructors = [":class:`pyrogram.api.types.{}`".format( - ".".join( - i.split(".")[:-1] - + [capit(i.split(".")[-1])] - ) - ) for i in constructors] - - return_type = " | ".join(constructors) - else: - return_type = ":class:`pyrogram.api.types.{}`".format( - ".".join(filter(None, [c.namespace, capit(c.name)])) - ) - - docstring_args += return_type - - if c.section == "functions": - docstring_args += "\n\n Raises:\n :class:`pyrogram.Error`" + if references: + docstring_args += "\n\n See Also:\n This object can be returned by " + references + "." if c.has_flags: write_flags = [] @@ -397,22 +402,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") @@ -443,5 +464,5 @@ def start(): if "__main__" == __name__: HOME = "." DESTINATION = "../../pyrogram/api" - notice_path = "../../NOTICE" + NOTICE_PATH = "../../NOTICE" start() diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl new file mode 100644 index 00000000..4a6fb182 --- /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 photo:flags.4?ChatPhoto = 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 file_size:flags.0?int date:flags.1?int width:int height:int = pyrogram.PhotoSize; +pyrogram.audio#b0700006 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int date:flags.4?int duration:int performer:flags.5?string title:flags.6?string = 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 date:flags.4?int = pyrogram.Document; +pyrogram.video#b0700008 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int date:flags.4?int width:int height:int duration:int = pyrogram.Video; +pyrogram.voice#b0700009 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int date:flags.4?int duration:int = pyrogram.Voice; +pyrogram.videoNote#b0700010 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int date:flags.4?int length:int duration: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 thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int date:flags.4?int width:int height:int emoji:flags.5?string set_name:flags.6?string mask_position:flags.7?MaskPosition = 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/compiler/docs/compiler.py b/compiler/docs/compiler.py index 6a6345d1..494697de 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -20,17 +20,17 @@ import ast import os import shutil -home = "compiler/docs" -destination = "docs/source" +HOME = "compiler/docs" +DESTINATION = "docs/source" -functions_path = "pyrogram/api/functions" -types_path = "pyrogram/api/types" +FUNCTIONS_PATH = "pyrogram/api/functions" +TYPES_PATH = "pyrogram/api/types" -functions_base = "functions" -types_base = "types" +FUNCTIONS_BASE = "functions" +TYPES_BASE = "types" -shutil.rmtree(types_base, ignore_errors=True) -shutil.rmtree(functions_base, ignore_errors=True) +shutil.rmtree(TYPES_BASE, ignore_errors=True) +shutil.rmtree(FUNCTIONS_BASE, ignore_errors=True) def generate(source_path, base): @@ -57,9 +57,9 @@ def generate(source_path, base): if level: full_path = base + "/" + full_path - os.makedirs(os.path.dirname(destination + "/" + full_path), exist_ok=True) + os.makedirs(os.path.dirname(DESTINATION + "/" + full_path), exist_ok=True) - with open(destination + "/" + full_path, "w", encoding="utf-8") as f: + with open(DESTINATION + "/" + full_path, "w", encoding="utf-8") as f: f.write( page_template.format( title=name, @@ -94,7 +94,10 @@ def generate(source_path, base): inner_path = base + "/index" + ".rst" module = "pyrogram.api.{}".format(base) - with open(destination + "/" + inner_path, "w", encoding="utf-8") as f: + with open(DESTINATION + "/" + inner_path, "w", encoding="utf-8") as f: + if k == base: + f.write(":tocdepth: 1\n\n") + f.write( toctree.format( title=k.title(), @@ -111,20 +114,20 @@ def start(): global page_template global toctree - with open(home + "/template/page.txt", encoding="utf-8") as f: + with open(HOME + "/template/page.txt", encoding="utf-8") as f: page_template = f.read() - with open(home + "/template/toctree.txt", encoding="utf-8") as f: + with open(HOME + "/template/toctree.txt", encoding="utf-8") as f: toctree = f.read() - generate(types_path, types_base) - generate(functions_path, functions_base) + generate(TYPES_PATH, TYPES_BASE) + generate(FUNCTIONS_PATH, FUNCTIONS_BASE) if "__main__" == __name__: - functions_path = "../../pyrogram/api/functions" - types_path = "../../pyrogram/api/types" - home = "." - destination = "../../docs/source" + FUNCTIONS_PATH = "../../pyrogram/api/functions" + TYPES_PATH = "../../pyrogram/api/types" + HOME = "." + DESTINATION = "../../docs/source" start() diff --git a/compiler/error/compiler.py b/compiler/error/compiler.py index fee3be4d..aaefde9f 100644 --- a/compiler/error/compiler.py +++ b/compiler/error/compiler.py @@ -21,9 +21,9 @@ import os import re import shutil -home = "compiler/error" -dest = "pyrogram/api/errors/exceptions" -notice_path = "NOTICE" +HOME = "compiler/error" +DEST = "pyrogram/api/errors/exceptions" +NOTICE_PATH = "NOTICE" def snek(s): @@ -38,12 +38,12 @@ def caml(s): def start(): - shutil.rmtree(dest, ignore_errors=True) - os.makedirs(dest) + shutil.rmtree(DEST, ignore_errors=True) + os.makedirs(DEST) - files = [i for i in os.listdir("{}/source".format(home))] + files = [i for i in os.listdir("{}/source".format(HOME))] - with open(notice_path, encoding="utf-8") as f: + with open(NOTICE_PATH, encoding="utf-8") as f: notice = [] for line in f.readlines(): @@ -51,7 +51,7 @@ def start(): notice = "\n".join(notice) - with open("{}/all.py".format(dest), "w", encoding="utf-8") as f_all: + with open("{}/all.py".format(DEST), "w", encoding="utf-8") as f_all: f_all.write(notice + "\n\n") f_all.write("count = {count}\n\n") f_all.write("exceptions = {\n") @@ -63,7 +63,7 @@ def start(): f_all.write(" {}: {{\n".format(code)) - init = "{}/__init__.py".format(dest) + init = "{}/__init__.py".format(DEST) if not os.path.exists(init): with open(init, "w", encoding="utf-8") as f_init: @@ -72,8 +72,8 @@ def start(): with open(init, "a", encoding="utf-8") as f_init: f_init.write("from .{}_{} import *\n".format(name.lower(), code)) - with open("{}/source/{}".format(home, i), encoding="utf-8") as f_csv, \ - open("{}/{}_{}.py".format(dest, name.lower(), code), "w", encoding="utf-8") as f_class: + with open("{}/source/{}".format(HOME, i), encoding="utf-8") as f_csv, \ + open("{}/{}_{}.py".format(DEST, name.lower(), code), "w", encoding="utf-8") as f_class: reader = csv.reader(f_csv, delimiter="\t") super_class = caml(name) @@ -98,10 +98,10 @@ def start(): sub_classes.append((sub_class, id, message)) - with open("{}/template/class.txt".format(home), "r", encoding="utf-8") as f_class_template: + with open("{}/template/class.txt".format(HOME), "r", encoding="utf-8") as f_class_template: class_template = f_class_template.read() - with open("{}/template/sub_class.txt".format(home), "r", encoding="utf-8") as f_sub_class_template: + with open("{}/template/sub_class.txt".format(HOME), "r", encoding="utf-8") as f_sub_class_template: sub_class_template = f_sub_class_template.read() class_template = class_template.format( @@ -123,18 +123,18 @@ def start(): f_all.write("}\n") - with open("{}/all.py".format(dest), encoding="utf-8") as f: + with open("{}/all.py".format(DEST), encoding="utf-8") as f: content = f.read() - with open("{}/all.py".format(dest), "w", encoding="utf-8") as f: + with open("{}/all.py".format(DEST), "w", encoding="utf-8") as f: f.write(re.sub("{count}", str(count), content)) print("Compiling Errors: [100%]") if "__main__" == __name__: - home = "." - dest = "../../pyrogram/api/errors/exceptions" - notice_path = "../../NOTICE" + HOME = "." + DEST = "../../pyrogram/api/errors/exceptions" + NOTICE_PATH = "../../NOTICE" start() diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index ffba8987..2ec8b7fd 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -48,4 +48,15 @@ ABOUT_TOO_LONG The about text is too long MULTI_MEDIA_TOO_LONG The album contains more than 10 items USERNAME_OCCUPIED The username is already in use BOT_INLINE_DISABLED The inline feature of the bot is disabled -INLINE_RESULT_EXPIRED The inline bot query expired \ No newline at end of file +INLINE_RESULT_EXPIRED The inline bot query expired +INVITE_HASH_INVALID The invite link hash is invalid +USER_ALREADY_PARTICIPANT The user is already a participant of this chat +TTL_MEDIA_INVALID This kind of media does not support self-destruction +MAX_ID_INVALID The max_id parameter is invalid +CHANNEL_INVALID The channel parameter is invalid +DC_ID_INVALID The dc_id parameter is invalid +LIMIT_INVALID The limit parameter is invalid +OFFSET_INVALID The offset parameter is invalid +EMAIL_INVALID The email provided is invalid +USER_IS_BOT A bot cannot send messages to other bots or to itself +WEBPAGE_CURL_FAILED Telegram could not fetch the provided URL \ No newline at end of file diff --git a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv index 4ee4f042..dcdc1ee0 100644 --- a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv +++ b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv @@ -1,2 +1,4 @@ id message AUTH_RESTART User authorization has restarted +RPC_CALL_FAIL Telegram is having internal problems. Please try again later +RPC_MCGET_FAIL Telegram is having internal problems. Please try again later \ No newline at end of file diff --git a/compiler/error/template/class.txt b/compiler/error/template/class.txt index fd240f4b..e520d80c 100644 --- a/compiler/error/template/class.txt +++ b/compiler/error/template/class.txt @@ -6,7 +6,7 @@ from ..error import Error class {super_class}(Error): {docstring} CODE = {code} - """:obj:`int`: Error Code""" + """``int``: Error Code""" NAME = __doc__ diff --git a/compiler/error/template/sub_class.txt b/compiler/error/template/sub_class.txt index a33503ec..e13e4cf2 100644 --- a/compiler/error/template/sub_class.txt +++ b/compiler/error/template/sub_class.txt @@ -1,7 +1,7 @@ class {sub_class}({super_class}): {docstring} ID = {id} - """:obj:`str`: Error ID""" + """``str``: Error ID""" MESSAGE = __doc__ diff --git a/docs/source/_static/pyrogram.ico b/docs/source/_images/favicon.ico similarity index 100% rename from docs/source/_static/pyrogram.ico rename to docs/source/_images/favicon.ico diff --git a/docs/source/_static/pyrogram.png b/docs/source/_images/logo.png similarity index 100% rename from docs/source/_static/pyrogram.png rename to docs/source/_images/logo.png diff --git a/docs/source/conf.py b/docs/source/conf.py index 439c0a1a..cecf047f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -36,11 +36,12 @@ from pyrogram import __version__ # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon' + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary' ] # Don't show source files on docs -html_show_sourcelink = False +html_show_sourcelink = True # Order by source, not alphabetically autodoc_member_order = 'bysource' @@ -84,41 +85,48 @@ language = None exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'tango' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- +html_title = "Pyrogram Documentation" + +# Overridden by template +html_show_copyright = False + # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' -html_theme_options = { - 'collapse_navigation': False -} - # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # -# html_theme_options = {} +html_theme_options = { + 'canonical_url': "https://docs.pyrogram.ml/", + 'collapse_navigation': False, + 'sticky_navigation': False, + 'logo_only': True, + 'display_version': True +} # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/pyrogram.png' +html_logo = '_images/logo.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = '_static/pyrogram.ico' +html_favicon = '_images/favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. diff --git a/docs/source/errors/UnknownError.rst b/docs/source/errors/UnknownError.rst index 18b4a4e8..030f3e02 100644 --- a/docs/source/errors/UnknownError.rst +++ b/docs/source/errors/UnknownError.rst @@ -3,6 +3,6 @@ Unknown Error .. module:: pyrogram.api.errors.UnknownError -.. autoclass:: pyrogram.api.errors.error.UnknownError +.. autoexception:: pyrogram.api.errors.error.UnknownError :members: :show-inheritance: diff --git a/docs/source/getting_started/BasicUsage.rst b/docs/source/getting_started/BasicUsage.rst deleted file mode 100644 index e9411c60..00000000 --- a/docs/source/getting_started/BasicUsage.rst +++ /dev/null @@ -1,73 +0,0 @@ -Basic Usage -=========== - -.. note:: - - All the snippets below assume you have successfully created and started a :obj:`pyrogram.Client` instance. - You also must be authorized, that is, a valid *.session file does exist in your working directory. - -Simple API Access ------------------ - -The easiest way to interact with the API is via the :obj:`pyrogram.Client` class which exposes bot-like_ methods. -The purpose of this Client class is to make it even simpler to work with Telegram's API by abstracting the -raw functions listed in the API scheme. - -The result is a much cleaner interface that allows you to: - -- Get information about the authorized user: - - .. code-block:: python - - print(client.get_me()) - -- Send a message to yourself (Saved Messages): - - .. code-block:: python - - client.send_message( - chat_id="me", - text="Hi there! I'm using Pyrogram" - ) - -.. seealso:: For a complete list of the available methods have a look at the :obj:`pyrogram.Client` class. - -.. _using-raw-functions: - -Using Raw Functions -------------------- - -If you want **complete**, low-level access to the Telegram API you have to use the raw -:obj:`functions ` and :obj:`types ` exposed by the ``pyrogram.api`` -package and call any Telegram API method you wish using the :obj:`send ` method provided by -the Client class. - -Here some examples: - -- Update first name, last name and bio: - - .. code-block:: python - - from pyrogram.api import functions - - client.send( - functions.account.UpdateProfile( - first_name="Dan", last_name="Tès", - about="Bio written from Pyrogram" - ) - ) - -- Share your Last Seen time only with your contacts: - - .. code-block:: python - - from pyrogram.api import functions, types - - client.send( - functions.account.SetPrivacy( - key=types.InputPrivacyKeyStatusTimestamp(), - rules=[types.InputPrivacyValueAllowContacts()] - ) - ) - -.. _bot-like: https://core.telegram.org/bots/api#available-methods \ No newline at end of file diff --git a/docs/source/getting_started/ProjectSetup.rst b/docs/source/getting_started/ProjectSetup.rst deleted file mode 100644 index bc2b6b74..00000000 --- a/docs/source/getting_started/ProjectSetup.rst +++ /dev/null @@ -1,66 +0,0 @@ -Project Setup -============= - -This section provides all the information you need to setup your project with Pyrogram. -There are a few steps you have to follow before you can actually use the library to make API calls. - -API Keys --------- - -The very first step requires you to obtain a valid Telegram API key. -If you already have one you can skip this, otherwise: - -#. Visit https://my.telegram.org/apps and log in with your Telegram Account. -#. Fill out the form to register a new Telegram application. -#. Done. The Telegram API key consists of two parts: the **App api_id** and the **App api_hash** - -.. important:: This key should be kept secret. - -Configuration -------------- - -Create a new ``config.ini`` file at the root of your working directory, -copy-paste the following and replace the **api_id** and **api_hash** values with `your own <#api-keys>`_: - -.. code-block:: ini - - [pyrogram] - api_id = 12345 - api_hash = 0123456789abcdef0123456789abcdef - -Authorization -------------- - -Telegram requires that users be authorized in order to use the API. -Pyrogram automatically manages this access, all you need to do is create an instance of -the :class:`pyrogram.Client` class by passing to it a ```` of your choice -and call the :obj:`start ` method: - -.. code-block:: python - - from pyrogram import Client - - client = Client(session_name="example") - client.start() - -This starts an interactive shell asking you to input your **phone number** (including your `Country Code`_) -and the **phone code** you will receive: - -.. code:: - - Enter phone number: +39********** - Is "+39**********" correct? (y/n): y - Enter phone code: 32768 - -After successfully authorizing yourself, a new file called ``example.session`` will be created allowing -Pyrogram executing API calls with your identity. - -.. important:: Your *.session file(s) must be kept secret. - -.. note:: - - The authorization process is executed only once. - However, the code above is always required; as long as a valid session file exists, - Pyrogram will use that and won't ask you to enter your phone number again when you restart your script. - -.. _`Country Code`: https://en.wikipedia.org/wiki/List_of_country_calling_codes \ No newline at end of file diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/getting_started/QuickInstallation.rst deleted file mode 100644 index 4d2912b8..00000000 --- a/docs/source/getting_started/QuickInstallation.rst +++ /dev/null @@ -1,37 +0,0 @@ -Quick Installation -================== - -The most straightforward and recommended way to install or upgrade Pyrogram is by using **pip**: - -.. code-block:: bash - - $ pip install --upgrade pyrogram - -Bleeding Edge -------------- - -If you want the latest development version of the library, you can either install it automatically with: - -.. code-block:: bash - - $ pip install git+https://github.com/pyrogram/pyrogram.git - -or manually, using: - -.. code-block:: bash - - $ git clone https://github.com/pyrogram/pyrogram.git - $ cd pyrogram - $ python setup.py install - -Verifying ---------- - -To verify that Pyrogram is correctly installed, open a Python shell and try to import it. -If no errors show up you are good to go. - -.. code-block:: bash - - >>> import pyrogram - >>> pyrogram.__version__ - '0.3.2' diff --git a/docs/source/index.rst b/docs/source/index.rst index dfa6d67e..fced42bb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,16 +3,10 @@ Welcome to Pyrogram .. raw:: html -

- Watch - Star - Fork -

- @@ -32,83 +26,72 @@ Welcome to Pyrogram

- Scheme Layer 75 - - MTProto v2.0 + + TgCrypto

+.. code-block:: python + + from pyrogram import Client, Filters + + app = Client("my_account") + + + @app.on_message(Filters.private) + def hello(client, message): + client.send_message( + message.chat.id, "Hello {}".format(message.from_user.first_name)) + + + app.start() + app.idle() + +Welcome to Pyrogram's Documentation! Here you can find resources for learning how to use the library. +Contents are organized by topic and can be accessed from the sidebar, or by following them one by one using the Next +button at the end of each page. But first, here's a brief overview of what is this all about. + About ----- -Pyrogram is a fully functional Telegram Client Library written from the ground up in Python. -It offers **simple** and **complete** access to the Telegram Messenger API and is designed for Python developers -keen on building custom Telegram applications. +Pyrogram is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for building +custom Telegram applications that interact with the MTProto API as both User and Bot. Features -------- -- **Easy to setup**: Pyrogram can be easily installed and upgraded using **pip**, requires - a minimal set of dependencies (which are also automatically managed) and very few lines - of code to get started with. +- **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. +- **High-level**: The low-level details of MTProto are abstracted and automatically handled. +- **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. +- **Updated** to the latest Telegram API version, currently Layer 76 running on MTProto 2.0. +- **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. +- **Full API**, allowing to execute any advanced action an official client is able to do, and more. -- **Easy to use**: Pyrogram provides idiomatic, developer-friendly, clean and readable - Python code (either generated or hand-written) making the Telegram API simple to use. - -- **High level**: Pyrogram automatically handles all the low-level details of - communication with the Telegram servers by implementing the - `MTProto Mobile Protocol v2.0`_ and the mechanisms needed for establishing - a reliable connection. - -- **Fast**: Pyrogram's speed is boosted up by `TgCrypto`_, a high-performance, easy-to-install - crypto library written in C. - -- **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 75`_. - -- **Documented**: Pyrogram API public methods are documented and resemble the well - established Telegram Bot API, thus offering a familiar look to Bot developers. - -- **Full API support**: Beside the simple, bot-like methods offered by the Pyrogram API, - the library also provides a complete, low-level access to every single Telegram API method. - -Preview -------- - -.. code-block:: python - - from pyrogram import Client - - client = Client("example") - client.start() - - client.send_message("me", "Hi there! I'm using Pyrogram") - client.send_photo("me", "/home/dan/pic.jpg", "Nice photo!") - - client.stop() - -To get started, press Next. +To get started, press the Next button. .. toctree:: :hidden: :caption: Getting Started - getting_started/QuickInstallation - getting_started/ProjectSetup - getting_started/BasicUsage + start/QuickInstallation + start/ProjectSetup + start/BasicUsage .. toctree:: :hidden: :caption: Resources - resources/TextFormatting resources/UpdateHandling - resources/ErrorHandling - resources/ProxyServer + resources/SOCKS5Proxy + resources/TgCrypto resources/AutoAuthorization - resources/FastCrypto + resources/TextFormatting + resources/BotsInteraction + resources/ErrorHandling .. toctree:: :hidden: @@ -123,8 +106,6 @@ To get started, press Next. functions/index types/index -.. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto +.. _`Telegram`: https://telegram.org/ -.. _TgCrypto: https://docs.pyrogram.ml/resources/FastCrypto/ - -.. _`Layer 75`: https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl \ No newline at end of file +.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto/ diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index c0c4e667..806950a8 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -1,5 +1,52 @@ Client ====== +.. currentmodule:::: pyrogram.Client + .. autoclass:: pyrogram.Client :members: + + **Available methods** + + .. autosummary:: + :nosignatures: + + start + stop + idle + on_message + on_raw_update + add_handler + send + resolve_peer + get_me + send_message + forward_messages + send_photo + send_audio + send_document + send_video + send_voice + send_video_note + send_media_group + send_location + send_venue + send_contact + send_chat_action + send_sticker + download_media + get_user_profile_photos + edit_message_text + edit_message_caption + delete_messages + join_chat + leave_chat + export_chat_invite_link + enable_cloud_password + change_cloud_password + remove_cloud_password + add_contacts + delete_contacts + get_inline_bot_results + send_inline_bot_result + get_messages \ No newline at end of file diff --git a/docs/source/pyrogram/Emoji.rst b/docs/source/pyrogram/Emoji.rst new file mode 100644 index 00000000..6f0d7fda --- /dev/null +++ b/docs/source/pyrogram/Emoji.rst @@ -0,0 +1,6 @@ +Emoji +====== + +.. autoclass:: pyrogram.Emoji + :members: + :undoc-members: diff --git a/docs/source/pyrogram/Error.rst b/docs/source/pyrogram/Error.rst index b5474e73..96a140fa 100644 --- a/docs/source/pyrogram/Error.rst +++ b/docs/source/pyrogram/Error.rst @@ -1,3 +1,5 @@ +:tocdepth: 1 + Error ===== 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/InputPhoneContact.rst b/docs/source/pyrogram/InputPhoneContact.rst new file mode 100644 index 00000000..e5c4a20d --- /dev/null +++ b/docs/source/pyrogram/InputPhoneContact.rst @@ -0,0 +1,6 @@ +InputPhoneContact +================= + +.. autoclass:: pyrogram.InputPhoneContact + :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..a6d21ef3 --- /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 160766eb..9a86c11d 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -1,17 +1,45 @@ Pyrogram ======== -In this section you can find a detailed description of the Pyrogram API. +In this section you can find a detailed description of the Pyrogram package and its high-level API. -:obj:`pyrogram.Client` is the main class you have to deal with. -You will notice that methods are named after the well established `Telegram Bot API`_ and that most of them accept -the same parameters as well, thus offering a familiar look to Bot developers. +:class:`Client ` is the main class. It exposes easy-to-use methods that are named +after the well established `Telegram Bot API`_ methods, thus offering a familiar look to Bot developers. .. toctree:: Client + MessageHandler + RawUpdateHandler + Filters ChatAction ParseMode - InputMedia + Emoji 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/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst new file mode 100644 index 00000000..46f0809d --- /dev/null +++ b/docs/source/resources/AutoAuthorization.rst @@ -0,0 +1,64 @@ +Auto Authorization +================== + +Manually writing phone number, phone code and password on the terminal every time you want to login can be tedious. +Pyrogram is able to automate both **Log In** and **Sign Up** processes, all you need to do is pass the relevant +parameters when creating a new :class:`Client `. + +.. note:: If you omit any of the optional parameter required for the authorization, Pyrogram will ask you to + manually write it. For instance, if you don't want to set a ``last_name`` when creating a new account you + have to explicitly pass an empty string ""; the default value (None) will trigger the input() call. + +Log In +------- + +To automate the **Log In** process, pass your ``phone_number`` and ``password`` (if you have one) in the Client parameters. +If you want to retrieve the phone code programmatically, pass a callback function in the ``phone_code`` field — this +function must return the correct phone code as string (e.g., "12345") — otherwise, ignore this parameter, Pyrogram will +ask you to input the phone code manually. + +.. code-block:: python + + from pyrogram import Client + + def phone_code_callback(): + code = ... # Get your code programmatically + return code # Must be string, e.g., "12345" + + + app = Client( + session_name="example", + phone_number="39**********", + phone_code=phone_code_callback, + password="password" # (if you have one) + ) + + app.start() + print(app.get_me()) + +Sign Up +------- + +To automate the **Sign Up** process (i.e., automatically create a new Telegram account), simply fill **both** +``first_name`` and ``last_name`` fields alongside the other parameters; they will be used to automatically create a new +Telegram account in case the phone number you passed is not registered yet. + +.. code-block:: python + + from pyrogram import Client + + def phone_code_callback(): + code = ... # Get your code programmatically + return code # Must be string, e.g., "12345" + + + app = Client( + session_name="example", + phone_number="39**********", + phone_code=phone_code_callback, + first_name="Pyrogram", + last_name="" # Can be an empty string + ) + + app.start() + print(app.get_me()) \ No newline at end of file diff --git a/docs/source/resources/BotsInteraction.rst b/docs/source/resources/BotsInteraction.rst new file mode 100644 index 00000000..cbbe23c1 --- /dev/null +++ b/docs/source/resources/BotsInteraction.rst @@ -0,0 +1,40 @@ +Bots Interaction +================ + +Users can interact with other bots via plain text messages as well as inline queries. + +Inline Bots +----------- + +- If a bot accepts inline queries, you can call it by using + :meth:`get_inline_bot_results() ` to get the list of its inline results + for a query: + + .. code-block:: python + + # Get bot results for "Fuzz Universe" from the inline bot @vid + bot_results = app.get_inline_bot_results("vid", "Fuzz Universe") + + .. figure:: https://i.imgur.com/IAqLs54.png + :width: 90% + :align: center + :figwidth: 60% + + ``get_inline_bot_results()`` is the equivalent action of writing ``@vid Fuzz Universe`` and getting the + results list. + +- After you retrieved the bot results, you can use + :meth:`send_inline_bot_result() ` to send a chosen result to any chat: + + .. code-block:: python + + # Send the first result (bot_results.results[0]) to your own chat (Saved Messages) + app.send_inline_bot_result("me", bot_results.query_id, bot_results.results[0].id) + + .. figure:: https://i.imgur.com/wwxr7B7.png + :width: 90% + :align: center + :figwidth: 60% + + ``send_inline_bot_result()`` is the equivalent action of choosing a result from the list and sending it + to a chat. diff --git a/docs/source/resources/ErrorHandling.rst b/docs/source/resources/ErrorHandling.rst index 74a9091a..0d5cf6f9 100644 --- a/docs/source/resources/ErrorHandling.rst +++ b/docs/source/resources/ErrorHandling.rst @@ -29,8 +29,7 @@ Examples ) try: - # Something - pass + ... except BadRequest: pass except Flood: @@ -44,18 +43,18 @@ Examples except UnknownError: pass -Exceptions may also contain some informative values which can be useful. -e.g. :obj:`FloodWait ` holds the amount of seconds you have to wait before you -can try again. The value is always stored in the ``x`` field of the returned exception object: +Exception objects may also contain some informative values. +E.g.: :obj:`FloodWait ` holds the amount of seconds you have to wait +before you can try again. The value is always stored in the ``x`` field of the returned exception object: .. code-block:: python + import time from pyrogram.api.errors import FloodWait try: - # something - pass + ... except FloodWait as e: - print(e.x) + time.sleep(e.x) **TODO: Better explanation on how to deal with exceptions** \ No newline at end of file diff --git a/docs/source/resources/FastCrypto.rst b/docs/source/resources/FastCrypto.rst deleted file mode 100644 index 0cefd146..00000000 --- a/docs/source/resources/FastCrypto.rst +++ /dev/null @@ -1,37 +0,0 @@ -Fast Crypto -=========== - -Pyrogram's speed can be *dramatically* boosted up by installing TgCrypto_, a high-performance, easy-to-install crypto -library specifically written in C for Pyrogram [#f1]_. TgCrypto is a replacement for the painfully slow PyAES and -implements the crypto algorithms MTProto requires, namely AES-IGE and AES-CTR 256 bit. - -Installation ------------- - -.. code-block:: bash - - $ pip install --upgrade tgcrypto - - -.. note:: Being a C extension for Python, TgCrypto is an optional but *highly recommended* dependency; when TgCrypto - is not detected on your system, Pyrogram will automatically fall back to PyAES and will show you a warning. - -The reason about being an optional package is that TgCrypto requires some extra system tools in order to be compiled. -Usually the errors you receive when trying to install TgCrypto are enough to understand what you should do next. - -- **Windows**: Install `Visual C++ 2015 Build Tools `_. - -- **macOS**: A pop-up will automatically ask you to install the command line developer tools as soon as you issue the - installation command. - -- **Linux**: Depending on your distro, install a proper C compiler (``gcc``, ``clang``) and the Python header files - (``python3-dev``). - -- **Termux (Android)**: Install ``clang`` and ``python-dev`` packages. - -More help on the `Pyrogram group chat `_. - -.. _TgCrypto: https://github.com/pyrogram/tgcrypto - -.. [#f1] Although TgCrypto is intended for Pyrogram, it is shipped as a standalone package and can thus be used for - other projects too. \ No newline at end of file diff --git a/docs/source/resources/SOCKS5Proxy.rst b/docs/source/resources/SOCKS5Proxy.rst new file mode 100644 index 00000000..761899e6 --- /dev/null +++ b/docs/source/resources/SOCKS5Proxy.rst @@ -0,0 +1,50 @@ +SOCKS5 Proxy +============ + +Pyrogram supports proxies with and without authentication. This feature allows Pyrogram to exchange data with Telegram +through an intermediate SOCKS5 proxy server. + +Usage +----- + +- To use Pyrogram with a proxy, simply append the following to your ``config.ini`` file and replace the values + with your own settings: + + .. code-block:: ini + + [proxy] + enabled = True + hostname = 11.22.33.44 + port = 1080 + username = + password = + + To enable or disable the proxy without deleting your settings from the config file, + change the ``enabled`` value as follows: + + - ``1``, ``yes``, ``True`` or ``on``: Enables the proxy + - ``0``, ``no``, ``False`` or ``off``: Disables the proxy + +- Alternatively, you can setup your proxy without the need of the ``config.ini`` file by using the *proxy* parameter + in the Client class: + + .. code-block:: python + + from pyrogram import Client + + app = Client( + session_name="example", + proxy=dict( + hostname="11.22.33.44", + port=1080, + username="", + password="" + ) + ) + + app.start() + + ... + +.. note:: If your proxy doesn't require authorization you can omit ``username`` and ``password`` by either leaving the + values blank/empty or completely delete the lines. \ No newline at end of file diff --git a/docs/source/resources/TextFormatting.rst b/docs/source/resources/TextFormatting.rst index d17ab8e8..124d02da 100644 --- a/docs/source/resources/TextFormatting.rst +++ b/docs/source/resources/TextFormatting.rst @@ -8,8 +8,8 @@ Beside bold, italic, and pre-formatted code, **Pyrogram does also support inline Markdown Style -------------- -To use this mode, pass :obj:`pyrogram.ParseMode.MARKDOWN` or "markdown" in the *parse_mode* field when using -:obj:`send_message `. Use the following syntax in your message: +To use this mode, pass :obj:`MARKDOWN ` or "markdown" in the *parse_mode* field when using +:obj:`send_message() `. Use the following syntax in your message: .. code:: @@ -30,8 +30,8 @@ To use this mode, pass :obj:`pyrogram.ParseMode.MARKDOWN` or "markdown" in the * HTML Style ---------- -To use this mode, pass :obj:`pyrogram.ParseMode.HTML` or "html" in the *parse_mode* field when using -:obj:`send_message `. The following tags are currently supported: +To use this mode, pass :obj:`HTML ` or "html" in the *parse_mode* field when using +:obj:`send_message() `. The following tags are currently supported: .. code:: @@ -56,14 +56,14 @@ Examples .. code-block:: python - client.send_message( + app.send_message( chat_id="me", text=( - "**bold**\n" - "__italic__\n" - "[mention](tg://user?id=23122162)\n" - "[url](https://pyrogram.ml)\n" - "`code`\n" + "**bold**, " + "__italic__, " + "[mention](tg://user?id=23122162), " + "[url](https://pyrogram.ml), " + "`code`" ) ) @@ -71,7 +71,7 @@ Examples .. code-block:: python - client.send_message( + app.send_message( chat_id="me", text=( # Code block language is optional @@ -88,15 +88,15 @@ Examples from pyrogram import ParseMode - client.send_message( + app.send_message( chat_id="me", text=( - "bold, bold\n" - "italic, italic\n" - "inline URL\n" - "inline mention of a user\n" - "inline fixed-width code\n" - "
pre-formatted fixed-width code block
\n" + "bold, bold, " + "italic, italic, " + "inline URL, " + "inline mention of a user, " + "inline fixed-width code, " + "
pre-formatted fixed-width code block
" ), parse_mode=ParseMode.HTML ) diff --git a/docs/source/resources/TgCrypto.rst b/docs/source/resources/TgCrypto.rst new file mode 100644 index 00000000..734c48e4 --- /dev/null +++ b/docs/source/resources/TgCrypto.rst @@ -0,0 +1,32 @@ +TgCrypto +======== + +Pyrogram's speed can be *dramatically* boosted up by TgCrypto_, a high-performance, easy-to-install Telegram Crypto +Library specifically written in C for Pyrogram [#f1]_ as a Python extension. + +TgCrypto is a replacement for the much slower PyAES and implements the crypto algorithms Telegram requires, namely +**AES-IGE 256 bit** (used in MTProto v2.0) and **AES-CTR 256 bit** (used for CDN encrypted files). + +Installation +------------ + +.. code-block:: bash + + $ pip3 install --upgrade tgcrypto + +.. note:: Being a C extension for Python, TgCrypto is an optional but *highly recommended* dependency; when TgCrypto is + not detected in your system, Pyrogram will automatically fall back to PyAES and will show you a warning. + +The reason about being an optional package is that TgCrypto requires some extra system tools in order to be compiled. +The errors you receive when trying to install TgCrypto are system dependent, but also descriptive enough to understand +what you should do next: + +- **Windows**: Install `Visual C++ 2015 Build Tools `_. +- **macOS**: A pop-up will automatically ask you to install the command line developer tools. +- **Linux**: Install a proper C compiler (``gcc``, ``clang``) and the Python header files (``python3-dev``). +- **Termux (Android)**: Install ``clang`` and ``python-dev`` packages. + +.. _TgCrypto: https://github.com/pyrogram/tgcrypto + +.. [#f1] Although TgCrypto is intended for Pyrogram, it is shipped as a standalone package and can thus be used for + other Python projects too. diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 8fc12543..2b2c05d9 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -1,46 +1,188 @@ Update Handling =============== -Updates are events that happen in your Telegram account (incoming messages, new channel posts, user name changes, ...) -and can be handled by using a callback function, that is, a function called every time an ``Update`` is received from -Telegram. +Updates are events that happen in your Telegram account (incoming messages, new channel posts, new members join, ...) +and are handled by registering one or more callback functions with an Handler. There are multiple Handlers to choose +from, one for each kind of update: -To set an update handler simply call :obj:`set_update_handler ` -by passing the name of your defined callback function as argument *before* you start the Client. +- `MessageHandler <../pyrogram/MessageHandler.html>`_ +- `RawUpdateHandler <../pyrogram/RawUpdateHandler.html>`_ -Here's a complete example on how to set it up: +Registering an Handler +---------------------- -.. code-block:: python +We shall examine the :obj:`MessageHandler `, which will be in charge for handling +:obj:`Message ` objects. - from pyrogram import Client - - - def callback(update): - print(update) - - - client = Client(session_name="example") - client.set_update_handler(callback) - - client.start() - client.idle() - -The last line, :obj:`client.idle() ` is not strictly necessary but highly recommended; -it will block your script execution until you press :obj:`CTRL`:obj:`C` and automatically call the -:obj:`stop ` method which stops the Client and gently close the underlying connection. - -Examples --------- - -- Echo: +- The easiest and nicest way to register a MessageHandler is by decorating your function with the + :meth:`on_message() ` decorator. Here's a full example that prints out the content + of a message as soon as it arrives. .. code-block:: python - from pyrogram.api import types + from pyrogram import Client - def callback(update): - if isinstance(update, types.UpdateShortMessage) and not update.out: - client.send_message(update.user_id, update.message) + app = Client("my_account") - This checks if the update type is :obj:`UpdateShortMessage ` and that the - update is not generated by yourself (i.e., the message is not outgoing), then sends back the same message. \ No newline at end of file + + @app.on_message() + def my_handler(client, message): + print(message) + + + app.start() + app.idle() + +- If you prefer not to use decorators, there is an alternative way for registering Handlers. + This is useful, for example, when you want to keep your callback functions in separate files. + + .. code-block:: python + + from pyrogram import Client, MessageHandler + + + def my_handler(client, message): + print(message) + + + app = Client("my_account") + + app.add_handler(MessageHandler(my_handler)) + + app.start() + app.idle() + +Using Filters +------------- + +For a finer grained control over what kind of messages will be allowed or not in your callback functions, you can use +:class:`Filters `. + +- This example will show you how to **only** handle messages containing an + :obj:`Audio ` object and filter out any other message: + + .. code-block:: python + + from pyrogram import Filters + + + @app.on_message(Filters.audio) + def my_handler(client, message): + print(message) + +- or, without decorators: + + .. code-block:: python + + from pyrogram import Filters, MessageHandler + + + def my_handler(client, message): + print(message) + + + app.add_handler(MessageHandler(my_handler, Filters.audio)) + +Combining Filters +----------------- + +Filters can also be used in a more advanced way by combining more filters together using bitwise operators: + +- Use ``~`` to invert a filter (behaves like the ``not`` operator). +- Use ``&`` and ``|`` to merge two filters (behave like ``and``, ``or`` operators respectively). + +Here are some examples: + +- Message is a **text** message **and** is **not edited**. + + .. code-block:: python + + @app.on_message(Filters.text & ~Filters.edited) + def my_handler(client, message): + print(message) + +- Message is a **sticker** **and** is coming from a **channel or** a **private** chat. + + .. code-block:: python + + @app.on_message(Filters.sticker & (Filters.channel | Filters.private)) + def my_handler(client, message): + print(message) + +Advanced Filters +---------------- + +Some filters, like :obj:`command() ` or :obj:`regex() ` +can also accept arguments: + +- Message is either a */start* or */help* **command**. + + .. code-block:: python + + @app.on_message(Filters.command(["start", "help"])) + def my_handler(client, message): + print(message) + +- Message is a **text** message matching the given **regex** pattern. + + .. code-block:: python + + @app.on_message(Filters.regex("pyrogram")) + def my_handler(client, message): + print(message) + +More handlers using different filters can also live together. + +.. code-block:: python + + @app.on_message(Filters.command("start")) + def start_command(client, message): + print("This is the /start command") + + + @app.on_message(Filters.command("help")) + def help_command(client, message): + print("This is the /help command") + + + @app.on_message(Filters.chat("PyrogramChat")) + def from_pyrogramchat(client, message): + print("New message in @PyrogramChat") + +Handler Groups +-------------- + +If you register handlers with overlapping filters, only the first one is executed and any other handler will be ignored. + +In order to process the same message more than once, you can register your handler in a different group. +Groups are identified by a number (number 0 being the default) and are sorted. This means that a lower group number has +a higher priority. + +For example, in: + +.. code-block:: python + + @app.on_message(Filters.text | Filters.sticker) + def text_or_sticker(client, message): + print("Text or Sticker") + + + @app.on_message(Filters.text) + def just_text(client, message): + print("Just Text") + +``just_text`` is never executed. To enable it, simply register the function using a different group: + +.. code-block:: python + + @app.on_message(Filters.text, group=1) + def just_text(client, message): + print("Just Text") + +or, if you want ``just_text`` to be fired *before* ``text_or_sticker``: + +.. code-block:: python + + @app.on_message(Filters.text, group=-1) + def just_text(client, message): + print("Just Text") \ No newline at end of file diff --git a/docs/source/sitemap.py b/docs/source/sitemap.py new file mode 100644 index 00000000..539bac0d --- /dev/null +++ b/docs/source/sitemap.py @@ -0,0 +1,68 @@ +# 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 datetime +import os +import re + +canonical = "https://docs.pyrogram.ml" + +dirs = { + "start": ("weekly", 0.9), + "resources": ("weekly", 0.8), + "pyrogram": ("weekly", 0.8), + "functions": ("monthly", 0.7), + "types": ("monthly", 0.7), + "errors": ("weekly", 0.6) +} + + +def now(): + return datetime.datetime.today().strftime("%Y-%m-%d") + + +with open("sitemap.xml", "w") as f: + f.write("\n") + f.write("\n") + + urls = [(canonical, now(), "weekly", 1.0)] + + + def search(path): + try: + for j in os.listdir(path): + search("{}/{}".format(path, j)) + except NotADirectoryError: + d = path.split("/")[0] + path = "{}/{}".format(canonical, path.split(".")[0]) + path = re.sub("^(.+)/index$", "\g<1>", path) + urls.append((path, now(), dirs[d][0], dirs[d][1])) + + + for i in dirs.keys(): + search(i) + + for i in urls: + f.write(" \n") + f.write(" {}\n".format(i[0])) + f.write(" {}\n".format(i[1])) + f.write(" {}\n".format(i[2])) + f.write(" {}\n".format(i[3])) + f.write(" \n\n") + + f.write("") diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst new file mode 100644 index 00000000..94dfd00c --- /dev/null +++ b/docs/source/start/BasicUsage.rst @@ -0,0 +1,97 @@ +Basic Usage +=========== + +.. note:: + + All the snippets below assume you have successfully created and started a :class:`Client ` + instance. You also must be authorized, that is, a valid *.session file does exist in your working directory. + +Simple API Access +----------------- + +The easiest way to interact with the Telegram API is via the :class:`Client ` class, which +exposes `Bot API-like`_ methods: + +- Get information about the authorized user: + + .. code-block:: python + + print(app.get_me()) + +- Send a message to yourself (Saved Messages): + + .. code-block:: python + + app.send_message("me", "Hi there! I'm using Pyrogram") + +- Upload a new photo (with caption): + + .. code-block:: python + + app.send_photo("me", "/home/dan/perla.jpg", "Cute!") + +.. seealso:: For a complete list of the available methods and an exhaustive description for each of them, have a look + at the :class:`Client ` class. + +.. _using-raw-functions: + +Using Raw Functions +------------------- + +If you want **complete**, low-level access to the Telegram API you have to use the raw +:mod:`functions ` and :mod:`types ` exposed by the ``pyrogram.api`` +package and call any Telegram API method you wish using the :meth:`send() ` method provided by +the Client class. + +Here some examples: + +- Update first name, last name and bio: + + .. code-block:: python + + from pyrogram.api import functions + + ... + + app.send( + functions.account.UpdateProfile( + first_name="Dan", last_name="Tès", + about="Bio written from Pyrogram" + ) + ) + +- Share your Last Seen time only with your contacts: + + .. code-block:: python + + from pyrogram.api import functions, types + + ... + + app.send( + functions.account.SetPrivacy( + key=types.InputPrivacyKeyStatusTimestamp(), + rules=[types.InputPrivacyValueAllowContacts()] + ) + ) + +- Invite users to your channel/supergroup: + + .. code-block:: python + + from pyrogram.api import functions, types + + ... + + app.send( + functions.channels.InviteToChannel( + channel=app.resolve_peer(123456789), # ID or Username + users=[ # The users you want to invite + app.resolve_peer(23456789), # By ID + app.resolve_peer("username"), # By username + app.resolve_peer("393281234567"), # By phone number + ] + ) + ) + +.. _`Bot API-like`: https://core.telegram.org/bots/api#available-methods \ No newline at end of file diff --git a/docs/source/start/ProjectSetup.rst b/docs/source/start/ProjectSetup.rst new file mode 100644 index 00000000..4e397413 --- /dev/null +++ b/docs/source/start/ProjectSetup.rst @@ -0,0 +1,100 @@ +Project Setup +============= + +This section provides all the information you need to setup your project with Pyrogram. +There are a few steps you have to follow before you can actually use the library to make API calls. + +API Keys +-------- + +The very first step requires you to obtain a valid Telegram API key. +If you already have one you can skip this step, otherwise: + +#. Visit https://my.telegram.org/apps and log in with your Telegram Account. +#. Fill out the form to register a new Telegram application. +#. Done. The Telegram API key consists of two parts: the **App api_id** and the **App api_hash**. + +.. important:: This key should be kept secret. + +Configuration +------------- + +There are two ways to configure a Pyrogram application project, and you can choose the one that fits better for you: + +- Create a new ``config.ini`` file at the root of your working directory, copy-paste the following and replace the + **api_id** and **api_hash** values with `your own <#api-keys>`_. This is the preferred method because allows you + to keep your credentials out of your code without having to deal with how to load them: + + .. code-block:: ini + + [pyrogram] + api_id = 12345 + api_hash = 0123456789abcdef0123456789abcdef + +- Alternatively, you can pass your API key to Pyrogram by simply using the *api_id* and *api_hash* + parameters of the Client class. This way you can have full control on how to store and load your credentials: + + .. code-block:: python + + from pyrogram import Client + + app = Client( + session_name="my_account", + api_id=12345 + api_hash="0123456789abcdef0123456789abcdef" + ) + +.. note:: The examples below assume you have created a ``config.ini`` file, thus they won't show the *api_id* + and *api_hash* parameters usage. + +User Authorization +------------------ + +In order to use the API, Telegram requires that Users be authorized via their phone numbers. +Pyrogram automatically manages this access, all you need to do is create an instance of +the :class:`Client ` class by passing to it a ``session_name`` of your choice +(e.g.: "my_account") and call the :meth:`start() ` method: + +.. code-block:: python + + from pyrogram import Client + + app = Client("my_account") + app.start() + +This starts an interactive shell asking you to input your **phone number** (including your `Country Code`_) +and the **phone code** you will receive: + +.. code:: + + Enter phone number: +39********** + Is "+39**********" correct? (y/n): y + Enter phone code: 32768 + +After successfully authorizing yourself, a new file called ``my_account.session`` will be created allowing +Pyrogram executing API calls with your identity. This file will be loaded again when you restart your app, +and as long as you keep the session alive, Pyrogram won't ask you again to enter your phone number. + +.. important:: Your *.session file(s) must be kept secret. + +Bot Authorization +----------------- + +Being written entirely from the ground up, Pyrogram is also able to authorize Bots. +Bots are a special kind of users which also make use of MTProto. This means that you can use Pyrogram to +execute API calls with a Bot identity. + +Instead of phone numbers, Bots are authorized via their tokens which are created by BotFather_: + +.. code-block:: python + + from pyrogram import Client + + app = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") + app.start() + +That's all, no further action is needed. The session file will be named after the Bot user_id, which is +``123456.session`` for the example above. + +.. _`Country Code`: https://en.wikipedia.org/wiki/List_of_country_calling_codes +.. _BotFather: https://t.me/botfather \ No newline at end of file diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst new file mode 100644 index 00000000..7044bfee --- /dev/null +++ b/docs/source/start/QuickInstallation.rst @@ -0,0 +1,37 @@ +Quick Installation +================== + +- The easiest way to install and upgrade Pyrogram is by using **pip**: + + .. code-block:: bash + + $ pip3 install --upgrade pyrogram + +- or, with TgCrypto_ (recommended): + + .. code-block:: bash + + $ pip3 install --upgrade pyrogram[tgcrypto] + +Bleeding Edge +------------- + +If you want the latest development version of the library, you can install it with: + +.. code-block:: bash + + $ pip3 install --upgrade git+https://github.com/pyrogram/pyrogram.git + +Verifying +--------- + +To verify that Pyrogram is correctly installed, open a Python shell and import it. +If no error shows up you are good to go. + +.. code-block:: bash + + >>> import pyrogram + >>> pyrogram.__version__ + '0.7.0' + +.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 66ca9405..4d709e99 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,16 +5,13 @@ You can start with [hello_world.py](https://github.com/pyrogram/pyrogram/blob/ma with the more advanced examples. Every script is working right away (provided you correctly set up your credentials), meaning -you can simply copy-paste and run, the only things you have to change are the target chats (username, id) and file paths for -sending media (photo, video, ...). +you can simply copy-paste and run, the only things you have to change are your session names and the target chats -- [**hello_world.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py) +- [**echo_bot.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/echo_bot.py) - [**get_history.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_history.py) - [**get_participants.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants.py) - [**get_participants2.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants2.py) +- [**hello_world.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py) - [**inline_bots.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/inline_bots.py) -- [**updates.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/updates.py) -- [**simple_echo.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/simple_echo.py) -- [**advanced_echo.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/advanced_echo.py) -- [**advanced_echo2.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/advanced_echo2.py) +- [**raw_update_handler.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/raw_update_handler.py) - [**welcome_bot.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/welcome_bot.py) diff --git a/examples/advanced_echo.py b/examples/advanced_echo.py deleted file mode 100644 index 9cc2fb6e..00000000 --- a/examples/advanced_echo.py +++ /dev/null @@ -1,64 +0,0 @@ -from pyrogram import Client -from pyrogram.api import types - -"""This is a more advanced example bot that will reply to all private and basic groups text messages -by also mentioning the Users. - -Beware! This script will make you reply to ALL new messages in private chats and in every basic group you are in. -Make sure you add an extra check to filter them: - -# Filter Groups by ID -if message.to_id.chat_id == MY_GROUP_ID: - ... -""" - - -def update_handler(client, update, users, chats): - if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (PM and Chats) - message = update.message - - if isinstance(message, types.Message): # Filter by Message to exclude MessageService and MessageEmpty - if isinstance(message.to_id, types.PeerUser): # Private Messages - text = '[{}](tg://user?id={}) said "{}" to me ([{}](tg://user?id={}))'.format( - users[message.from_id].first_name, - users[message.from_id].id, - message.message, - users[message.to_id.user_id].first_name, - users[message.to_id.user_id].id - ) - - client.send_message( - message.from_id, # Send the message to the private chat (from_id) - text, - reply_to_message_id=message.id - ) - else: # Group chats - text = '[{}](tg://user?id={}) said "{}" in **{}** group'.format( - users[message.from_id].first_name, - users[message.from_id].id, - message.message, - chats[message.to_id.chat_id].title - ) - - client.send_message( - message.to_id, # Send the message to the group chat (to_id) - text, - reply_to_message_id=message.id - ) - - -def main(): - # Pyrogram setup - client = Client("example") - - # Set the update_handler callback function - client.set_update_handler(update_handler) - client.start() - - # Blocks the program execution until you press CTRL+C then - # automatically stops the Client by closing the underlying connection - client.idle() - - -if __name__ == "__main__": - main() diff --git a/examples/advanced_echo2.py b/examples/advanced_echo2.py deleted file mode 100644 index 460c4cf8..00000000 --- a/examples/advanced_echo2.py +++ /dev/null @@ -1,55 +0,0 @@ -from pyrogram import Client -from pyrogram.api import types - -"""This example is similar to advanced_echo.py, except for the fact that it will reply to Supergroup text messages only. - -Beware! This script will make you reply to ALL new messages in every single supergroup you are in. -Make sure you add an extra check to filter them: - -# Filter Supergroups by ID -if message.to_id.channel_id == MY_SUPERGROUP_ID: - ... - -# Filter Supergroups by Username -if chats[message.to_id.channel_id].username == MY_SUPERGROUP_USERNAME: - ... -""" - - -def update_handler(client, update, users, chats): - # Channels and Supergroups share the same type (Channel). The .megagroup field is used to tell them apart, and is - # True for Supegroups, False for Channels. - if isinstance(update, types.UpdateNewChannelMessage): # Filter by UpdateNewChannelMessage (Channels/Supergroups) - message = update.message - - if isinstance(message, types.Message): # Filter by Message to exclude MessageService and MessageEmpty - if chats[message.to_id.channel_id].megagroup: # Only handle messages from Supergroups not Channels - text = '[{}](tg://user?id={}) said "{}" in **{}** supergroup'.format( - users[message.from_id].first_name, - users[message.from_id].id, - message.message, - chats[message.to_id.channel_id].title - ) - - client.send_message( - message.to_id, - text, - reply_to_message_id=message.id - ) - - -def main(): - # Pyrogram setup - client = Client("example") - - # Set the update_handler callback function - client.set_update_handler(update_handler) - client.start() - - # Blocks the program execution until you press CTRL+C then - # automatically stops the Client by closing the underlying connection - client.idle() - - -if __name__ == "__main__": - main() diff --git a/examples/data/pyrogram.png b/examples/data/pyrogram.png deleted file mode 100644 index 57bfefe8..00000000 Binary files a/examples/data/pyrogram.png and /dev/null differ diff --git a/examples/echo_bot.py b/examples/echo_bot.py new file mode 100644 index 00000000..c33449f6 --- /dev/null +++ b/examples/echo_bot.py @@ -0,0 +1,17 @@ +from pyrogram import Client, Filters + +"""This simple echo bot replies to every private text message""" + +app = Client("my_account") + + +@app.on_message(Filters.text & Filters.private) +def echo(client, message): + client.send_message( + message.chat.id, message.text, + reply_to_message_id=message.message_id + ) + + +app.start() +app.idle() diff --git a/examples/get_history.py b/examples/get_history.py index 34e6a34c..f7d28818 100644 --- a/examples/get_history.py +++ b/examples/get_history.py @@ -4,8 +4,8 @@ from pyrogram import Client from pyrogram.api import functions from pyrogram.api.errors import FloodWait -client = Client("example") -client.start() +app = Client("my_account") +app.start() target = "me" # "me" refers to your own chat (Saved Messages) history = [] # List that will contain all the messages of the target chat @@ -14,9 +14,9 @@ offset = 0 # Offset starts at 0 while True: try: - messages = client.send( + messages = app.send( functions.messages.GetHistory( - client.resolve_peer(target), + app.resolve_peer(target), 0, 0, offset, limit, 0, 0, 0 ) ) @@ -31,7 +31,7 @@ while True: history.extend(messages.messages) offset += limit -client.stop() +app.stop() # Now the "history" list contains all the messages sorted by date in # descending order (from the most recent to the oldest one) diff --git a/examples/get_participants.py b/examples/get_participants.py index 89b01f60..9f63424b 100644 --- a/examples/get_participants.py +++ b/examples/get_participants.py @@ -4,8 +4,8 @@ from pyrogram import Client from pyrogram.api import functions, types from pyrogram.api.errors import FloodWait -client = Client("example") -client.start() +app = Client("my_account") +app.start() target = "username" # Target channel/supergroup users = [] # List that will contain all the users of the target chat @@ -14,9 +14,9 @@ offset = 0 # Offset starts at 0 while True: try: - participants = client.send( + participants = app.send( functions.channels.GetParticipants( - channel=client.resolve_peer(target), + channel=app.resolve_peer(target), filter=types.ChannelParticipantsSearch(""), # Filter by empty string (search for all) offset=offset, limit=limit, @@ -35,6 +35,6 @@ while True: users.extend(participants.users) offset += limit -client.stop() +app.stop() # Now the "users" list contains all the members of the target chat diff --git a/examples/get_participants2.py b/examples/get_participants2.py index 23ed328f..799fddcc 100644 --- a/examples/get_participants2.py +++ b/examples/get_participants2.py @@ -15,8 +15,8 @@ This can be further improved by also searching for non-ascii characters (e.g.: J as some user names may not contain ascii letters at all. """ -client = Client("example") -client.start() +app = Client("my_account") +app.start() target = "username" # Target channel/supergroup username or id users = {} # To ensure uniqueness, users will be stored in a dictionary with user_id as key @@ -31,9 +31,9 @@ for q in queries: while True: try: - participants = client.send( + participants = app.send( functions.channels.GetParticipants( - channel=client.resolve_peer(target), + channel=app.resolve_peer(target), filter=types.ChannelParticipantsSearch(q), offset=offset, limit=limit, @@ -60,4 +60,4 @@ for q in queries: print("Total users: {}".format(len(users))) -client.stop() +app.stop() diff --git a/examples/hello_world.py b/examples/hello_world.py index 5c3f0304..cd338ed5 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,19 +1,18 @@ from pyrogram import Client +"""This example demonstrates a simple API methods usage""" + # Create a new Client -client = Client("example") +app = Client("my_account") # Start the Client -client.start() +app.start() # Send a message to yourself, Markdown is enabled by default -client.send_message("me", "Hi there! I'm using **Pyrogram**") - -# Send a photo with a formatted caption to yourself -client.send_photo("me", "data/pyrogram.png", "__This is a formatted__ **caption**") +app.send_message("me", "Hi there! I'm using **Pyrogram**") # Send a location to yourself -client.send_location("me", 51.500729, -0.124583) +app.send_location("me", 51.500729, -0.124583) # Stop the client -client.stop() +app.stop() diff --git a/examples/inline_bots.py b/examples/inline_bots.py index d5bd43fb..a0796281 100644 --- a/examples/inline_bots.py +++ b/examples/inline_bots.py @@ -1,15 +1,15 @@ from pyrogram import Client # Create a new Client -client = Client("example") +app = Client("my_account") # Start the Client -client.start() +app.start() # Get bot results for "Fuzz Universe" from the inline bot @vid -bot_results = client.get_inline_bot_results("vid", "Fuzz Universe") +bot_results = app.get_inline_bot_results("vid", "Fuzz Universe") # Send the first result (bot_results.results[0]) to your own chat (Saved Messages) -client.send_inline_bot_result("me", bot_results.query_id, bot_results.results[0].id) +app.send_inline_bot_result("me", bot_results.query_id, bot_results.results[0].id) # Stop the client -client.stop() +app.stop() diff --git a/examples/raw_update_handler.py b/examples/raw_update_handler.py new file mode 100644 index 00000000..2590c64b --- /dev/null +++ b/examples/raw_update_handler.py @@ -0,0 +1,14 @@ +from pyrogram import Client + +"""This example shows how to handle raw updates""" + +app = Client("my_account") + + +@app.on_raw_update() +def raw(client, update, users, chats): + print(update) + + +app.start() +app.idle() diff --git a/examples/simple_echo.py b/examples/simple_echo.py deleted file mode 100644 index 14abce2e..00000000 --- a/examples/simple_echo.py +++ /dev/null @@ -1,34 +0,0 @@ -from pyrogram import Client -from pyrogram.api import types - -"""This simple example bot will reply to all private text messages""" - - -def update_handler(client, update, users, chats): - if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (Private Messages) - message = update.message # type: types.Message - - if isinstance(message, types.Message): # Filter by Message to exclude MessageService and MessageEmpty - if isinstance(message.to_id, types.PeerUser): # Private Messages (Message from user) - client.send_message( - chat_id=message.from_id, - text=message.message, - reply_to_message_id=message.id - ) - - -def main(): - # Pyrogram setup - client = Client("example") - - # Set the update_handler callback function - client.set_update_handler(update_handler) - client.start() - - # Blocks the program execution until you press CTRL+C then - # automatically stops the Client by closing the underlying connection - client.idle() - - -if __name__ == "__main__": - main() diff --git a/examples/updates.py b/examples/updates.py deleted file mode 100644 index db28eeb6..00000000 --- a/examples/updates.py +++ /dev/null @@ -1,25 +0,0 @@ -from pyrogram import Client - - -# This function will be called every time a new Update is received from Telegram -def update_handler(client, update, users, chats): - # Send EVERY update that arrives to your own chat (Saved Messages) - # Use triple backticks to make the text look nicer. - client.send_message("me", "```\n" + str(update) + "```") - - -def main(): - # Pyrogram setup - client = Client("example") - - # Set the update_handler callback function - client.set_update_handler(update_handler) - client.start() - - # Blocks the program execution until you press CTRL+C then - # automatically stops the Client by closing the underlying connection - client.idle() - - -if __name__ == "__main__": - main() diff --git a/examples/welcome_bot.py b/examples/welcome_bot.py index d2e00a88..ac85d582 100644 --- a/examples/welcome_bot.py +++ b/examples/welcome_bot.py @@ -1,52 +1,32 @@ -from pyrogram import Client, Emoji -from pyrogram.api import types +from pyrogram import Client, Emoji, Filters """ This is the Welcome Bot in @PyrogramChat -The code is commented to help you understand each part - -It also uses the Emoji module to easily add emojis in your text messages +It uses the Emoji module to easily add emojis in your text messages and Filters +to make it only work for specific messages in a specific chat """ -# Your Supergroup ID -SUPERGROUP_ID = 1387666944 +app = Client("my_account") -def update_handler(client, update, users, chats): - # Supergroup messages are contained in the "UpdateNewChannelMessage" update type - if isinstance(update, types.UpdateNewChannelMessage): - message = update.message - # When a user joins, a "MessageService" is received - if isinstance(message, types.MessageService): - # Check if the message is sent to your SUPERGROUP_ID - if message.to_id.channel_id == SUPERGROUP_ID: - # A "MessageService" contains the "action" field. - # The action for user joins is "MessageActionChatAddUser" if the user - # joined using the username, otherwise is "MessageActionChatJoinedByLink" if - # the user joined a private group by link - if isinstance(message.action, (types.MessageActionChatAddUser, types.MessageActionChatJoinedByLink)): - # Now send the welcome message. Extra info about a user (such as the first_name, username, ...) - # are contained in the users dictionary and can be accessed by the user ID - client.send_message( - SUPERGROUP_ID, - "{} Welcome to [Pyrogram](https://docs.pyrogram.ml/)'s " - "group chat, [{}](tg://user?id={})!".format( - Emoji.SPARKLES, # Add an emoji - users[message.from_id].first_name, - users[message.from_id].id - ), - reply_to_message_id=message.id, - disable_web_page_preview=True - ) +@app.on_message(Filters.chat("PyrogramChat") & Filters.new_chat_members) +def welcome(client, message): + new_members = ", ".join([ + "[{}](tg://user?id={})".format(i.first_name, i.id) + for i in message.new_chat_members] + ) + + text = "{} Welcome to [Pyrogram](https://docs.pyrogram.ml/)'s group chat {}!".format( + Emoji.SPARKLES, + new_members + ) + + client.send_message( + message.chat.id, text, + reply_to_message_id=message.message_id, + disable_web_page_preview=True + ) -def main(): - client = Client("example") - client.set_update_handler(update_handler) - - client.start() - client.idle() - - -if __name__ == "__main__": - main() +app.start() +app.idle() diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 96ce0e30..4dd090be 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,12 +23,16 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès str: return dumps(self, cls=Encoder, indent=4) + def __bool__(self) -> bool: + return True + def __eq__(self, other) -> bool: return self.__dict__ == other.__dict__ @@ -50,6 +53,15 @@ class Object: return getattr(self, item) +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: @@ -60,7 +72,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/chat_action.py b/pyrogram/client/chat_action.py index d2a8c35a..95bffb8e 100644 --- a/pyrogram/client/chat_action.py +++ b/pyrogram/client/chat_action.py @@ -21,7 +21,7 @@ from pyrogram.api import types class ChatAction: """This class provides a convenient access to all Chat Actions available. - It is intended to be used with :obj:`pyrogram.Client.send_chat_action`. + Chat Actions are intended to be used with :meth:`send_chat_action() `. """ CANCEL = types.SendMessageCancelAction diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 96257ff3..a1033629 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 . import utils -from .input_media import InputMedia +from .dispatcher import Dispatcher +from .input_media_photo import InputMediaPhoto +from .input_media_video import InputMediaVideo from .style import Markdown, HTML from .syncer import Syncer @@ -124,6 +128,18 @@ class Client: DOWNLOAD_WORKERS = 1 OFFLINE_SLEEP = 300 + MEDIA_TYPE_ID = { + 0: "thumbnail", + 2: "photo", + 3: "voice", + 4: "video", + 5: "document", + 8: "sticker", + 9: "audio", + 10: "gif", + 13: "video_note" + } + def __init__(self, session_name: str, api_id: int or str = None, @@ -176,10 +192,64 @@ class Client: self.is_idle = None self.updates_queue = Queue() - self.update_queue = Queue() + self.updates_workers_list = [] + self.download_queue = Queue() + self.download_workers_list = [] + + 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 (``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. @@ -234,13 +304,26 @@ class Client: self.send(functions.updates.GetState()) for i in range(self.UPDATES_WORKERS): - Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start() + self.updates_workers_list.append( + Thread( + target=self.updates_worker, + name="UpdatesWorker#{}".format(i + 1) + ) + ) - for i in range(self.workers): - Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() + self.updates_workers_list[-1].start() for i in range(self.DOWNLOAD_WORKERS): - Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start() + self.download_workers_list.append( + Thread( + target=self.download_worker, + name="DownloadWorker#{}".format(i + 1) + ) + ) + + self.download_workers_list[-1].start() + + self.dispatcher.start() mimetypes.init() Syncer.add(self) @@ -252,18 +335,23 @@ class Client: if not self.is_started: raise ConnectionError("Client is already stopped") - self.is_started = False - self.session.stop() - for _ in range(self.UPDATES_WORKERS): self.updates_queue.put(None) - for _ in range(self.workers): - self.update_queue.put(None) + for i in self.updates_workers_list: + i.join() for _ in range(self.DOWNLOAD_WORKERS): self.download_queue.put(None) + for i in self.download_workers_list: + i.join() + + self.dispatcher.stop() + + self.is_started = False + self.session.stop() + Syncer.remove(self) def authorize_bot(self): @@ -530,73 +618,86 @@ class Client: while True: media = self.download_queue.get() - temp_file_path = "" - final_file_path = "" if media is None: break + temp_file_path = "" + final_file_path = "" + try: media, file_name, done, progress, path = media + file_id = media.file_id + size = media.file_size + directory, file_name = os.path.split(file_name) directory = directory or "downloads" - if isinstance(media, types.MessageMediaDocument): - document = media.document + try: + decoded = utils.decode(file_id) + fmt = " 24 else " 24: + volume_id = unpacked[4] + secret = unpacked[5] + local_id = unpacked[6] - for i in document.attributes: - if isinstance(i, types.DocumentAttributeFilename): - file_name = i.file_name - break - elif isinstance(i, types.DocumentAttributeSticker): - file_name = file_name.replace("doc", "sticker") - elif isinstance(i, types.DocumentAttributeAudio): - file_name = file_name.replace("doc", "audio") - elif isinstance(i, types.DocumentAttributeVideo): - file_name = file_name.replace("doc", "video") - elif isinstance(i, types.DocumentAttributeAnimated): - file_name = file_name.replace("doc", "gif") + media_type_str = Client.MEDIA_TYPE_ID.get(media_type, None) - temp_file_path = self.get_file( - dc_id=document.dc_id, - id=document.id, - access_hash=document.access_hash, - version=document.version, - size=document.size, - progress=progress - ) - elif isinstance(media, (types.MessageMediaPhoto, types.Photo)): - if isinstance(media, types.MessageMediaPhoto): - photo = media.photo + if media_type_str: + log.info("The file_id belongs to a {}".format(media_type_str)) else: - photo = media + raise FileIdInvalid("Unknown media type: {}".format(unpacked[0])) - if isinstance(photo, types.Photo): - if not file_name: - file_name = "photo_{}_{}.jpg".format( - datetime.fromtimestamp(photo.date).strftime("%Y-%m-%d_%H-%M-%S"), - self.rnd_id() - ) + file_name = file_name or getattr(media, "file_name", None) - photo_loc = photo.sizes[-1].location + if not file_name: + if media_type == 3: + extension = ".ogg" + elif media_type in (4, 10, 13): + extension = mimetypes.guess_extension(media.mime_type) or ".mp4" + elif media_type == 5: + extension = mimetypes.guess_extension(media.mime_type) or ".unknown" + elif media_type == 8: + extension = ".webp" + elif media_type == 9: + extension = mimetypes.guess_extension(media.mime_type) or ".mp3" + elif media_type == 0: + extension = ".jpg" + elif media_type == 2: + extension = ".jpg" + else: + continue - temp_file_path = self.get_file( - dc_id=photo_loc.dc_id, - volume_id=photo_loc.volume_id, - local_id=photo_loc.local_id, - secret=photo_loc.secret, - size=photo.sizes[-1].size, - progress=progress - ) + file_name = "{}_{}_{}{}".format( + media_type_str, + datetime.fromtimestamp(media.date or time.time()).strftime("%Y-%m-%d_%H-%M-%S"), + self.rnd_id(), + extension + ) + + temp_file_path = self.get_file( + dc_id=dc_id, + id=id, + access_hash=access_hash, + volume_id=volume_id, + local_id=local_id, + secret=secret, + size=size, + progress=progress + ) if temp_file_path: final_file_path = os.path.abspath(re.sub("\\\\", "/", os.path.join(directory, file_name))) @@ -680,7 +781,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( @@ -690,7 +791,7 @@ class Client: ) ) - self.update_queue.put(( + self.dispatcher.updates.put(( types.UpdateNewMessage( message=diff.new_messages[0], pts=updates.pts, @@ -700,30 +801,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) @@ -750,47 +828,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. @@ -826,7 +863,10 @@ class Client: self.api_id = parser.getint("pyrogram", "api_id") self.api_hash = parser.get("pyrogram", "api_hash") else: - raise AttributeError("No API Key found") + raise AttributeError( + "No API Key found. " + "More info: https://docs.pyrogram.ml/start/ProjectSetup#configuration" + ) if self.proxy: pass @@ -1113,7 +1153,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. @@ -1147,23 +1189,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 = utils.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 = utils.decode(audio) + 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(document, progress=progress) + + if os.path.exists(document): + file = self.save_file(document, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type=mimetypes.types_map.get("." + document.split(".")[-1], "text/plain"), + file=file, + attributes=[ + types.DocumentAttributeFilename(os.path.basename(document)) + ] + ) + elif document.startswith("http"): + media = types.InputMediaDocumentExternal( + url=document + ) + else: + try: + decoded = utils.decode(document) + fmt = " 24 else "` is returned. Raises: :class:`Error ` """ - file = self.save_file(sticker, progress=progress) + file = None + + if os.path.exists(sticker): + file = self.save_file(sticker, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type="image/webp", + file=file, + attributes=[ + types.DocumentAttributeFilename(os.path.basename(sticker)) + ] + ) + elif sticker.startswith("http"): + media = types.InputMediaDocumentExternal( + url=sticker + ) + else: + try: + decoded = utils.decode(sticker) + 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(video, progress=progress) - file_thumb = None if thumb is None else self.save_file(thumb) + + if os.path.exists(video): + thumb = None if thumb is None else self.save_file(thumb) + file = self.save_file(video, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type=mimetypes.types_map[".mp4"], + file=file, + thumb=thumb, + attributes=[ + types.DocumentAttributeVideo( + supports_streaming=supports_streaming or None, + duration=duration, + w=width, + h=height + ), + types.DocumentAttributeFilename(os.path.basename(video)) + ] + ) + elif video.startswith("http"): + media = types.InputMediaDocumentExternal( + url=video + ) + else: + try: + decoded = utils.decode(video) + 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(voice, progress=progress) + + if os.path.exists(voice): + file = self.save_file(voice, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type=mimetypes.types_map.get("." + voice.split(".")[-1], "audio/mpeg"), + file=file, + attributes=[ + types.DocumentAttributeAudio( + voice=True, + duration=duration + ) + ] + ) + elif voice.startswith("http"): + media = types.InputMediaDocumentExternal( + url=voice + ) + else: + try: + decoded = utils.decode(voice) + fmt = " 24 else "` is returned. Raises: :class:`Error ` """ - file = self.save_file(video_note, progress=progress) + file = None + + if os.path.exists(video_note): + file = self.save_file(video_note, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type=mimetypes.types_map[".mp4"], + file=file, + attributes=[ + types.DocumentAttributeVideo( + round_message=True, + duration=duration, + w=length, + h=length + ) + ] + ) + else: + try: + decoded = utils.decode(video_note) + fmt = " 24 else "` or + :obj:`InputMediaVideo ` objects describing photos and videos to be sent, must include 2–10 items. disable_notification (``bool``, optional): @@ -1726,66 +2026,104 @@ class Client: multi_media = [] for i in media: - if isinstance(i, InputMedia.Photo): - style = self.html if i.parse_mode.lower() == "html" else self.markdown - media = self.save_file(i.media) + style = self.html if i.parse_mode.lower() == "html" else self.markdown - media = self.send( - functions.messages.UploadMedia( - peer=self.resolve_peer(chat_id), - media=types.InputMediaUploadedPhoto( - file=media + if isinstance(i, InputMediaPhoto): + if os.path.exists(i.media): + media = self.send( + functions.messages.UploadMedia( + peer=self.resolve_peer(chat_id), + media=types.InputMediaUploadedPhoto( + file=self.save_file(i.media) + ) ) ) - ) - single_media = types.InputSingleMedia( - media=types.InputMediaPhoto( + media = types.InputMediaPhoto( id=types.InputPhoto( id=media.photo.id, access_hash=media.photo.access_hash ) - ), - random_id=self.rnd_id(), - **style.parse(i.caption) - ) + ) + else: + try: + decoded = utils.decode(i.media) + fmt = " 24 else " 24 else "` is returned. Raises: :class:`Error ` """ - return self.send( + r = self.send( functions.messages.SendMedia( peer=self.resolve_peer(chat_id), media=types.InputMediaGeoPoint( @@ -1846,6 +2184,13 @@ class Client: ) ) + 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_venue(self, chat_id: int or str, latitude: float, @@ -1887,12 +2232,12 @@ class Client: If the message is a reply, ID of the original message Returns: - On success, the sent Message is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` """ - return self.send( + r = self.send( functions.messages.SendMedia( peer=self.resolve_peer(chat_id), media=types.InputMediaVenue( @@ -1913,11 +2258,18 @@ class Client: ) ) + 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_contact(self, chat_id: int or str, phone_number: str, first_name: str, - last_name: str, + last_name: str = "", disable_notification: bool = None, reply_to_message_id: int = None): """Use this method to send phone contacts. @@ -1946,12 +2298,12 @@ class Client: If the message is a reply, ID of the original message. Returns: - On success, the sent Message is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` """ - return self.send( + r = self.send( functions.messages.SendMedia( peer=self.resolve_peer(chat_id), media=types.InputMediaContact( @@ -1966,6 +2318,13 @@ class Client: ) ) + 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_chat_action(self, chat_id: int or str, action: callable, @@ -1987,6 +2346,9 @@ class Client: progress (``int``, optional): Progress of the upload process. + Returns: + On success, True is returned. + Raises: :class:`Error ` """ @@ -2002,6 +2364,7 @@ class Client: ) ) + # TODO: Improvements for the new API def get_user_profile_photos(self, user_id: int or str, offset: int = 0, @@ -2061,12 +2424,15 @@ class Client: disable_web_page_preview (``bool``, optional): Disables link previews for links in this message. + Returns: + On success, the edited :obj:`Message ` is returned. + Raises: :class:`Error ` """ style = self.html if parse_mode.lower() == "html" else self.markdown - return self.send( + r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), id=message_id, @@ -2075,6 +2441,13 @@ class Client: ) ) + for i in r.updates: + if isinstance(i, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): + 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 edit_message_caption(self, chat_id: int or str, message_id: int, @@ -2100,12 +2473,15 @@ class Client: if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption. Defaults to Markdown. + Returns: + On success, the edited :obj:`Message ` is returned. + Raises: :class:`Error ` """ style = self.html if parse_mode.lower() == "html" else self.markdown - return self.send( + r = self.send( functions.messages.EditMessage( peer=self.resolve_peer(chat_id), id=message_id, @@ -2113,6 +2489,13 @@ class Client: ) ) + for i in r.updates: + if isinstance(i, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): + 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 delete_messages(self, chat_id: int or str, message_ids: list, @@ -2163,7 +2546,7 @@ class Client: ) ) - # TODO: Remove redundant code + # TODO: Improvements for the new API def save_file(self, path: str, file_id: int = None, @@ -2238,6 +2621,7 @@ class Client: finally: session.stop() + # TODO: Improvements for the new API def get_file(self, dc_id: int, id: int = None, @@ -2676,15 +3060,16 @@ class Client: return False def download_media(self, - message: types.Message, + message: pyrogram.Message, file_name: str = "", block: bool = True, progress: callable = None): """Use this method to download the media from a Message. Args: - message (:obj:`Message `): - The Message containing the media. + message (:obj:`Message ` | ``str``): + Pass a Message containing the media, the media itself (message.audio, message.video, ...) or + the file id as string. file_name (``str``, optional): A custom *file_name* to be used instead of the one provided by Telegram. @@ -2699,6 +3084,7 @@ class Client: progress (``callable``): Pass a callback function to view the download progress. The function must accept two arguments (current, total). + Note that this will not work in case you are downloading a media using a *file_id*. Other Parameters: current (``int``): @@ -2713,67 +3099,51 @@ class Client: Raises: :class:`Error ` """ - if isinstance(message, (types.Message, types.Photo)): - done = Event() - path = [None] - - if isinstance(message, types.Message): - media = message.media - else: - media = message - - if media is not None: - self.download_queue.put((media, file_name, done, progress, path)) + if isinstance(message, pyrogram.Message): + if message.photo: + media = message.photo[-1] + elif message.audio: + media = message.audio + elif message.document: + media = message.document + elif message.video: + media = message.video + elif message.voice: + media = message.voice + elif message.video_note: + media = message.video_note + elif message.sticker: + media = message.sticker else: return - - if block: - done.wait() - - return path[0] - - def download_photo(self, - photo: types.Photo or types.UserProfilePhoto or types.ChatPhoto, - file_name: str = "", - block: bool = True): - """Use this method to download a photo not contained inside a Message. - For example, a photo of a User or a Chat/Channel. - - Args: - photo (:obj:`Photo ` | :obj:`UserProfilePhoto ` | :obj:`ChatPhoto `): - The photo object. - - file_name (``str``, optional): - A custom *file_name* to be used instead of the one provided by Telegram. - By default, all photos are downloaded in the *downloads* folder in your working directory. - You can also specify a path for downloading photos in a custom location: paths that end with "/" - are considered directories. All non-existent folders will be created automatically. - - block (``bool``, optional): - Blocks the code execution until the photo has been downloaded. - Defaults to True. - - Returns: - On success, the absolute path of the downloaded photo as string is returned, None otherwise. - - Raises: - :class:`Error ` - """ - if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): - photo = types.Photo( - id=0, - access_hash=0, - date=int(time.time()), - sizes=[types.PhotoSize( - type="", - location=photo.photo_big, - w=0, - h=0, - size=0 - )] + elif isinstance(message, ( + pyrogram.PhotoSize, + pyrogram.Audio, + pyrogram.Document, + pyrogram.Video, + pyrogram.Voice, + pyrogram.VideoNote, + pyrogram.Sticker + )): + media = message + elif isinstance(message, str): + media = pyrogram.Document( + file_id=message, + file_size=0, + mime_type="" ) + else: + return - return self.download_media(photo, file_name, block) + done = Event() + path = [None] + + self.download_queue.put((media, file_name, done, progress, path)) + + if block: + done.wait() + + return path[0] def add_contacts(self, contacts: list): """Use this method to add contacts to your Telegram address book. @@ -2946,7 +3316,7 @@ class Client: A list of Message identifiers in the chat specified in *chat_id*. Returns: - List of the requested messages + On success, a list of the requested :obj:`Messages ` is returned. Raises: :class:`Error ` @@ -2964,4 +3334,21 @@ class Client: id=message_ids ) - return self.send(rpc) + r = self.send(rpc) + + users = {i.id: i for i in r.users} + chats = {i.id: i for i in r.chats} + + messages = [] + + for i in r.messages: + if isinstance(i, types.Message): + parser = message_parser.parse_message + elif isinstance(i, types.MessageService): + parser = message_parser.parse_message_service + else: + continue + + messages.append(parser(self, i, users, chats)) + + return messages diff --git a/pyrogram/client/dispatcher/__init__.py b/pyrogram/client/dispatcher/__init__.py new file mode 100644 index 00000000..c0cb368a --- /dev/null +++ b/pyrogram/client/dispatcher/__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 .dispatcher import Dispatcher diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py new file mode 100644 index 00000000..72bcbfc9 --- /dev/null +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -0,0 +1,159 @@ +# 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, MessageHandler + +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.workers_list = [] + self.updates = Queue() + self.groups = OrderedDict() + + def start(self): + for i in range(self.workers): + self.workers_list.append( + Thread( + target=self.update_worker, + name="UpdateWorker#{}".format(i + 1) + ) + ) + + self.workers_list[-1].start() + + def stop(self): + for _ in range(self.workers): + self.updates.put(None) + + for i in self.workers_list: + i.join() + + 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: + if not isinstance(handler, MessageHandler): + continue + + 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..3386c352 --- /dev/null +++ b/pyrogram/client/filters/filters.py @@ -0,0 +1,235 @@ +# 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 Filters available in Pyrogram. + Filters are intended to be used with the :obj:`MessageHandler `.""" + + 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 :obj:`Audio ` objects.""" + + document = build("Document", lambda _, m: bool(m.document)) + """Filter messages that contain :obj:`Document ` objects.""" + + photo = build("Photo", lambda _, m: bool(m.photo)) + """Filter messages that contain :obj:`Photo ` objects.""" + + sticker = build("Sticker", lambda _, m: bool(m.sticker)) + """Filter messages that contain :obj:`Sticker ` objects.""" + + video = build("Video", lambda _, m: bool(m.video)) + """Filter messages that contain :obj:`Video ` objects.""" + + voice = build("Voice", lambda _, m: bool(m.voice)) + """Filter messages that contain :obj:`Voice ` note objects.""" + + video_note = build("Voice", lambda _, m: bool(m.video_note)) + """Filter messages that contain :obj:`VideoNote ` objects.""" + + contact = build("Contact", lambda _, m: bool(m.contact)) + """Filter messages that contain :obj:`Contact ` objects.""" + + location = build("Location", lambda _, m: bool(m.location)) + """Filter messages that contain :obj:`Location ` objects.""" + + venue = build("Venue", lambda _, m: bool(m.venue)) + """Filter messages that contain :obj:`Venue ` objects.""" + + 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.""" + + 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.""" + + @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} + ) + ) + + service = build( + "Service", + lambda _, m: bool( + Filters.new_chat_members(m) + or Filters.left_chat_member(m) + or Filters.new_chat_title(m) + or Filters.new_chat_photo(m) + or Filters.delete_chat_photo(m) + or Filters.group_chat_created(m) + or Filters.supergroup_chat_created(m) + or Filters.channel_chat_created(m) + or Filters.migrate_to_chat_id(m) + or Filters.migrate_from_chat_id(m) + or Filters.pinned_message(m) + ) + ) + """Filter all service messages.""" + + media = build( + "Media", + lambda _, m: bool( + Filters.audio(m) + or Filters.document(m) + or Filters.photo(m) + or Filters.sticker(m) + or Filters.video(m) + or Filters.voice(m) + or Filters.video_note(m) + or Filters.contact(m) + or Filters.location(m) + or Filters.venue(m) + ) + ) + """Filter all media 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..e909e218 --- /dev/null +++ b/pyrogram/client/handlers/handlers.py @@ -0,0 +1,95 @@ +# 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. Used to handle text, media and service messages coming from + any chat (private, group, channel). It is intended to be used with + :meth:`add_handler() ` + + 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. Used to handle raw updates. It is intended to be used with + :meth:`add_handler() ` + + 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..3f0e4488 --- /dev/null +++ b/pyrogram/client/input_media_photo.py @@ -0,0 +1,46 @@ +# 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 to send. + Pass a file_id as string to send a photo that exists on the Telegram servers or + pass a file path as string to upload a new photo that exists on your local machine. + Sending photo by a URL is currently unsupported. + + caption (:obj:`str`, optional): + Caption of the photo 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. + """ + + 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..7ad5ce50 --- /dev/null +++ b/pyrogram/client/input_media_video.py @@ -0,0 +1,66 @@ +# 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 to send. + Pass a file_id as string to send a video that exists on the Telegram servers or + pass a file path as string to upload a new video that exists on your local machine. + Sending video by a URL is currently unsupported. + + 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/input_phone_contact.py b/pyrogram/client/input_phone_contact.py index 9268ca0a..1ca8bf4c 100644 --- a/pyrogram/client/input_phone_contact.py +++ b/pyrogram/client/input_phone_contact.py @@ -22,19 +22,22 @@ from pyrogram.session.internals import MsgId class InputPhoneContact: """This object represents a Phone Contact to be added in your Telegram address book. - It is intended to be used with :obj:`pyrogram.Client.add_contacts` + It is intended to be used with :meth:`add_contacts() ` Args: - phone (:obj:`str`): + phone (``str``): Contact's phone number - first_name (:obj:`str`): + first_name (``str``): Contact's first name - last_name (:obj:`str`, optional): + last_name (``str``, optional): Contact's last name """ + def __init__(self, phone: str, first_name: str, last_name: str = ""): + pass + def __new__(cls, phone: str, first_name: str, last_name: str = ""): return RawInputPhoneContact( client_id=MsgId(), diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py new file mode 100644 index 00000000..51eba3ae --- /dev/null +++ b/pyrogram/client/message_parser.py @@ -0,0 +1,581 @@ +# 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, functions +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_chat_photo(photo): + if not isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): + return None + + if not isinstance(photo.photo_small, types.FileLocation): + return None + + if not isinstance(photo.photo_big, types.FileLocation): + return None + + loc_small = photo.photo_small + loc_big = photo.photo_big + + return pyrogram.ChatPhoto( + small_file_id=encode( + pack( + " 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, + photo=parse_chat_photo(user.photo) + ) 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, + photo=parse_chat_photo(user.photo) + ) + + +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, + photo=parse_chat_photo(chat.photo) + ) + + +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, + photo=parse_chat_photo(channel.photo) + ) + + +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[message.from_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 . + import base64 import json import logging diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index b95159cc..d4b1c38e 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -1,3 +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 . + +from base64 import b64decode, b64encode + from pyrogram.api import types @@ -26,3 +46,39 @@ def get_offset_date(dialogs): return m.date else: return 0 + + +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("=") diff --git a/pyrogram/session/auth.py b/pyrogram/session/auth.py index 74d45845..449524b3 100644 --- a/pyrogram/session/auth.py +++ b/pyrogram/session/auth.py @@ -262,7 +262,6 @@ class Auth: else: raise e - log.warning("Auth key creation failed. Let's try again: {}".format(repr(e))) time.sleep(1) continue else: diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index 5be2eaec..8ae70ccc 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -32,7 +32,7 @@ from pyrogram import __copyright__, __license__, __version__ from pyrogram.api import functions, types, core from pyrogram.api.all import layer from pyrogram.api.core import Message, Object, MsgContainer, Long, FutureSalt, Int -from pyrogram.api.errors import Error +from pyrogram.api.errors import Error, InternalServerError from pyrogram.connection import Connection from pyrogram.crypto import AES, KDF from .internals import MsgId, MsgFactory, DataCenter @@ -399,17 +399,19 @@ class Session: else: return result - def send(self, data: Object): - for i in range(self.MAX_RETRIES): - self.is_connected.wait(self.WAIT_TIMEOUT) + def send(self, data: Object, retries: int = MAX_RETRIES): + self.is_connected.wait(self.WAIT_TIMEOUT) - try: - return self._send(data) - except (OSError, TimeoutError): - (log.warning if i > 2 else log.info)( - "{}: {} Retrying {}".format(i, datetime.now(), type(data)) - ) - time.sleep(1) - continue - else: - return None + try: + return self._send(data) + except (OSError, TimeoutError, InternalServerError) as e: + if retries == 0: + raise e from None + + (log.warning if retries < 3 else log.info)( + "{}: {} Retrying {}".format( + Session.MAX_RETRIES - retries, + datetime.now(), type(data))) + + time.sleep(0.5) + self.send(data, retries - 1)