From e79afb938893b57c0d7510b7d36ba2f234451a09 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 6 Jan 2018 12:55:37 +0100 Subject: [PATCH 001/285] Add full cross-references through docstrings for the whole Telegram API --- compiler/api/compiler.py | 201 +++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 115 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 1cfdb437..b7ae4a49 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -32,6 +32,59 @@ FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)") FLAGS_RE_3 = re.compile(r"flags:#") 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): + if t in core_types: + if "int" in t or t == "long": + return ":obj:`int`" + elif t == "double": + return ":obj:`float`" + else: + return ":obj:`{}`".format(t.lower()) + elif t == "true": + return ":obj:`bool`" + elif t == "Object": + return ":obj:`Any type`" + elif t == "!X": + return ":obj:`Any function`" + elif t.startswith("Vector"): + return "List of " + get_docstring_arg_type(t.split("<")[1][:-1], is_list=True) + else: + t = types_to_constructors.get(t, [t]) + n = len(t) - 1 + + t = (("e" if is_list else "E") + "ither " if n else "") + ", ".join( + ":obj:`{0} `".format(i) + 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: @@ -129,23 +182,36 @@ 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])] + ) ) ) - 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 @@ -188,46 +254,12 @@ def start(): is_optional = arg_type.startswith("flags.") 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])] - ) - ) - docstring_args.append( "{}: {}{}".format( arg_name, - arg_type, - " (optional)" if is_optional else "" + "(optional) " if is_optional else "", + get_docstring_arg_type(arg_type), + ) ) @@ -239,78 +271,17 @@ def start(): docstring_args = "Attributes:\n ID (:obj:`int`): ``{}``\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:`pyrogram.Error`" - 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] + try: + docstring_args += "\n\n Returns:\n " + get_docstring_arg_type(c.return_type) + except KeyError: + pass + else: + references = get_references(".".join(filter(None, [c.namespace, c.name]))) - 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 type is returned by " + references + "." if c.has_flags: write_flags = [] From 21d521fb601e7f60ef357b20bc0e6ce1ed5f69ef Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 8 Jan 2018 05:17:55 +0100 Subject: [PATCH 002/285] Update docs --- compiler/docs/compiler.py | 3 ++ docs/source/_templates/footer.html | 60 ++++++++++++++++++++++++++++++ docs/source/conf.py | 16 +++++--- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 docs/source/_templates/footer.html diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index af9d1315..63d41a00 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -95,6 +95,9 @@ def generate(source_path, base): module = "pyrogram.api.{}".format(base) with open(destination + "/" + inner_path, "w") as f: + # if k == base: + # f.write(":tocdepth: 1\n\n") + f.write( toctree.format( title=k.title(), diff --git a/docs/source/_templates/footer.html b/docs/source/_templates/footer.html new file mode 100644 index 00000000..54924f63 --- /dev/null +++ b/docs/source/_templates/footer.html @@ -0,0 +1,60 @@ +
+ {% if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} + + {% endif %} + +
+ +
+

+ {%- if show_copyright %} + {%- if hasdoc('copyright') %} + {% trans path=pathto('copyright'), copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} + {%- else %} + {% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} + {%- endif %} + {%- endif %} + +

+ Pyrogram documentation + © Copyright 2017 - 2018, Dan Tès +
+

+ Licensed under the + Creative Commons Attribution 3.0 License +

+ + {%- if build_id and build_url %} + {% trans build_url=build_url, build_id=build_id %} + + Build + {{ build_id }}. + + {% endtrans %} + {%- elif commit %} + {% trans commit=commit %} + + Revision {{ commit }}. + + {% endtrans %} + {%- elif last_updated %} + {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} + {%- endif %} + +

+
+ + {%- if show_sphinx %} + {% trans %}Built with Sphinx using a theme provided by Read the Docs{% endtrans %}. + {%- endif %} + + {%- block extrafooter %} {% endblock %} + +
diff --git a/docs/source/conf.py b/docs/source/conf.py index 439c0a1a..75b2474e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -84,27 +84,31 @@ 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 ---------------------------------------------- +# 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 +} # The name of an image file (relative to this directory) to place at the top # of the sidebar. From 1ef3fec314b84998bad788d3a2846224be1a43bc Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 8 Jan 2018 07:15:38 +0100 Subject: [PATCH 003/285] Update API and Docs compiler --- compiler/api/compiler.py | 12 ++++-------- compiler/docs/compiler.py | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index b7ae4a49..7cce2752 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -47,10 +47,10 @@ def get_docstring_arg_type(t: str, is_list: bool = False): return ":obj:`{}`".format(t.lower()) elif t == "true": return ":obj:`bool`" - elif t == "Object": - return ":obj:`Any type`" + elif t == "Object" or t == "X": + return "Any type from :obj:`pyrogram.api.types`" elif t == "!X": - return ":obj:`Any function`" + return "Any query from :obj:`pyrogram.api.functions`" elif t.startswith("Vector"): return "List of " + get_docstring_arg_type(t.split("<")[1][:-1], is_list=True) else: @@ -272,11 +272,7 @@ def start(): if c.section == "functions": docstring_args += "\n\n Raises:\n :obj:`pyrogram.Error`" - - try: - docstring_args += "\n\n Returns:\n " + get_docstring_arg_type(c.return_type) - except KeyError: - pass + docstring_args += "\n\n Returns:\n " + get_docstring_arg_type(c.return_type) else: references = get_references(".".join(filter(None, [c.namespace, c.name]))) diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 63d41a00..58dc3e2a 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -95,8 +95,8 @@ def generate(source_path, base): module = "pyrogram.api.{}".format(base) with open(destination + "/" + inner_path, "w") as f: - # if k == base: - # f.write(":tocdepth: 1\n\n") + if k == base: + f.write(":tocdepth: 1\n\n") f.write( toctree.format( From 3d8faa125a160c49c7d1fe6f63323b86eb43a8db Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 8 Jan 2018 07:15:58 +0100 Subject: [PATCH 004/285] Don't show version on docs --- docs/source/conf.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 75b2474e..33933e4f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,7 @@ import sys sys.path.insert(0, os.path.abspath('../..')) # Import after sys.path.insert() to avoid issues -from pyrogram import __version__ +# from pyrogram import __version__ # -- General configuration ------------------------------------------------ @@ -67,9 +67,9 @@ author = 'Dan Tès' # built documents. # # The short X.Y version. -version = "version " + __version__ +# version = "version " + __version__ # The full version, including alpha/beta/rc tags. -release = version +# release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -107,7 +107,8 @@ html_theme_options = { 'canonical_url': "https://docs.pyrogram.ml/", 'collapse_navigation': False, 'sticky_navigation': False, - 'logo_only': True + 'logo_only': True, + 'display_version': False } # The name of an image file (relative to this directory) to place at the top From 20c0cf2efb558049afd9f6de1d499d212f31f647 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 8 Jan 2018 08:31:07 +0100 Subject: [PATCH 005/285] Add custom CSS --- docs/source/_static/css/style.css | 13 +++++++++++++ docs/source/conf.py | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 docs/source/_static/css/style.css diff --git a/docs/source/_static/css/style.css b/docs/source/_static/css/style.css new file mode 100644 index 00000000..a49a838f --- /dev/null +++ b/docs/source/_static/css/style.css @@ -0,0 +1,13 @@ +@import url("theme.css"); + +.wy-side-nav-search, .wy-nav-top { + background-color: #d1ccc1; +} + +.wy-nav-top, .wy-nav-top a { + color: #3b3936; +} + +.wy-side-nav-search input[type=text] { + border-color: #918e86; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 33933e4f..012365ea 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -125,6 +125,8 @@ html_favicon = '_static/pyrogram.ico' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +html_style = 'css/style.css' + # Custom sidebar templates, must be a dictionary that maps document names # to template names. # From edbca87d36282077802dff8b4d9a218b442b4a45 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 8 Jan 2018 08:31:20 +0100 Subject: [PATCH 006/285] Update docs --- compiler/api/compiler.py | 2 +- docs/source/errors/UnknownError.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 7cce2752..7e8851ac 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -277,7 +277,7 @@ def start(): references = get_references(".".join(filter(None, [c.namespace, c.name]))) if references: - docstring_args += "\n\n See Also:\n This type is returned by " + references + "." + docstring_args += "\n\n See Also:\n This type can be returned by " + references + "." if c.has_flags: write_flags = [] 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: From b0f1d8991444f5a79f995d805936b25ca5bbbcc3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 8 Jan 2018 09:27:47 +0100 Subject: [PATCH 007/285] Show string type as "str" --- compiler/api/compiler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 7e8851ac..09536c32 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -43,6 +43,8 @@ def get_docstring_arg_type(t: str, is_list: bool = False): return ":obj:`int`" elif t == "double": return ":obj:`float`" + elif t == "string": + return ":obj:`str`" else: return ":obj:`{}`".format(t.lower()) elif t == "true": From 675c69a617dccffe80e366c508f1e2cb17e8f4f4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 9 Jan 2018 21:20:13 +0100 Subject: [PATCH 008/285] Fix typo --- docs/source/getting_started/QuickInstallation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/getting_started/QuickInstallation.rst index 4d2912b8..0af77390 100644 --- a/docs/source/getting_started/QuickInstallation.rst +++ b/docs/source/getting_started/QuickInstallation.rst @@ -34,4 +34,4 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.3.2' + '0.3.3' From f3dbfd1f4e8ec6cfef7c059309d1346ab15356b6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 10 Jan 2018 20:56:43 +0100 Subject: [PATCH 009/285] Add description on docs' welcome page --- docs/source/index.rst | 48 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index a45efa74..60c48853 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,3 @@ -Welcome to Pyrogram -=================== - .. raw:: html
@@ -11,8 +8,47 @@ Welcome to Pyrogram

Telegram MTProto API Client Library for Python +

+ + Scheme Layer 74 + + + MTProto v2.0 +

+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. + +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**: 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. + +- **Updated**: Pyrogram makes use of the latest Telegram API version, currently **Layer 74**. + +- **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 ------- @@ -28,12 +64,6 @@ Preview client.stop() -About ------ - -Welcome to the Pyrogram's documentation! Here you can find resources for learning how to use the library. -Contents are organized by topic and are accessible from the sidebar. - To get started, press Next. .. toctree:: From 23d89cc37ba1bd99500ff337b8e9a5c8f498b5ef Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 11 Jan 2018 18:14:26 +0100 Subject: [PATCH 010/285] Add notes for installation --- docs/source/getting_started/QuickInstallation.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/getting_started/QuickInstallation.rst index 0af77390..532745b9 100644 --- a/docs/source/getting_started/QuickInstallation.rst +++ b/docs/source/getting_started/QuickInstallation.rst @@ -7,6 +7,16 @@ The most straightforward and recommended way to install or upgrade Pyrogram is b $ pip install --upgrade pyrogram +.. important:: + + Pyrogram only works on Python 3.3 or higher; if your **pip** points to Python 2.x use **pip3** instead. + + Also, if you are getting an error while installing or importing the library, please update setuptools and try again. + + .. code-block:: bash + + $ pip install --upgrade setuptools + Bleeding Edge ------------- From 655ad4c7ab0f04cbea3e5731a8c3723720b3fc93 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 13 Jan 2018 16:22:49 +0100 Subject: [PATCH 011/285] Add docs license link --- docs/source/_templates/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/_templates/footer.html b/docs/source/_templates/footer.html index 54924f63..f50f83a1 100644 --- a/docs/source/_templates/footer.html +++ b/docs/source/_templates/footer.html @@ -28,7 +28,7 @@

Licensed under the - Creative Commons Attribution 3.0 License + Creative Commons Attribution 4.0 International License

{%- if build_id and build_url %} From 84b28d4221ad58254405fb23795b2c351480657e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 13 Jan 2018 17:06:14 +0100 Subject: [PATCH 012/285] Enable source link --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 012365ea..343b6f07 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,7 +40,7 @@ extensions = [ ] # Don't show source files on docs -html_show_sourcelink = False +html_show_sourcelink = True # Order by source, not alphabetically autodoc_member_order = 'bysource' From 029b71d7b9785fc5c04d58cd33f44130b68c403c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 13 Jan 2018 17:08:18 +0100 Subject: [PATCH 013/285] Edit source link address --- docs/source/_templates/breadcrumbs.html | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/source/_templates/breadcrumbs.html diff --git a/docs/source/_templates/breadcrumbs.html b/docs/source/_templates/breadcrumbs.html new file mode 100644 index 00000000..efb31a5e --- /dev/null +++ b/docs/source/_templates/breadcrumbs.html @@ -0,0 +1,82 @@ +{# Support for Sphinx 1.3+ page_source_suffix, but don't break old builds. #} + +{% if page_source_suffix %} +{% set suffix = page_source_suffix %} +{% else %} +{% set suffix = source_suffix %} +{% endif %} + +{% if meta is defined and meta is not none %} +{% set check_meta = True %} +{% else %} +{% set check_meta = False %} +{% endif %} + +{% if check_meta and 'github_url' in meta %} +{% set display_github = True %} +{% endif %} + +{% if check_meta and 'bitbucket_url' in meta %} +{% set display_bitbucket = True %} +{% endif %} + +{% if check_meta and 'gitlab_url' in meta %} +{% set display_gitlab = True %} +{% endif %} + +
+ + + + {% if (theme_prev_next_buttons_location == 'top' or theme_prev_next_buttons_location == 'both') and (next or prev) %} + + {% endif %} +
+
\ No newline at end of file From ca075d6b5bdab99cd1936871b045bca81471262d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 13 Jan 2018 17:08:58 +0100 Subject: [PATCH 014/285] Add button.js script --- docs/source/_templates/layout.html | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/source/_templates/layout.html diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html new file mode 100644 index 00000000..9e80856f --- /dev/null +++ b/docs/source/_templates/layout.html @@ -0,0 +1,4 @@ +{% extends "!layout.html" %} +{% block extrahead %} + +{% endblock %} \ No newline at end of file From e1aebfbcc6793d49f98927d78e5eb4c408fb4aae Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 13 Jan 2018 17:09:25 +0100 Subject: [PATCH 015/285] Add github buttons --- docs/source/index.rst | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 60c48853..b46d4189 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,11 @@ .. raw:: html +
+ Watch + Star + Fork +
+
Logo @@ -8,8 +14,20 @@

Telegram MTProto API Client Library for Python +
+
+ Download + + • + + Source code + + • + + Community +

- + Scheme Layer 74 @@ -38,10 +56,10 @@ Features - **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 + `MTProto Mobile Protocol v2.0`_ and the mechanisms needed for establishing a reliable connection. -- **Updated**: Pyrogram makes use of the latest Telegram API version, currently **Layer 74**. +- **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 74`_. - **Documented**: Pyrogram API public methods are documented and resemble the well established Telegram Bot API, thus offering a familiar look to Bot developers. @@ -94,3 +112,7 @@ To get started, press Next. functions/index types/index + +.. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto + +.. _`Layer 74`: https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl \ No newline at end of file From 5ea86cf8f571e3237532a49878bf7d6b38b8626b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 13 Jan 2018 17:21:08 +0100 Subject: [PATCH 016/285] Update index page --- docs/source/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index b46d4189..e1e3e5de 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,10 +1,10 @@ .. raw:: html -

+

Watch Star Fork -

+

From 44632d93fe2ae95bd6d275ea5cff28181632f6c7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 14 Jan 2018 17:57:49 +0100 Subject: [PATCH 017/285] Rename source link label --- docs/source/_templates/breadcrumbs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/_templates/breadcrumbs.html b/docs/source/_templates/breadcrumbs.html index efb31a5e..d6d29f14 100644 --- a/docs/source/_templates/breadcrumbs.html +++ b/docs/source/_templates/breadcrumbs.html @@ -61,7 +61,7 @@ {% elif show_source and source_url_prefix %} {{ _('View page source') }} {% elif show_source and has_source and sourcename %} - {{ _('GitHub') }} + {{ _('View on GitHub') }} {% endif %} {% endif %} From da18417b3b36f466efc80e1a3d0bb6b6d6831892 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 14 Jan 2018 17:58:43 +0100 Subject: [PATCH 018/285] Add some style tweaks --- docs/source/_static/css/style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/source/_static/css/style.css b/docs/source/_static/css/style.css index a49a838f..7f9cbdfd 100644 --- a/docs/source/_static/css/style.css +++ b/docs/source/_static/css/style.css @@ -11,3 +11,12 @@ .wy-side-nav-search input[type=text] { border-color: #918e86; } + +.wy-nav-content a:visited { + color: #2980B9; +} + +.wy-menu-vertical a:active { + color: #3b3936; + background-color: #d1ccc1; +} From eca71dc44a2a037b1a9f95cffc46ca67b5a60142 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 15 Jan 2018 12:36:03 +0100 Subject: [PATCH 019/285] Update doc title --- docs/source/conf.py | 2 ++ docs/source/index.rst | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 343b6f07..52e0f649 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -91,6 +91,8 @@ todo_include_todos = False # -- Options for HTML output ---------------------------------------------- +html_title = "Pyrogram Documentation" + # Overridden by template html_show_copyright = False diff --git a/docs/source/index.rst b/docs/source/index.rst index e1e3e5de..87867fc0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,3 +1,6 @@ +Welcome to Pyrogram +=================== + .. raw:: html

@@ -38,7 +41,7 @@

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 From 407be60505ef585418efa76420400c7e6f866ecf Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 15 Jan 2018 14:29:18 +0100 Subject: [PATCH 020/285] Fix installation command --- docs/source/getting_started/QuickInstallation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/getting_started/QuickInstallation.rst index 532745b9..4b7329c3 100644 --- a/docs/source/getting_started/QuickInstallation.rst +++ b/docs/source/getting_started/QuickInstallation.rst @@ -24,7 +24,7 @@ If you want the latest development version of the library, you can either instal .. code-block:: bash - $ pip install git+https://github.com/pyrogram/pyrogram.git + $ pip install --upgrade git+https://github.com/pyrogram/pyrogram.git or manually, using: From ed88885070407fe9376dd55b1970caa872240898 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 18 Jan 2018 12:51:29 +0100 Subject: [PATCH 021/285] Update footer --- docs/source/_templates/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/_templates/footer.html b/docs/source/_templates/footer.html index f50f83a1..cdad6f37 100644 --- a/docs/source/_templates/footer.html +++ b/docs/source/_templates/footer.html @@ -24,7 +24,7 @@
Pyrogram documentation - © Copyright 2017 - 2018, Dan Tès + © Copyright 2017-2018, Dan Tès

Licensed under the From 07b4250aea7ef9240504c322c1ab1554a9394303 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 18 Jan 2018 13:23:05 +0100 Subject: [PATCH 022/285] Add proxy documentation --- docs/source/index.rst | 1 + docs/source/resources/ProxyServer.rst | 29 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 docs/source/resources/ProxyServer.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 87867fc0..2a18feda 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -102,6 +102,7 @@ To get started, press Next. resources/TextFormatting resources/UpdateHandling resources/ErrorHandling + resources/ProxyServer .. toctree:: :hidden: diff --git a/docs/source/resources/ProxyServer.rst b/docs/source/resources/ProxyServer.rst new file mode 100644 index 00000000..1486c93e --- /dev/null +++ b/docs/source/resources/ProxyServer.rst @@ -0,0 +1,29 @@ +Proxy Server +============ + +Pyrogram supports SOCKS5 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 + +- If your proxy doesn't require authorization you can omit username and password by either leaving the values blank + or completely delete the lines. \ No newline at end of file From 9a60a51a00d514e0374fb0fd895fe97297d9cd47 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 20 Jan 2018 16:23:10 +0100 Subject: [PATCH 023/285] Update docs --- docs/source/getting_started/QuickInstallation.rst | 2 +- docs/source/index.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/getting_started/QuickInstallation.rst index 4b7329c3..1a76d512 100644 --- a/docs/source/getting_started/QuickInstallation.rst +++ b/docs/source/getting_started/QuickInstallation.rst @@ -44,4 +44,4 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.3.3' + '0.4.0' diff --git a/docs/source/index.rst b/docs/source/index.rst index 2a18feda..7ac0e7de 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,7 +32,7 @@ Welcome to Pyrogram

Scheme Layer 74 + alt="Scheme Layer 75"> Date: Sat, 20 Jan 2018 20:16:42 +0100 Subject: [PATCH 024/285] Small fix --- compiler/api/compiler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 09536c32..c7e8d95c 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -22,7 +22,7 @@ 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) @@ -133,7 +133,7 @@ def start(): with open("{}/template/class.txt".format(HOME)) as f: template = f.read() - with open(notice_path) as f: + with open(NOTICE_PATH) as f: notice = [] for line in f.readlines(): @@ -412,5 +412,5 @@ def start(): if "__main__" == __name__: HOME = "." DESTINATION = "../../pyrogram/api" - notice_path = "../../NOTICE" + NOTICE_PATH = "../../NOTICE" start() From bc021a756579641be49121c089965363d0ef5b92 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 21 Jan 2018 12:35:36 +0100 Subject: [PATCH 025/285] Update compiler --- compiler/api/compiler.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index c7e8d95c..de44940a 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -30,6 +30,7 @@ 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 = {} @@ -39,10 +40,13 @@ constructors_to_functions = {} def get_docstring_arg_type(t: str, is_list: bool = False): if t in core_types: - if "int" in t or t == "long": - return ":obj:`int`" + if t == "long": + return ":obj:`int`:obj:`64-bit`" + elif "int" in t: + size = INT_RE.match(t) + return ":obj:`int`:obj:`{}-bit`".format(size.group(1)) if size else ":obj:`int`:obj:`32-bit`" elif t == "double": - return ":obj:`float`" + return ":obj:`float`:obj:`64-bit`" elif t == "string": return ":obj:`str`" else: @@ -52,7 +56,7 @@ def get_docstring_arg_type(t: str, is_list: bool = False): elif t == "Object" or t == "X": return "Any type from :obj:`pyrogram.api.types`" elif t == "!X": - return "Any query from :obj:`pyrogram.api.functions`" + return "Any method from :obj:`pyrogram.api.functions`" elif t.startswith("Vector"): return "List of " + get_docstring_arg_type(t.split("<")[1][:-1], is_list=True) else: @@ -253,15 +257,15 @@ def start(): 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] docstring_args.append( "{}: {}{}".format( arg_name, - "(optional) " if is_optional else "", - get_docstring_arg_type(arg_type), - + "(optional {}) ".format(flag_number) if is_optional else "", + get_docstring_arg_type(arg_type) ) ) From ddb8c9e595345e15d94195b09db94308619d6470 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 21 Jan 2018 18:04:11 +0100 Subject: [PATCH 026/285] Update docs --- docs/source/getting_started/QuickInstallation.rst | 8 +------- docs/source/resources/ErrorHandling.rst | 6 ++---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/getting_started/QuickInstallation.rst index 1a76d512..26b85d7a 100644 --- a/docs/source/getting_started/QuickInstallation.rst +++ b/docs/source/getting_started/QuickInstallation.rst @@ -11,12 +11,6 @@ The most straightforward and recommended way to install or upgrade Pyrogram is b Pyrogram only works on Python 3.3 or higher; if your **pip** points to Python 2.x use **pip3** instead. - Also, if you are getting an error while installing or importing the library, please update setuptools and try again. - - .. code-block:: bash - - $ pip install --upgrade setuptools - Bleeding Edge ------------- @@ -44,4 +38,4 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.4.0' + '0.4.2' diff --git a/docs/source/resources/ErrorHandling.rst b/docs/source/resources/ErrorHandling.rst index 74a9091a..71a3b7a7 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: @@ -53,8 +52,7 @@ can try again. The value is always stored in the ``x`` field of the returned exc from pyrogram.api.errors import FloodWait try: - # something - pass + ... except FloodWait as e: print(e.x) From b1b04b5768fc777e1a00d3f6976e1b3dc4e8c9ba Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 21 Jan 2018 19:07:48 +0100 Subject: [PATCH 027/285] Update API compiler --- compiler/api/compiler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index a6c370c9..1e87c0b3 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -41,12 +41,12 @@ constructors_to_functions = {} def get_docstring_arg_type(t: str, is_list: bool = False): if t in core_types: if t == "long": - return ":obj:`int`:obj:`64-bit`" + return ":obj:`int` :obj:`64-bit`" elif "int" in t: size = INT_RE.match(t) - return ":obj:`int`:obj:`{}-bit`".format(size.group(1)) if size else ":obj:`int`:obj:`32-bit`" + return ":obj:`int` :obj:`{}-bit`".format(size.group(1)) if size else ":obj:`int` :obj:`32-bit`" elif t == "double": - return ":obj:`float`:obj:`64-bit`" + return ":obj:`float` :obj:`64-bit`" elif t == "string": return ":obj:`str`" else: @@ -264,7 +264,7 @@ def start(): docstring_args.append( "{}: {}{}".format( arg_name, - "(optional {}) ".format(flag_number) if is_optional else "", + "``optional.{}``".format(flag_number) if is_optional else "", get_docstring_arg_type(arg_type) ) ) From eb27c8c97e1f52b3f3b23bfbf08e4b497c62415c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 23 Jan 2018 16:43:48 +0100 Subject: [PATCH 028/285] Update API compiler --- compiler/api/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 1e87c0b3..ad5964bf 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -264,7 +264,7 @@ def start(): docstring_args.append( "{}: {}{}".format( arg_name, - "``optional.{}``".format(flag_number) if is_optional else "", + "``optional``".format(flag_number) if is_optional else "", get_docstring_arg_type(arg_type) ) ) From faeb5d6e01220069316241e799c770e001a95f4f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 23 Jan 2018 16:50:57 +0100 Subject: [PATCH 029/285] Enable docs source compilation on setup --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9fd9e082..a58bc7bf 100644 --- a/setup.py +++ b/setup.py @@ -22,14 +22,13 @@ from sys import argv from setuptools import setup, find_packages from compiler.api import compiler as api_compiler +from compiler.docs import compiler as docs_compiler from compiler.error import compiler as error_compiler -# from compiler.docs import compiler as docs_compiler - if len(argv) > 1 and argv[1] != "sdist": api_compiler.start() error_compiler.start() - # docs_compiler.start() + docs_compiler.start() with open("pyrogram/__init__.py", encoding="utf-8") as f: version = re.findall(r"__version__ = \"(.+)\"", f.read())[0] From 981fba0ffd7eb0a0f486090ff7ec6f1ca559cda4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 23 Jan 2018 18:13:50 +0100 Subject: [PATCH 030/285] Update API compiler --- compiler/api/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index ad5964bf..f5f45ae4 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -264,7 +264,7 @@ def start(): docstring_args.append( "{}: {}{}".format( arg_name, - "``optional``".format(flag_number) if is_optional else "", + "``optional`` ".format(flag_number) if is_optional else "", get_docstring_arg_type(arg_type) ) ) From a3fea258902a3bca72f0dc197445fde0e8a6a055 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 24 Jan 2018 22:17:12 +0100 Subject: [PATCH 031/285] Update examples --- docs/source/resources/TextFormatting.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/source/resources/TextFormatting.rst b/docs/source/resources/TextFormatting.rst index d17ab8e8..976bd8c6 100644 --- a/docs/source/resources/TextFormatting.rst +++ b/docs/source/resources/TextFormatting.rst @@ -59,11 +59,11 @@ Examples client.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`" ) ) @@ -91,12 +91,12 @@ Examples client.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 ) From a0e7a19805f567d0c91a893d6a0a949e9d75a991 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 25 Jan 2018 13:46:43 +0100 Subject: [PATCH 032/285] Tidy up Pyrogram API index toctree --- docs/source/pyrogram/Error.rst | 2 ++ docs/source/pyrogram/index.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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/index.rst b/docs/source/pyrogram/index.rst index e484bd5e..551aef05 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -9,8 +9,8 @@ the same parameters as well, thus offering a familiar look to Bot developers. .. toctree:: Client + Error ChatAction ParseMode - Error .. _Telegram Bot API: https://core.telegram.org/bots/api#available-methods From ec7bf5d614f65f1cf5c540fb9599d047b558ec27 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 25 Jan 2018 13:47:05 +0100 Subject: [PATCH 033/285] Enable autosummary ext --- docs/source/conf.py | 3 ++- docs/source/pyrogram/Client.rst | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 52e0f649..4dbc4c85 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -36,7 +36,8 @@ sys.path.insert(0, os.path.abspath('../..')) # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon' + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary' ] # Don't show source files on docs diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index c0c4e667..dfb5fa27 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -1,5 +1,41 @@ Client ====== +.. currentmodule:::: pyrogram.Client + .. autoclass:: pyrogram.Client :members: + + **Available methods** + + .. autosummary:: + :nosignatures: + + start + stop + idle + set_update_handler + send + get_me + send_message + forward_messages + send_photo + send_audio + send_document + send_video + send_voice + send_video_note + send_location + send_venue + send_contact + send_chat_action + 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 \ No newline at end of file From c4b79d67cb2ce10354d44cf647e51f06027fd0e7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 25 Jan 2018 13:47:26 +0100 Subject: [PATCH 034/285] Add AutoAuthorization page --- docs/source/index.rst | 3 +- docs/source/resources/AutoAuthorization.rst | 60 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 docs/source/resources/AutoAuthorization.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 7ac0e7de..b75801a2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,7 +18,7 @@ Welcome to Pyrogram

Telegram MTProto API Client Library for Python
- + Download • @@ -103,6 +103,7 @@ To get started, press Next. resources/UpdateHandling resources/ErrorHandling resources/ProxyServer + resources/AutoAuthorization .. toctree:: :hidden: diff --git a/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst new file mode 100644 index 00000000..304c9fa5 --- /dev/null +++ b/docs/source/resources/AutoAuthorization.rst @@ -0,0 +1,60 @@ +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 **Sign-In** and **Sign-Up** processes, all you need to do is pass the relevant +parameters when creating a new Client. + +Sign-In +------- + +To automate the **Sign-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" + + + client = Client( + session_name="example", + phone_number="39**********", + phone_code=phone_code_callback, + password="password" # (if you have one) + ) + + client.start() + print(client.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" + + + client = Client( + session_name="example", + phone_number="39**********", + phone_code=phone_code_callback, + first_name="Pyrogram", + last_name="" # Can be an empty string + ) + + client.start() + print(client.get_me()) \ No newline at end of file From 63f5839a52bd8f59a2a6d3f099a716f702abd02c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 25 Jan 2018 18:26:33 +0100 Subject: [PATCH 035/285] Update logo --- docs/source/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index b75801a2..4b7dd339 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,8 @@ Welcome to Pyrogram

- Logo +
Pyrogram Icon
+
Pyrogram Label
From d83a9f5267e733d8dcb23127cb4ff942119e2455 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 25 Jan 2018 19:53:09 +0100 Subject: [PATCH 036/285] Add gtag --- docs/source/_templates/layout.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html index 9e80856f..72eaeda4 100644 --- a/docs/source/_templates/layout.html +++ b/docs/source/_templates/layout.html @@ -1,4 +1,16 @@ {% extends "!layout.html" %} {% block extrahead %} + + {% endblock %} \ No newline at end of file From cf213e65a41c49ff8b718ac557f6587e40206946 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 28 Jan 2018 03:32:48 +0100 Subject: [PATCH 037/285] Add Fast Crypto (TgCrypto) docs --- docs/source/index.rst | 6 +++++ docs/source/resources/FastCrypto.rst | 37 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 docs/source/resources/FastCrypto.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 4b7dd339..dfa6d67e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -63,6 +63,9 @@ Features `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 @@ -105,6 +108,7 @@ To get started, press Next. resources/ErrorHandling resources/ProxyServer resources/AutoAuthorization + resources/FastCrypto .. toctree:: :hidden: @@ -121,4 +125,6 @@ To get started, press Next. .. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto +.. _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 diff --git a/docs/source/resources/FastCrypto.rst b/docs/source/resources/FastCrypto.rst new file mode 100644 index 00000000..4e24466a --- /dev/null +++ b/docs/source/resources/FastCrypto.rst @@ -0,0 +1,37 @@ +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 required 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 From 0700bdfc6115b419e4b54984f1232634340dd55d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 28 Jan 2018 03:33:02 +0100 Subject: [PATCH 038/285] Update style --- docs/source/_static/css/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/_static/css/style.css b/docs/source/_static/css/style.css index 7f9cbdfd..f224ebb2 100644 --- a/docs/source/_static/css/style.css +++ b/docs/source/_static/css/style.css @@ -20,3 +20,7 @@ color: #3b3936; background-color: #d1ccc1; } + +.rst-content code.literal { + color: #3b3936; +} \ No newline at end of file From a7dd753ec528028eae3b65cc19dbd6c60853b4fa Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 7 Feb 2018 03:47:50 +0100 Subject: [PATCH 039/285] Update docs --- docs/source/resources/FastCrypto.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/resources/FastCrypto.rst b/docs/source/resources/FastCrypto.rst index 4e24466a..0cefd146 100644 --- a/docs/source/resources/FastCrypto.rst +++ b/docs/source/resources/FastCrypto.rst @@ -3,7 +3,7 @@ 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 required crypto algorithms MTProto requires, namely AES-IGE and AES-CTR 256 bit. +implements the crypto algorithms MTProto requires, namely AES-IGE and AES-CTR 256 bit. Installation ------------ From 077eb386deeffddd80dac720efa8d92037f7819e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 8 Feb 2018 16:24:28 +0100 Subject: [PATCH 040/285] Rename folder --- docs/source/{getting_started => start}/BasicUsage.rst | 0 docs/source/{getting_started => start}/ProjectSetup.rst | 0 docs/source/{getting_started => start}/QuickInstallation.rst | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename docs/source/{getting_started => start}/BasicUsage.rst (100%) rename docs/source/{getting_started => start}/ProjectSetup.rst (100%) rename docs/source/{getting_started => start}/QuickInstallation.rst (100%) diff --git a/docs/source/getting_started/BasicUsage.rst b/docs/source/start/BasicUsage.rst similarity index 100% rename from docs/source/getting_started/BasicUsage.rst rename to docs/source/start/BasicUsage.rst diff --git a/docs/source/getting_started/ProjectSetup.rst b/docs/source/start/ProjectSetup.rst similarity index 100% rename from docs/source/getting_started/ProjectSetup.rst rename to docs/source/start/ProjectSetup.rst diff --git a/docs/source/getting_started/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst similarity index 100% rename from docs/source/getting_started/QuickInstallation.rst rename to docs/source/start/QuickInstallation.rst From 7a904d8fff9fd284e1ed529cd41183a831988928 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 8 Feb 2018 16:48:16 +0100 Subject: [PATCH 041/285] Revert "Add Fast Crypto (TgCrypto) docs" This reverts commit cf213e6 --- docs/source/index.rst | 6 ----- docs/source/resources/FastCrypto.rst | 37 ---------------------------- 2 files changed, 43 deletions(-) delete mode 100644 docs/source/resources/FastCrypto.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index dfa6d67e..4b7dd339 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -63,9 +63,6 @@ Features `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 @@ -108,7 +105,6 @@ To get started, press Next. resources/ErrorHandling resources/ProxyServer resources/AutoAuthorization - resources/FastCrypto .. toctree:: :hidden: @@ -125,6 +121,4 @@ To get started, press Next. .. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto -.. _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 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 From 27550ce9371e0f02981452d833763d87a0a85ee4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 8 Feb 2018 17:01:17 +0100 Subject: [PATCH 042/285] Update folder/path --- docs/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 4b7dd339..d6ae4342 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -92,9 +92,9 @@ To get started, press Next. :hidden: :caption: Getting Started - getting_started/QuickInstallation - getting_started/ProjectSetup - getting_started/BasicUsage + start/QuickInstallation + start/ProjectSetup + start/BasicUsage .. toctree:: :hidden: From ae2d89ca05cea1537e10f4e879d9b5969f03e916 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 8 Feb 2018 17:01:27 +0100 Subject: [PATCH 043/285] Fix example version --- docs/source/start/QuickInstallation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index 26b85d7a..2b047b76 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -38,4 +38,4 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.4.2' + '0.5.0' From 1b98729e3ec5444f957c62f3ccb0a3b0db525926 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 8 Feb 2018 17:04:10 +0100 Subject: [PATCH 044/285] Rename proxy page --- docs/source/index.rst | 2 +- docs/source/resources/{ProxyServer.rst => SOCKS5Proxy.rst} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename docs/source/resources/{ProxyServer.rst => SOCKS5Proxy.rst} (76%) diff --git a/docs/source/index.rst b/docs/source/index.rst index d6ae4342..acd94baa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -103,7 +103,7 @@ To get started, press Next. resources/TextFormatting resources/UpdateHandling resources/ErrorHandling - resources/ProxyServer + resources/SOCKS5Proxy resources/AutoAuthorization .. toctree:: diff --git a/docs/source/resources/ProxyServer.rst b/docs/source/resources/SOCKS5Proxy.rst similarity index 76% rename from docs/source/resources/ProxyServer.rst rename to docs/source/resources/SOCKS5Proxy.rst index 1486c93e..55993deb 100644 --- a/docs/source/resources/ProxyServer.rst +++ b/docs/source/resources/SOCKS5Proxy.rst @@ -1,8 +1,8 @@ -Proxy Server +SOCKS5 Proxy ============ -Pyrogram supports SOCKS5 proxies with and without authentication. This feature allows Pyrogram to exchange data with -Telegram through an intermediate SOCKS5 proxy server. +Pyrogram supports proxies with and without authentication. This feature allows Pyrogram to exchange data with Telegram +through an intermediate SOCKS5 proxy server. Usage ----- From be773352edd48cb73fb4c599e92c215798d79213 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 8 Feb 2018 17:16:02 +0100 Subject: [PATCH 045/285] Add send_media_group to docs --- docs/source/pyrogram/Client.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index dfb5fa27..8d107516 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -38,4 +38,5 @@ Client export_chat_invite_link enable_cloud_password change_cloud_password - remove_cloud_password \ No newline at end of file + remove_cloud_password + send_media_group \ No newline at end of file From 0e78fc20e513c579cf6b95ad5a1b53a4d7b9ac58 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Feb 2018 03:09:18 +0100 Subject: [PATCH 046/285] Fix set_event_handler --- docs/source/pyrogram/Client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 8d107516..954900d8 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -14,7 +14,7 @@ Client start stop idle - set_update_handler + set_event_handler send get_me send_message From e7b51ecec815464f6e8e3a0ff4e85aa2ee476ae4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Feb 2018 03:17:36 +0100 Subject: [PATCH 047/285] Update generated code docstrings --- compiler/api/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index f5f45ae4..5607ce5a 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -274,7 +274,7 @@ 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 Raises:\n :obj:`pyrogram.Error`" From 84f614b77115ca28cacaeb00ececfa051dab0a7e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Feb 2018 04:09:16 +0100 Subject: [PATCH 048/285] Update style --- docs/source/_static/css/style.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/_static/css/style.css b/docs/source/_static/css/style.css index f224ebb2..ca327b4d 100644 --- a/docs/source/_static/css/style.css +++ b/docs/source/_static/css/style.css @@ -19,8 +19,4 @@ .wy-menu-vertical a:active { color: #3b3936; background-color: #d1ccc1; -} - -.rst-content code.literal { - color: #3b3936; } \ No newline at end of file From 96f895425f7d3d6e4904018c4d3aca627acb7c7d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Feb 2018 16:12:04 +0100 Subject: [PATCH 049/285] Add some types --- pyrogram/client/types/__init__.py | 2 + pyrogram/client/types/audio.py | 14 +++++ pyrogram/client/types/chat.py | 16 ++++++ pyrogram/client/types/message.py | 85 +++++++++++++++++++++++++++++++ pyrogram/client/types/user.py | 36 +++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 pyrogram/client/types/__init__.py create mode 100644 pyrogram/client/types/audio.py create mode 100644 pyrogram/client/types/chat.py create mode 100644 pyrogram/client/types/message.py create mode 100644 pyrogram/client/types/user.py diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py new file mode 100644 index 00000000..e858d4dc --- /dev/null +++ b/pyrogram/client/types/__init__.py @@ -0,0 +1,2 @@ +from .user import User +from .chat import Chat diff --git a/pyrogram/client/types/audio.py b/pyrogram/client/types/audio.py new file mode 100644 index 00000000..d2e53f44 --- /dev/null +++ b/pyrogram/client/types/audio.py @@ -0,0 +1,14 @@ +class Audio: + def __init__(self, + file_id: str, + duration: int = None, + performer: str = None, + title: str = None, + mime_type: str = None, + file_size: str = None): + self.file_id = file_id + self.duration = duration + self.performer = performer + self.title = title + self.mime_type = mime_type + self.file_size = file_size diff --git a/pyrogram/client/types/chat.py b/pyrogram/client/types/chat.py new file mode 100644 index 00000000..fb31bee1 --- /dev/null +++ b/pyrogram/client/types/chat.py @@ -0,0 +1,16 @@ +class Chat: + def __init__(self, + id: int, + type: str, + title: str = None, + username: str = None, + first_name: str = None, + last_name: str = None, + all_members_are_administrators: bool = None, + photo=None, + description: str = None, + invite_link: str = None, + pinned_message=None, + sticker_set_name: str = None, + can_set_sticker_set=None): + ... diff --git a/pyrogram/client/types/message.py b/pyrogram/client/types/message.py new file mode 100644 index 00000000..f617201c --- /dev/null +++ b/pyrogram/client/types/message.py @@ -0,0 +1,85 @@ +from . import User, Chat + + +class Message: + def __init__(self, + message_id: int, + from_user: User, + date: int, + chat: Chat, + forward_from: Chat = None, + forward_from_message_id: int = None, + forward_signature: str = None, + forward_date: int = None, + reply_to_message: "Message" = None, + edit_date: int = None, + media_group_id: str = None, + author_signature: str = None, + text: str = None, + entities=None, + caption_entities=None, + audio=None, + document=None, + game=None, + photo=None, + sticker=None, + video=None, + voice=None, + video_note=None, + caption: str = None, + contact=None, + location=None, + venue=None, + new_chat_members: list = None, + left_chat_member: User = None, + new_chat_title: str = None, + new_chat_photo: list = None, + delete_chat_photo: bool = None, + group_chat_created: bool = None, + supergroup_chat_created: bool = None, + channel_chat_created: bool = None, + migrate_to_chat_id: int = None, + migrate_from_chat_id: int = None, + pinned_message: "Message" = None, + invoice=None, + successful_payment=None): + self.message_id = message_id + self.from_user = from_user + self.date = date + self.chat = chat + self.forward_from = forward_from + self.forward_from_message_id = forward_from_message_id + self.forward_signature = forward_signature + self.forward_date = forward_date + self.reply_to_message = reply_to_message + self.edit_date = edit_date + self.media_group_id = media_group_id + self.author_signature = author_signature + self.text = text + self.entities = entities + self.caption_entities = caption_entities + self.audio = audio + self.document = document + self.game = game + self.photo = photo + self.sticker = sticker + self.video = video + self.voice = voice + self.video_note = video_note + self.caption = caption + self.contact = contact + self.location = location + self.venue = venue + self.new_chat_members = new_chat_members + self.left_chat_member = left_chat_member + self.new_chat_title = new_chat_title + self.new_chat_photo = new_chat_photo + self.delete_chat_photo = delete_chat_photo + self.group_chat_created = group_chat_created + self.supergroup_chat_created = supergroup_chat_created + self.channel_chat_created = channel_chat_created + self.migrate_to_chat_id = migrate_to_chat_id + self.migrate_from_chat_id = migrate_from_chat_id + self.pinned_message = pinned_message + self.invoice = invoice + self.successful_payment = successful_payment diff --git a/pyrogram/client/types/user.py b/pyrogram/client/types/user.py new file mode 100644 index 00000000..eee9113d --- /dev/null +++ b/pyrogram/client/types/user.py @@ -0,0 +1,36 @@ +class User: + """This object represents a Telegram user or bot. + + Args: + id (:obj:`int`): + Unique identifier for this user or bot. + + is_bot (:obj:`bool`): + True, if this user is a bot. + + first_name (:obj:`str`): + User's or bot’s first name. + + last_name (:obj:`str`, optional): + User's or bot’s last name. + + username (:obj:`str`, optional): + User's or bot’s username. + + language_code (:obj:`str`, optional): + IETF language tag of the user's language. + """ + + def __init__(self, + id: int, + is_bot: bool, + first_name: str, + last_name: str = None, + username: str = None, + language_code: str = None): + self.id = id + self.is_bot = is_bot + self.first_name = first_name + self.last_name = last_name + self.username = username + self.language_code = language_code From 55f1c66f927c247833df4d615f9915d7a5dfb491 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 13 Feb 2018 18:13:49 +0100 Subject: [PATCH 050/285] Update "Update Handling" page with more examples too --- docs/source/resources/UpdateHandling.rst | 115 ++++++++++++++++++++--- 1 file changed, 101 insertions(+), 14 deletions(-) diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 8fc12543..de390d7e 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -2,7 +2,7 @@ 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 +and can be handled by using a callback function, that is, a function called every time an :obj:`Update` is received from Telegram. To set an update handler simply call :obj:`set_update_handler ` @@ -15,32 +15,119 @@ Here's a complete example on how to set it up: from pyrogram import Client - def callback(update): + def update_handler(client, update, users, chats): print(update) + def main() + client = Client(session_name="example") + client.set_update_handler(update_handler) - client = Client(session_name="example") - client.set_update_handler(callback) + client.start() + client.idle() - client.start() - client.idle() + if __name__ == "__main__": + main() -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 +The last line of the main() function, :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: +- Simple Echo example for **Private Messages**: .. code-block:: python from pyrogram.api import types - def callback(update): - if isinstance(update, types.UpdateShortMessage) and not update.out: - client.send_message(update.user_id, update.message) + if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (PM and Groups) + message = update.message # type: types.Message - 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 + 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(message.from_id, message.message, reply_to_message_id=message.id) + + + +- Advanced Echo example for both **Private Messages** and **Basic Groups** (with mentions). + + .. code-block:: python + + from pyrogram.api import types + + 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 + ) + +- Advanced Echo example for **Supergroups** (with mentions): + + .. code-block:: python + + from pyrogram.api import types + + def update_handler(client, update, users, chats): + 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 + ) + +.. warning:: + The Advanced Examples above will make you reply to **all** new messages in private chats and in every single + group/supergroup you are in. Make sure you add an extra check to filter them: + + .. code-block:: python + + # Filter Groups by ID + if message.to_id.chat_id == MY_GROUP_ID: + ... + + # 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: + ... \ No newline at end of file From cb7601005b51f9594dd9112d9ad450f265be4507 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 15 Feb 2018 18:48:23 +0100 Subject: [PATCH 051/285] Add Emoji module docs --- docs/source/pyrogram/Emoji.rst | 6 ++++++ docs/source/pyrogram/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 docs/source/pyrogram/Emoji.rst 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/index.rst b/docs/source/pyrogram/index.rst index 160766eb..aa10ec4c 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -11,6 +11,7 @@ the same parameters as well, thus offering a familiar look to Bot developers. Client ChatAction ParseMode + Emoji InputMedia Error From 54ff739c820db912b8cb8d7c4eb9c36c5cedbeed Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 15 Feb 2018 21:28:45 +0100 Subject: [PATCH 052/285] Update docs --- docs/source/pyrogram/Client.rst | 3 ++- docs/source/resources/UpdateHandling.rst | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 954900d8..b4bb1270 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -14,8 +14,9 @@ Client start stop idle - set_event_handler + set_update_handler send + resolve_peer get_me send_message forward_messages diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index de390d7e..594bdee4 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -29,8 +29,9 @@ Here's a complete example on how to set it up: main() The last line of the main() function, :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. +recommended when using the update handler; 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 -------- @@ -41,6 +42,7 @@ Examples from pyrogram.api import types + def update_handler(client, update, users, chats): if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (PM and Groups) message = update.message # type: types.Message From c2646b03607a38a143c3a13fe55ca6a05310d474 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 15 Feb 2018 21:49:09 +0100 Subject: [PATCH 053/285] Update version on docs --- docs/source/start/QuickInstallation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index 2b047b76..86be098d 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -38,4 +38,4 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.5.0' + '0.6.0' From c47fe4190ae531402129f3674d8bbef30aaf78c8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 16 Feb 2018 19:07:03 +0100 Subject: [PATCH 054/285] Add missing colon --- docs/source/resources/UpdateHandling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 594bdee4..17f39eeb 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -18,7 +18,7 @@ Here's a complete example on how to set it up: def update_handler(client, update, users, chats): print(update) - def main() + def main(): client = Client(session_name="example") client.set_update_handler(update_handler) From 50d56f46d2865436db598f8f41b89f2791915ba3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 14:44:59 +0100 Subject: [PATCH 055/285] Update TgCrypto docs --- docs/source/index.rst | 10 ++++---- docs/source/resources/FastCrypto.rst | 37 ---------------------------- docs/source/resources/TgCrypto.rst | 32 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 42 deletions(-) delete mode 100644 docs/source/resources/FastCrypto.rst create mode 100644 docs/source/resources/TgCrypto.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 45fc483f..9c27bc22 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,9 +35,9 @@ Welcome to Pyrogram Scheme Layer 75 - - MTProto v2.0 + + TgCrypto

@@ -64,7 +64,7 @@ Features a reliable connection. - **Fast**: Pyrogram's speed is boosted up by `TgCrypto`_, a high-performance, easy-to-install - crypto library written in C. + Telegram Crypto Library written in C as a Python extension. - **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 75`_. @@ -108,7 +108,7 @@ To get started, press Next. resources/ErrorHandling resources/SOCKS5Proxy resources/AutoAuthorization - resources/FastCrypto + resources/TgCrypto .. toctree:: :hidden: 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/TgCrypto.rst b/docs/source/resources/TgCrypto.rst new file mode 100644 index 00000000..64759b9b --- /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 + + $ pip 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. From b606ec44c5a31b7f994f68f2cbedcc5c1795554a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 15:44:53 +0100 Subject: [PATCH 056/285] Update docs --- docs/source/index.rst | 2 +- docs/source/start/QuickInstallation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9c27bc22..b74ff93c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -125,6 +125,6 @@ To get started, press Next. .. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto -.. _TgCrypto: https://docs.pyrogram.ml/resources/FastCrypto/ +.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto/ .. _`Layer 75`: https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl \ No newline at end of file diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index 86be098d..221acd36 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -38,4 +38,4 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.6.0' + '0.6.1' From 0b4fa8b51730878cd253ee999e81fc21c57b0b2d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 17 Feb 2018 16:25:05 +0100 Subject: [PATCH 057/285] Add TgCrypto installation command (as extra) --- docs/source/start/QuickInstallation.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index 221acd36..e9e4cba8 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -7,6 +7,12 @@ The most straightforward and recommended way to install or upgrade Pyrogram is b $ pip install --upgrade pyrogram +Or, with TgCrypto_: + +.. code-block:: bash + + $ pip install --upgrade pyrogram[tgcrypto] + .. important:: Pyrogram only works on Python 3.3 or higher; if your **pip** points to Python 2.x use **pip3** instead. @@ -39,3 +45,5 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ '0.6.1' + +.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file From 35d5c4170cfbf91982f18100086613b10a0f3a13 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 21 Feb 2018 12:21:10 +0100 Subject: [PATCH 058/285] Update docs --- docs/source/resources/AutoAuthorization.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst index 304c9fa5..7b56e1f0 100644 --- a/docs/source/resources/AutoAuthorization.rst +++ b/docs/source/resources/AutoAuthorization.rst @@ -5,6 +5,10 @@ Manually writing phone number, phone code and password on the terminal every tim Pyrogram is able to automate both **Sign-In** and **Sign-Up** processes, all you need to do is pass the relevant parameters when creating a new 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. + Sign-In ------- From 7439e1e8f28c2eaa5e367cfc5023fb8882ae1fe4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 22 Feb 2018 11:02:38 +0100 Subject: [PATCH 059/285] Update docs --- docs/source/pyrogram/Client.rst | 4 +- docs/source/pyrogram/InputPhoneContact.rst | 6 ++ docs/source/pyrogram/index.rst | 1 + docs/source/resources/SOCKS5Proxy.rst | 25 +++++- docs/source/start/BasicUsage.rst | 91 ++++++++++++++++++++++ docs/source/start/ProjectSetup.rst | 28 +++++-- 6 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 docs/source/pyrogram/InputPhoneContact.rst diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index b4bb1270..84e49749 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -26,6 +26,7 @@ Client send_video send_voice send_video_note + send_media_group send_location send_venue send_contact @@ -40,4 +41,5 @@ Client enable_cloud_password change_cloud_password remove_cloud_password - send_media_group \ No newline at end of file + add_contacts + delete_contacts \ No newline at end of file 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/index.rst b/docs/source/pyrogram/index.rst index aa10ec4c..7f64648d 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -13,6 +13,7 @@ the same parameters as well, thus offering a familiar look to Bot developers. ParseMode Emoji InputMedia + InputPhoneContact Error .. _Telegram Bot API: https://core.telegram.org/bots/api#available-methods diff --git a/docs/source/resources/SOCKS5Proxy.rst b/docs/source/resources/SOCKS5Proxy.rst index 55993deb..0004ecad 100644 --- a/docs/source/resources/SOCKS5Proxy.rst +++ b/docs/source/resources/SOCKS5Proxy.rst @@ -25,5 +25,26 @@ with your own settings: - ``1``, ``yes``, ``True`` or ``on``: Enables the proxy - ``0``, ``no``, ``False`` or ``off``: Disables the proxy -- If your proxy doesn't require authorization you can omit username and password by either leaving the values blank - or completely delete the lines. \ No newline at end of file +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 + + client = Client( + session_name="example", + proxy=dict( + hostname="11.22.33.44", + port=1080, + username="", + password="" + ) + ) + + client.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/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index e9411c60..c602b413 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -50,6 +50,8 @@ Here some examples: from pyrogram.api import functions + ... + client.send( functions.account.UpdateProfile( first_name="Dan", last_name="Tès", @@ -63,6 +65,8 @@ Here some examples: from pyrogram.api import functions, types + ... + client.send( functions.account.SetPrivacy( key=types.InputPrivacyKeyStatusTimestamp(), @@ -70,4 +74,91 @@ Here some examples: ) ) +- Invite users to your channel/supergroup: + + .. code-block:: python + + from pyrogram.api import functions, types + + ... + + client.send( + functions.channels.InviteToChannel( + channel=client.resolve_peer(123456789), # ID or Username of your channel + users=[ # The users you want to invite + client.resolve_peer(23456789), # By ID + client.resolve_peer("username"), # By username + client.resolve_peer("393281234567"), # By phone number + ] + ) + ) + +- Get channel/supergroup participants: + + .. code-block:: python + + import time + from pyrogram.api import types, functions + + ... + + users = [] + limit = 200 + offset = 0 + + while True: + try: + participants = client.send( + functions.channels.GetParticipants( + channel=client.resolve_peer("username"), # ID or username + filter=types.ChannelParticipantsSearch(""), # Filter by empty string (search for all) + offset=offset, + limit=limit, + hash=0 + ) + ) + except FloodWait as e: + # Very large channels will trigger FloodWait. + # In that case wait X seconds before continuing + time.sleep(e.x) + continue + + if not participants.participants: + break # No more participants left + + users.extend(participants.users) + offset += len(participants.users) + +- Get history of a chat: + + .. code-block:: python + + import time + from pyrogram.api import types, functions + + ... + + history = [] + limit = 100 + offset = 0 + + while True: + try: + messages = client.send( + functions.messages.GetHistory( + client.resolve_peer("me"), # Get your own history + 0, 0, offset, limit, 0, 0, 0 + ) + ) + except FloodWait as e: + # For very large histories the method call can raise a FloodWait + time.sleep(e.x) + continue + + if not messages.messages: + break # No more messages left + + history.extend(messages.messages) + offset += len(messages.messages) + .. _bot-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 index bc2b6b74..2899fb7a 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/ProjectSetup.rst @@ -19,14 +19,30 @@ If you already have one you can skip this, otherwise: 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>`_: +There are two ways to configure a Pyrogram application project, and you can choose the one that fits better for you: -.. code-block:: ini +- 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>`_: - [pyrogram] - api_id = 12345 - api_hash = 0123456789abcdef0123456789abcdef + .. code-block:: ini + + [pyrogram] + api_id = 12345 + api_hash = 0123456789abcdef0123456789abcdef + +- Alternatively, you can pass your API key to Pyrogram by simply using the *api_key* parameter of the Client class: + + .. code-block:: python + + from pyrogram import Client + + client = Client( + session_name="example", + api_key=(12345, "0123456789abcdef0123456789abcdef") + ) + +.. note:: The examples below will assume you have created a *config.ini* file, thus they won't show the *api_key* + parameter usage in the Client class. Authorization ------------- From 197539809afaa515de4832923de93cb5410a2486 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 22 Feb 2018 13:07:41 +0100 Subject: [PATCH 060/285] Update docs --- docs/source/resources/ErrorHandling.rst | 9 +++++---- docs/source/start/ProjectSetup.rst | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/source/resources/ErrorHandling.rst b/docs/source/resources/ErrorHandling.rst index 71a3b7a7..0d5cf6f9 100644 --- a/docs/source/resources/ErrorHandling.rst +++ b/docs/source/resources/ErrorHandling.rst @@ -43,17 +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: ... 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/start/ProjectSetup.rst b/docs/source/start/ProjectSetup.rst index 2899fb7a..3d0dea32 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/ProjectSetup.rst @@ -21,8 +21,9 @@ 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>`_: +- 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 @@ -30,7 +31,8 @@ There are two ways to configure a Pyrogram application project, and you can choo api_id = 12345 api_hash = 0123456789abcdef0123456789abcdef -- Alternatively, you can pass your API key to Pyrogram by simply using the *api_key* parameter of the Client class: +- Alternatively, you can pass your API key to Pyrogram by simply using the *api_key* parameter of the Client class. + This way you can have full control on how to store and load your credentials: .. code-block:: python From 1677220f178a32de92f901dfe378fdc3c4e1fd80 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 24 Feb 2018 17:50:32 +0100 Subject: [PATCH 061/285] Add send_sticker to docs --- docs/source/pyrogram/Client.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 84e49749..f0b6ca6b 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -31,6 +31,7 @@ Client send_venue send_contact send_chat_action + send_sticker get_user_profile_photos edit_message_text edit_message_caption From c89130b5c6c45bd52d37d6cfc801a532e77d4d7e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 24 Feb 2018 17:58:21 +0100 Subject: [PATCH 062/285] Update docs examples --- docs/source/start/BasicUsage.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index c602b413..db8ad2cf 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -119,7 +119,7 @@ Here some examples: ) except FloodWait as e: # Very large channels will trigger FloodWait. - # In that case wait X seconds before continuing + # When happens, wait X seconds before continuing time.sleep(e.x) continue @@ -127,7 +127,7 @@ Here some examples: break # No more participants left users.extend(participants.users) - offset += len(participants.users) + offset += limit - Get history of a chat: @@ -159,6 +159,6 @@ Here some examples: break # No more messages left history.extend(messages.messages) - offset += len(messages.messages) + offset += limit .. _bot-like: https://core.telegram.org/bots/api#available-methods \ No newline at end of file From 454ae5402ce21650b465d4629a4bc36d8d289692 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 16:53:23 +0100 Subject: [PATCH 063/285] Add get_inline_bot_results and send_inline_bot_result methods to docs --- docs/source/_static/get_inline_bot_results.png | Bin 0 -> 98119 bytes docs/source/_static/send_inline_bot_result.png | Bin 0 -> 131765 bytes docs/source/pyrogram/Client.rst | 4 +++- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/source/_static/get_inline_bot_results.png create mode 100644 docs/source/_static/send_inline_bot_result.png diff --git a/docs/source/_static/get_inline_bot_results.png b/docs/source/_static/get_inline_bot_results.png new file mode 100644 index 0000000000000000000000000000000000000000..4e84a15d8693db415fceec5b004845086051f787 GIT binary patch literal 98119 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�i$<@X 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 z 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

mm^=yr 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 literal 0 HcmV?d00001 diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index f0b6ca6b..aab0ea4c 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -43,4 +43,6 @@ Client change_cloud_password remove_cloud_password add_contacts - delete_contacts \ No newline at end of file + delete_contacts + get_inline_bot_results + send_inline_bot_result \ No newline at end of file From 48f13229bb035a83959571c19066c1cef0a2c2db Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 16:53:41 +0100 Subject: [PATCH 064/285] Add BotsInteraction page --- docs/source/resources/BotsInteraction.rst | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/source/resources/BotsInteraction.rst diff --git a/docs/source/resources/BotsInteraction.rst b/docs/source/resources/BotsInteraction.rst new file mode 100644 index 00000000..b3306fe1 --- /dev/null +++ b/docs/source/resources/BotsInteraction.rst @@ -0,0 +1,39 @@ +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 + :obj:`get_inline_bot_results ` to get the list of its inline results: + + .. code-block:: python + + # Get bot results for "Fuzz Universe" from the inline bot @vid + bot_results = client.get_inline_bot_results("vid", "Fuzz Universe") + + .. figure:: ../_static/get_inline_bot_results.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 + :obj:`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) + client.send_inline_bot_result("me", bot_results.query_id, bot_results.results[0].id) + + .. figure:: ../_static/send_inline_bot_result.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. From 33ea5d864115cb17a6b07c74724264b10b41d0c1 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 16:54:07 +0100 Subject: [PATCH 065/285] Remove some redundant examples --- docs/source/start/BasicUsage.rst | 68 -------------------------------- 1 file changed, 68 deletions(-) diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index db8ad2cf..f154c086 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -93,72 +93,4 @@ Here some examples: ) ) -- Get channel/supergroup participants: - - .. code-block:: python - - import time - from pyrogram.api import types, functions - - ... - - users = [] - limit = 200 - offset = 0 - - while True: - try: - participants = client.send( - functions.channels.GetParticipants( - channel=client.resolve_peer("username"), # ID or username - filter=types.ChannelParticipantsSearch(""), # Filter by empty string (search for all) - offset=offset, - limit=limit, - hash=0 - ) - ) - except FloodWait as e: - # Very large channels will trigger FloodWait. - # When happens, wait X seconds before continuing - time.sleep(e.x) - continue - - if not participants.participants: - break # No more participants left - - users.extend(participants.users) - offset += limit - -- Get history of a chat: - - .. code-block:: python - - import time - from pyrogram.api import types, functions - - ... - - history = [] - limit = 100 - offset = 0 - - while True: - try: - messages = client.send( - functions.messages.GetHistory( - client.resolve_peer("me"), # Get your own history - 0, 0, offset, limit, 0, 0, 0 - ) - ) - except FloodWait as e: - # For very large histories the method call can raise a FloodWait - time.sleep(e.x) - continue - - if not messages.messages: - break # No more messages left - - history.extend(messages.messages) - offset += limit - .. _bot-like: https://core.telegram.org/bots/api#available-methods \ No newline at end of file From 4b34e695cb462e1cb8c69b6f0d115959ce505faa Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 16:54:33 +0100 Subject: [PATCH 066/285] Add BotsInteraction to the index page --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index b74ff93c..204430e5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -109,6 +109,7 @@ To get started, press Next. resources/SOCKS5Proxy resources/AutoAuthorization resources/TgCrypto + resources/BotsInteraction .. toctree:: :hidden: From f7e304493943fec23aedefdfa9c1ac79b53ec855 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:15:10 +0100 Subject: [PATCH 067/285] Rename Sign In to Log In for more clarity --- docs/source/resources/AutoAuthorization.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst index 7b56e1f0..1aff63c5 100644 --- a/docs/source/resources/AutoAuthorization.rst +++ b/docs/source/resources/AutoAuthorization.rst @@ -2,17 +2,17 @@ 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 **Sign-In** and **Sign-Up** processes, all you need to do is pass the relevant +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 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. -Sign-In +Log In ------- -To automate the **Sign-In** process, pass your *phone_number* and *password* (if you have one) in the Client parameters. +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. @@ -36,10 +36,10 @@ ask you to input the phone code manually. client.start() print(client.get_me()) -Sign-Up +Sign Up ------- -To automate the **Sign-Up** process (i.e., automatically create a new Telegram account), simply fill **both** +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. From 2c917fa9aeb55531ebe9146f0ebc19a2b7784a78 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:15:37 +0100 Subject: [PATCH 068/285] Use pip3 instead of pip --- docs/source/resources/TgCrypto.rst | 2 +- docs/source/start/QuickInstallation.rst | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/source/resources/TgCrypto.rst b/docs/source/resources/TgCrypto.rst index 64759b9b..734c48e4 100644 --- a/docs/source/resources/TgCrypto.rst +++ b/docs/source/resources/TgCrypto.rst @@ -12,7 +12,7 @@ Installation .. code-block:: bash - $ pip install --upgrade tgcrypto + $ 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. diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index e9e4cba8..d9062476 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -5,17 +5,13 @@ The most straightforward and recommended way to install or upgrade Pyrogram is b .. code-block:: bash - $ pip install --upgrade pyrogram + $ pip3 install --upgrade pyrogram Or, with TgCrypto_: .. code-block:: bash - $ pip install --upgrade pyrogram[tgcrypto] - -.. important:: - - Pyrogram only works on Python 3.3 or higher; if your **pip** points to Python 2.x use **pip3** instead. + $ pip3 install --upgrade pyrogram[tgcrypto] Bleeding Edge ------------- @@ -24,9 +20,9 @@ If you want the latest development version of the library, you can either instal .. code-block:: bash - $ pip install --upgrade git+https://github.com/pyrogram/pyrogram.git + $ pip3 install --upgrade git+https://github.com/pyrogram/pyrogram.git -or manually, using: +Or manually, using: .. code-block:: bash From 03916a47927aff86f5525fa52d8d99e88acaeefe Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:18:25 +0100 Subject: [PATCH 069/285] Update Proxy page --- docs/source/resources/SOCKS5Proxy.rst | 56 +++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/source/resources/SOCKS5Proxy.rst b/docs/source/resources/SOCKS5Proxy.rst index 0004ecad..a9b51a39 100644 --- a/docs/source/resources/SOCKS5Proxy.rst +++ b/docs/source/resources/SOCKS5Proxy.rst @@ -7,44 +7,44 @@ 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: +- 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 + .. code-block:: ini - [proxy] - enabled = True - hostname = 11.22.33.44 - port = 1080 - username = - password = + [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: + - 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 + - ``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: +- 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 + .. code-block:: python - from pyrogram import Client + from pyrogram import Client - client = Client( - session_name="example", - proxy=dict( - hostname="11.22.33.44", - port=1080, - username="", - password="" - ) - ) + client = Client( + session_name="example", + proxy=dict( + hostname="11.22.33.44", + port=1080, + username="", + password="" + ) + ) - client.start() + client.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 From 4a173500dd7edd762fd40d4235b153248cfa426f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:18:54 +0100 Subject: [PATCH 070/285] Replace code examples with links to the example files on github --- docs/source/resources/UpdateHandling.rst | 100 +---------------------- 1 file changed, 3 insertions(+), 97 deletions(-) diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 17f39eeb..4293054f 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -36,100 +36,6 @@ connection. Examples -------- -- Simple Echo example for **Private Messages**: - - .. code-block:: python - - from pyrogram.api import types - - def update_handler(client, update, users, chats): - if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (PM and Groups) - 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(message.from_id, message.message, reply_to_message_id=message.id) - - - -- Advanced Echo example for both **Private Messages** and **Basic Groups** (with mentions). - - .. code-block:: python - - from pyrogram.api import types - - 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 - ) - -- Advanced Echo example for **Supergroups** (with mentions): - - .. code-block:: python - - from pyrogram.api import types - - def update_handler(client, update, users, chats): - 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 - ) - -.. warning:: - The Advanced Examples above will make you reply to **all** new messages in private chats and in every single - group/supergroup you are in. Make sure you add an extra check to filter them: - - .. code-block:: python - - # Filter Groups by ID - if message.to_id.chat_id == MY_GROUP_ID: - ... - - # 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: - ... \ No newline at end of file +- `Simple Echo `_ +- `Advanced Echo `_ +- `Advanced Echo 2 `_ From afb48d5d31c97a6951d49e1f2feb29b2295f7e03 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:25:59 +0100 Subject: [PATCH 071/285] Update example --- docs/source/start/BasicUsage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index f154c086..50b21375 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -84,7 +84,7 @@ Here some examples: client.send( functions.channels.InviteToChannel( - channel=client.resolve_peer(123456789), # ID or Username of your channel + channel=client.resolve_peer(123456789), # ID or Username users=[ # The users you want to invite client.resolve_peer(23456789), # By ID client.resolve_peer("username"), # By username From c3ca21f4ed390589d72ee4eff9515bf8c8e0fafe Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:40:50 +0100 Subject: [PATCH 072/285] Update docs --- docs/source/resources/BotsInteraction.rst | 3 ++- docs/source/resources/SOCKS5Proxy.rst | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/source/resources/BotsInteraction.rst b/docs/source/resources/BotsInteraction.rst index b3306fe1..486ee397 100644 --- a/docs/source/resources/BotsInteraction.rst +++ b/docs/source/resources/BotsInteraction.rst @@ -7,7 +7,8 @@ Inline Bots ----------- - If a bot accepts inline queries, you can call it by using - :obj:`get_inline_bot_results ` to get the list of its inline results: + :obj:`get_inline_bot_results ` to get the list of its inline results + for a query: .. code-block:: python diff --git a/docs/source/resources/SOCKS5Proxy.rst b/docs/source/resources/SOCKS5Proxy.rst index a9b51a39..02822eeb 100644 --- a/docs/source/resources/SOCKS5Proxy.rst +++ b/docs/source/resources/SOCKS5Proxy.rst @@ -19,11 +19,11 @@ Usage username = password = - - To enable or disable the proxy without deleting your settings from the config file, - change the ``enabled`` value as follows: + 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 + - ``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: From cf965a0337dbab91c147b72b88197e0fced1aeae Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 18:41:11 +0100 Subject: [PATCH 073/285] Update version on docs --- docs/source/start/QuickInstallation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index d9062476..cafa7f0d 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -40,6 +40,6 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.6.1' + '0.6.2' .. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file From 3338e6c9b7f56cf6244f20a3f875a903df67c940 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 27 Feb 2018 20:07:01 +0100 Subject: [PATCH 074/285] Add download_media to docs --- docs/source/pyrogram/Client.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index aab0ea4c..2b864799 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -32,6 +32,7 @@ Client send_contact send_chat_action send_sticker + download_media get_user_profile_photos edit_message_text edit_message_caption From 63d3dd6dc16e642c636e8ca7b155b3d1a610dfcf Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 28 Feb 2018 01:28:31 +0100 Subject: [PATCH 075/285] Use upper case names for string constants --- compiler/docs/compiler.py | 38 +++++++++++++++++++------------------- compiler/error/compiler.py | 36 ++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 0d797f6f..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,7 @@ 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") @@ -114,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() From b5c9eb2e76c6f993b9b7a9798f1feb5bf4099e7c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 1 Mar 2018 00:11:12 +0100 Subject: [PATCH 076/285] Move images to _images folder --- .../{_static => _images}/get_inline_bot_results.png | Bin .../{_static => _images}/send_inline_bot_result.png | Bin docs/source/resources/BotsInteraction.rst | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/source/{_static => _images}/get_inline_bot_results.png (100%) rename docs/source/{_static => _images}/send_inline_bot_result.png (100%) diff --git a/docs/source/_static/get_inline_bot_results.png b/docs/source/_images/get_inline_bot_results.png similarity index 100% rename from docs/source/_static/get_inline_bot_results.png rename to docs/source/_images/get_inline_bot_results.png diff --git a/docs/source/_static/send_inline_bot_result.png b/docs/source/_images/send_inline_bot_result.png similarity index 100% rename from docs/source/_static/send_inline_bot_result.png rename to docs/source/_images/send_inline_bot_result.png diff --git a/docs/source/resources/BotsInteraction.rst b/docs/source/resources/BotsInteraction.rst index 486ee397..ec436224 100644 --- a/docs/source/resources/BotsInteraction.rst +++ b/docs/source/resources/BotsInteraction.rst @@ -15,7 +15,7 @@ Inline Bots # Get bot results for "Fuzz Universe" from the inline bot @vid bot_results = client.get_inline_bot_results("vid", "Fuzz Universe") - .. figure:: ../_static/get_inline_bot_results.png + .. figure:: ../_images/get_inline_bot_results.png :width: 90% :align: center :figwidth: 60% @@ -31,7 +31,7 @@ Inline Bots # 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) - .. figure:: ../_static/send_inline_bot_result.png + .. figure:: ../_images/send_inline_bot_result.png :width: 90% :align: center :figwidth: 60% From f7b8c0389dc86321b25dfc9c4ac97f9efe7ce893 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 2 Mar 2018 02:27:17 +0100 Subject: [PATCH 077/285] Add INVITE_HASH_INVALID and USER_ALREADY_PARTICIPANT errors --- compiler/error/source/400_BAD_REQUEST.tsv | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index ffba8987..a419c5e2 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -48,4 +48,6 @@ 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 \ No newline at end of file From 9638984d6bb1390252de2090f738c6c3b243aec8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 3 Mar 2018 01:53:39 +0100 Subject: [PATCH 078/285] Add TTL_MEDIA_INVALID error --- compiler/error/source/400_BAD_REQUEST.tsv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index a419c5e2..d2476dc0 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -50,4 +50,5 @@ 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 INVITE_HASH_INVALID The invite link hash is invalid -USER_ALREADY_PARTICIPANT The user is already a participant of this chat \ No newline at end of file +USER_ALREADY_PARTICIPANT The user is already a participant of this chat +TTL_MEDIA_INVALID This kind of media does not support self-destruction \ No newline at end of file From 5793192d1e199a4e407a0aa55d601cf80f0bd140 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 4 Mar 2018 01:25:06 +0100 Subject: [PATCH 079/285] Add some more errors --- compiler/error/source/400_BAD_REQUEST.tsv | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index d2476dc0..ce5a6118 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -51,4 +51,10 @@ BOT_INLINE_DISABLED The inline feature of the bot is disabled 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 \ No newline at end of file +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 \ No newline at end of file From 8c82aff7594f3ce33d4ccf4cdd9b4e0b9a2b3586 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 5 Mar 2018 02:05:49 +0100 Subject: [PATCH 080/285] Fix tabs being spaces --- compiler/error/source/400_BAD_REQUEST.tsv | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index ce5a6118..dfb2c062 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -51,10 +51,10 @@ BOT_INLINE_DISABLED The inline feature of the bot is disabled 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 \ No newline at end of file +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 \ No newline at end of file From aab1d848eba01562bb9bea5f76390213a5619399 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 6 Mar 2018 01:48:08 +0100 Subject: [PATCH 081/285] Add RPC_CALL_FAIL and RPC_MCGET_FAIL --- compiler/error/source/400_BAD_REQUEST.tsv | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index dfb2c062..ae51c7f5 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -57,4 +57,6 @@ 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 \ No newline at end of file +EMAIL_INVALID The email provided is invalid +RPC_CALL_FAIL The method can't be called because Telegram is having internal problems. Please try again later +RPC_MCGET_FAIL The method can't be called because Telegram is having internal problems. Please try again later \ No newline at end of file From 8216201f19ccacfc08998bace78e16eabc77a2e5 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:44:31 +0100 Subject: [PATCH 082/285] Add Voice type --- pyrogram/client/types/voice.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyrogram/client/types/voice.py diff --git a/pyrogram/client/types/voice.py b/pyrogram/client/types/voice.py new file mode 100644 index 00000000..d21559f0 --- /dev/null +++ b/pyrogram/client/types/voice.py @@ -0,0 +1,10 @@ +class Voice: + def __init__(self, + file_id: str, + duration: int, + mime_type: str = None, + file_size: int = None): + self.file_id = file_id + self.duration = duration + self.mime_type = mime_type + self.file_size = file_size From 8bd5a796438c3392c5d71c4d9dfa054d799fe04b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:44:43 +0100 Subject: [PATCH 083/285] Add Animation type --- pyrogram/client/types/animation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyrogram/client/types/animation.py diff --git a/pyrogram/client/types/animation.py b/pyrogram/client/types/animation.py new file mode 100644 index 00000000..a3693396 --- /dev/null +++ b/pyrogram/client/types/animation.py @@ -0,0 +1,15 @@ +from . import PhotoSize + + +class Animation: + def __init__(self, + file_id: str, + thumb: PhotoSize = None, + file_name: str = None, + mime_type: str = None, + file_size: int = None): + self.file_id = file_id + self.thumb = thumb + self.file_name = file_name + self.mime_type = mime_type + self.file_size = file_size From 18c20f0ca5d69cca238e90eb923798688656dd77 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:44:53 +0100 Subject: [PATCH 084/285] Add Game type --- pyrogram/client/types/game.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 pyrogram/client/types/game.py diff --git a/pyrogram/client/types/game.py b/pyrogram/client/types/game.py new file mode 100644 index 00000000..387af3f4 --- /dev/null +++ b/pyrogram/client/types/game.py @@ -0,0 +1,17 @@ +from . import Animation + + +class Game: + def __init__(self, + title: str, + description: str, + photo: list, + text: str = None, + text_entities: list = None, + animation: Animation = None): + self.title = title + self.description = description + self.photo = photo + self.text = text + self.text_entities = text_entities + self.animation = animation From 48bf7438b71b48bfa4bd766677a59ebe6e192aa7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:45:06 +0100 Subject: [PATCH 085/285] Add PhotoSize type --- pyrogram/client/types/photo_size.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyrogram/client/types/photo_size.py diff --git a/pyrogram/client/types/photo_size.py b/pyrogram/client/types/photo_size.py new file mode 100644 index 00000000..3ada21d7 --- /dev/null +++ b/pyrogram/client/types/photo_size.py @@ -0,0 +1,10 @@ +class PhotoSize: + def __init__(self, + file_id: str, + width: int, + height: int, + file_size: int): + self.file_id = file_id + self.width = width + self.height = height + self.file_size = file_size From 4e4d9e6e44c0d74401c143ed07762d1ab8ceee29 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:45:18 +0100 Subject: [PATCH 086/285] Add MessageEntity type --- pyrogram/client/types/message_entity.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyrogram/client/types/message_entity.py diff --git a/pyrogram/client/types/message_entity.py b/pyrogram/client/types/message_entity.py new file mode 100644 index 00000000..bdc686c0 --- /dev/null +++ b/pyrogram/client/types/message_entity.py @@ -0,0 +1,15 @@ +from . import User + + +class MessageEntity: + def __init__(self, + type: str, + offset: int, + length: int, + url: str = None, + user: User = None): + self.type = type + self.offset = offset + self.length = length + self.url = url + self.user = user From 33baf91a26051ec8127742795aba48b0353bd163 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:45:27 +0100 Subject: [PATCH 087/285] Add Update type --- pyrogram/client/types/update.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 pyrogram/client/types/update.py diff --git a/pyrogram/client/types/update.py b/pyrogram/client/types/update.py new file mode 100644 index 00000000..f367afc3 --- /dev/null +++ b/pyrogram/client/types/update.py @@ -0,0 +1,28 @@ +from . import Message + + +class Update: + """This object represents an incoming update. + At most one of the optional parameters can be present in any given update. + + + Args: + message (:obj:`Message `): + + edited_message (): + + channel_post (): + + edited_channel_post (): + + """ + + def __init__(self, + message: Message = None, + edited_message: Message = None, + channel_post: Message = None, + edited_channel_post: Message = None): + self.message = message + self.edited_message = edited_message + self.channel_post = channel_post + self.edited_channel_post = edited_channel_post From 3fa30b3a505a5eb014b23893bf3ded7ce53f970e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:45:40 +0100 Subject: [PATCH 088/285] Add Contact type --- pyrogram/client/types/contact.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyrogram/client/types/contact.py diff --git a/pyrogram/client/types/contact.py b/pyrogram/client/types/contact.py new file mode 100644 index 00000000..e07fb441 --- /dev/null +++ b/pyrogram/client/types/contact.py @@ -0,0 +1,10 @@ +class Contact: + def __init__(self, + phone_number: str, + first_name: str, + last_name: str = None, + user_id: int = None): + self.phone_number = phone_number + self.first_name = first_name + self.last_name = last_name + self.user_id = user_id From 35fcfe8266d93a353a5ea8f7235d56c810f4cc5a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:45:58 +0100 Subject: [PATCH 089/285] Add Sticker type --- pyrogram/client/types/sticker.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 pyrogram/client/types/sticker.py diff --git a/pyrogram/client/types/sticker.py b/pyrogram/client/types/sticker.py new file mode 100644 index 00000000..1e87fd84 --- /dev/null +++ b/pyrogram/client/types/sticker.py @@ -0,0 +1,21 @@ +from . import PhotoSize, MaskPosition + + +class Sticker: + def __init__(self, + file_id: str, + width: int, + height: int, + thumb: PhotoSize = None, + emoji: str = None, + set_name: str = None, + mask_position: MaskPosition = None, + file_size: int = None): + self.file_id = file_id + self.width = width + self.height = height + self.thumb = thumb + self.emoji = emoji + self.set_name = set_name + self.mask_position = mask_position + self.file_size = file_size From 281323288e385fde3ae591af528f973894f9bea6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:46:08 +0100 Subject: [PATCH 090/285] Add Document type --- pyrogram/client/types/document.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyrogram/client/types/document.py diff --git a/pyrogram/client/types/document.py b/pyrogram/client/types/document.py new file mode 100644 index 00000000..cc74c4e7 --- /dev/null +++ b/pyrogram/client/types/document.py @@ -0,0 +1,15 @@ +from . import PhotoSize + + +class Document: + def __init__(self, + file_id: str, + thumb: PhotoSize = None, + file_name: str = None, + mime_type: str = None, + file_size: int = None): + self.file_id = file_id + self.thumb = thumb + self.file_name = file_name + self.mime_type = mime_type + self.file_size = file_size From 24c2d20137c3601f8d835eb483f8536ead6c4cf1 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:46:16 +0100 Subject: [PATCH 091/285] Add Venue type --- pyrogram/client/types/venue.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pyrogram/client/types/venue.py diff --git a/pyrogram/client/types/venue.py b/pyrogram/client/types/venue.py new file mode 100644 index 00000000..f1c058fd --- /dev/null +++ b/pyrogram/client/types/venue.py @@ -0,0 +1,13 @@ +from . import Location + + +class Venue: + def __init__(self, + location: Location, + title: str, + address: str, + foursquare_id: str = None): + self.location = location + self.title = title + self.address = address + self.foursquare_id = foursquare_id From 472495397179c98946afb972b327d2ada7fbff43 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:46:25 +0100 Subject: [PATCH 092/285] Add VideoNote type --- pyrogram/client/types/video_note.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pyrogram/client/types/video_note.py diff --git a/pyrogram/client/types/video_note.py b/pyrogram/client/types/video_note.py new file mode 100644 index 00000000..3969c2d9 --- /dev/null +++ b/pyrogram/client/types/video_note.py @@ -0,0 +1,15 @@ +from . import PhotoSize + + +class VideoNote: + def __init__(self, + file_id: str, + length: int, + duration: int, + thumb: PhotoSize = None, + file_size: int = None): + self.file_id = file_id + self.length = length + self.duration = duration + self.thumb = thumb + self.file_size = file_size From f3f462c009caa5e684cbc50505234c36f653d5bf Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:46:38 +0100 Subject: [PATCH 093/285] Add MaskPosition type --- pyrogram/client/types/mask_position.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyrogram/client/types/mask_position.py diff --git a/pyrogram/client/types/mask_position.py b/pyrogram/client/types/mask_position.py new file mode 100644 index 00000000..a7720dc7 --- /dev/null +++ b/pyrogram/client/types/mask_position.py @@ -0,0 +1,10 @@ +class MaskPosition: + def __init__(self, + point: str, + x_shift: float, + y_shift: float, + scale: float): + self.point = point + self.x_shift = x_shift + self.y_shift = y_shift + self.scale = scale From 4d367ce04e66f7f38abc62fbd3baae8d437b6a61 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:46:49 +0100 Subject: [PATCH 094/285] Add ChatMember type --- pyrogram/client/types/chat_member.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pyrogram/client/types/chat_member.py diff --git a/pyrogram/client/types/chat_member.py b/pyrogram/client/types/chat_member.py new file mode 100644 index 00000000..e8e4d9be --- /dev/null +++ b/pyrogram/client/types/chat_member.py @@ -0,0 +1,37 @@ +from . import User + + +class ChatMember: + def __init__(self, + user: User, + status: str, + until_date: int = None, + can_be_edited: bool = None, + can_change_info: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_invite_users: bool = None, + can_restrict_members: bool = None, + can_pin_messages: bool = None, + can_promote_members: bool = None, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_pewviews: bool = None): + self.user = user + self.status = status + self.until_date = until_date + self.can_be_edited = can_be_edited + self.can_change_info = can_change_info + self.can_post_messages = can_post_messages + self.can_edit_messages = can_edit_messages + self.can_delete_messages = can_delete_messages + self.can_invite_users = can_invite_users + self.can_restrict_members = can_restrict_members + self.can_pin_messages = can_pin_messages + self.can_promote_members = can_promote_members + self.can_send_messages = can_send_messages + self.can_send_media_messages = can_send_media_messages + self.can_send_other_messages = can_send_other_messages + self.can_add_web_page_pewviews = can_add_web_page_pewviews From 59f1fd9ee62ce5a43dec39256f7de1b5ce6cc7f9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:46:58 +0100 Subject: [PATCH 095/285] Add Location type --- pyrogram/client/types/location.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pyrogram/client/types/location.py diff --git a/pyrogram/client/types/location.py b/pyrogram/client/types/location.py new file mode 100644 index 00000000..5252e67f --- /dev/null +++ b/pyrogram/client/types/location.py @@ -0,0 +1,6 @@ +class Location: + def __init__(self, + longitude: float, + latitude: float): + self.longitude = longitude + self.latitude = latitude From 345da2fccdfd6ccb0269c27f96a3aff83b4dcf5f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:47:12 +0100 Subject: [PATCH 096/285] Add UserProfilePhotos type --- pyrogram/client/types/user_profile_photos.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pyrogram/client/types/user_profile_photos.py diff --git a/pyrogram/client/types/user_profile_photos.py b/pyrogram/client/types/user_profile_photos.py new file mode 100644 index 00000000..f22296d2 --- /dev/null +++ b/pyrogram/client/types/user_profile_photos.py @@ -0,0 +1,6 @@ +class UserProfilePhotos: + def __init__(self, + total_count: int, + photos: list): + self.total_count = total_count + self.photos = photos From 76b78f6a9e438cc340f1b8e6e93515b979551a60 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:47:22 +0100 Subject: [PATCH 097/285] Add ChatPhoto type --- pyrogram/client/types/chat_photo.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pyrogram/client/types/chat_photo.py diff --git a/pyrogram/client/types/chat_photo.py b/pyrogram/client/types/chat_photo.py new file mode 100644 index 00000000..c2d30bdf --- /dev/null +++ b/pyrogram/client/types/chat_photo.py @@ -0,0 +1,6 @@ +class ChatPhoto: + def __init__(self, + small_file_id: str, + big_file_id: str): + self.small_file_id = small_file_id + self.big_file_id = big_file_id From 140f324cd029f25e164d61c5da6d302a5da8d356 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:47:36 +0100 Subject: [PATCH 098/285] Add StickerSet type --- pyrogram/client/types/sticker_set.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyrogram/client/types/sticker_set.py diff --git a/pyrogram/client/types/sticker_set.py b/pyrogram/client/types/sticker_set.py new file mode 100644 index 00000000..2970a111 --- /dev/null +++ b/pyrogram/client/types/sticker_set.py @@ -0,0 +1,10 @@ +class StickerSet: + def __init__(self, + name: str, + title: str, + contain_masks: bool, + stickers: list): + self.name = name + self.title = title + self.contain_masks = contain_masks + self.stickers = stickers From 7ff9f28e153969279ea18affc3af1d9664af5129 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:47:45 +0100 Subject: [PATCH 099/285] Add Video type --- pyrogram/client/types/video.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 pyrogram/client/types/video.py diff --git a/pyrogram/client/types/video.py b/pyrogram/client/types/video.py new file mode 100644 index 00000000..37d0a8c8 --- /dev/null +++ b/pyrogram/client/types/video.py @@ -0,0 +1,19 @@ +from . import PhotoSize + + +class Video: + def __init__(self, + file_id: str, + width: int, + height: int, + duration: int, + thumb: PhotoSize = None, + mime_type: str = None, + file_size: int = None): + self.file_id = file_id + self.width = width + self.height = height + self.duration = duration + self.thumb = thumb + self.mime_type = mime_type + self.file_size = file_size From 3d2029e9eaf3afc8b4ca287ccefedcc10a3f9312 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:48:01 +0100 Subject: [PATCH 100/285] Update Audio type --- pyrogram/client/types/audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/types/audio.py b/pyrogram/client/types/audio.py index d2e53f44..10febb4b 100644 --- a/pyrogram/client/types/audio.py +++ b/pyrogram/client/types/audio.py @@ -5,7 +5,7 @@ class Audio: performer: str = None, title: str = None, mime_type: str = None, - file_size: str = None): + file_size: int = None): self.file_id = file_id self.duration = duration self.performer = performer From caf1dea22789271a382c6c33ab52bb17ed70a8f8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:48:13 +0100 Subject: [PATCH 101/285] Update Chat type --- pyrogram/client/types/chat.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/types/chat.py b/pyrogram/client/types/chat.py index fb31bee1..78853bf4 100644 --- a/pyrogram/client/types/chat.py +++ b/pyrogram/client/types/chat.py @@ -1,3 +1,6 @@ +from . import Message, ChatPhoto + + class Chat: def __init__(self, id: int, @@ -7,10 +10,22 @@ class Chat: first_name: str = None, last_name: str = None, all_members_are_administrators: bool = None, - photo=None, + photo: ChatPhoto = None, description: str = None, invite_link: str = None, - pinned_message=None, + pinned_message: Message = None, sticker_set_name: str = None, - can_set_sticker_set=None): - ... + can_set_sticker_set: bool = None): + self.id = id + self.type = type + self.title = title + self.username = username + self.first_name = first_name + self.last_name = last_name + self.all_members_are_administrators = all_members_are_administrators + self.photo = photo + self.description = description + self.invite_link = invite_link + self.pinned_message = pinned_message + self.sticker_set_name = sticker_set_name + self.can_set_sticker_set = can_set_sticker_set From d8c634152a1a538301afec8481a72eda2a5a600a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:59:08 +0100 Subject: [PATCH 102/285] Update Message type --- pyrogram/client/types/message.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyrogram/client/types/message.py b/pyrogram/client/types/message.py index f617201c..02cfe74c 100644 --- a/pyrogram/client/types/message.py +++ b/pyrogram/client/types/message.py @@ -1,13 +1,14 @@ -from . import User, Chat +from . import User, Chat, Audio class Message: def __init__(self, message_id: int, - from_user: User, date: int, chat: Chat, - forward_from: Chat = None, + from_user: User = None, + forward_from: User = None, + forward_from_chat: Chat = None, forward_from_message_id: int = None, forward_signature: str = None, forward_date: int = None, @@ -16,9 +17,9 @@ class Message: media_group_id: str = None, author_signature: str = None, text: str = None, - entities=None, - caption_entities=None, - audio=None, + entities: list = None, + caption_entities: list = None, + audio: Audio = None, document=None, game=None, photo=None, @@ -42,12 +43,14 @@ class Message: migrate_from_chat_id: int = None, pinned_message: "Message" = None, invoice=None, - successful_payment=None): + successful_payment=None, + connected_website=None): self.message_id = message_id - self.from_user = from_user self.date = date self.chat = chat + self.from_user = from_user self.forward_from = forward_from + self.forward_from_chat = forward_from_chat self.forward_from_message_id = forward_from_message_id self.forward_signature = forward_signature self.forward_date = forward_date @@ -83,3 +86,4 @@ class Message: self.pinned_message = pinned_message self.invoice = invoice self.successful_payment = successful_payment + self.connected_website = connected_website From 85f92120d1561376ec7eb8057469f12dbff2db55 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 14:59:30 +0100 Subject: [PATCH 103/285] Add types to __init__.py --- pyrogram/client/types/__init__.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/types/__init__.py b/pyrogram/client/types/__init__.py index e858d4dc..f6617387 100644 --- a/pyrogram/client/types/__init__.py +++ b/pyrogram/client/types/__init__.py @@ -1,2 +1,22 @@ -from .user import User +from .animation import Animation +from .audio import Audio from .chat import Chat +from .chat_member import ChatMember +from .chat_photo import ChatPhoto +from .contact import Contact +from .document import Document +from .game import Game +from .location import Location +from .mask_position import MaskPosition +from .message import Message +from .message_entity import MessageEntity +from .photo_size import PhotoSize +from .sticker import Sticker +from .sticker_set import StickerSet +from .update import Update +from .user import User +from .user_profile_photos import UserProfilePhotos +from .venue import Venue +from .video import Video +from .video_note import VideoNote +from .voice import Voice From f26dc10ee2b390c5e475a16f69b778fe02843435 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 9 Mar 2018 15:43:54 +0100 Subject: [PATCH 104/285] Use namespace instead of importing types --- pyrogram/client/client.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 5a834ee7..e4edd0dd 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -42,12 +42,6 @@ from pyrogram.api.errors import ( PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, VolumeLocNotFound) -from pyrogram.api.types import ( - User, Chat, Channel, - PeerUser, PeerChannel, - InputPeerEmpty, InputPeerSelf, - InputPeerUser, InputPeerChat, InputPeerChannel -) from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId @@ -227,7 +221,7 @@ class Client: def fetch_peers(self, entities: list): for entity in entities: - if isinstance(entity, User): + if isinstance(entity, types.User): user_id = entity.id if user_id in self.peers_by_id: @@ -241,7 +235,7 @@ class Client: username = entity.username phone = entity.phone - input_peer = InputPeerUser( + input_peer = types.InputPeerUser( user_id=user_id, access_hash=access_hash ) @@ -254,20 +248,20 @@ class Client: if phone is not None: self.peers_by_phone[phone] = input_peer - if isinstance(entity, Chat): + if isinstance(entity, types.Chat): chat_id = entity.id peer_id = -chat_id if peer_id in self.peers_by_id: continue - input_peer = InputPeerChat( + input_peer = types.InputPeerChat( chat_id=chat_id ) self.peers_by_id[peer_id] = input_peer - if isinstance(entity, Channel): + if isinstance(entity, types.Channel): channel_id = entity.id peer_id = int("-100" + str(channel_id)) @@ -281,7 +275,7 @@ class Client: username = entity.username - input_peer = InputPeerChannel( + input_peer = types.InputPeerChannel( channel_id=channel_id, access_hash=access_hash ) @@ -791,7 +785,7 @@ class Client: dialogs = self.send( functions.messages.GetDialogs( - 0, 0, InputPeerEmpty(), + 0, 0, types.InputPeerEmpty(), self.DIALOGS_AT_ONCE, True ) ) @@ -824,14 +818,14 @@ class Client: ) ) # type: types.contacts.ResolvedPeer - if type(resolved_peer.peer) is PeerUser: - input_peer = InputPeerUser( + if type(resolved_peer.peer) is types.PeerUser: + input_peer = types.InputPeerUser( user_id=resolved_peer.users[0].id, access_hash=resolved_peer.users[0].access_hash ) peer_id = input_peer.user_id - elif type(resolved_peer.peer) is PeerChannel: - input_peer = InputPeerChannel( + elif type(resolved_peer.peer) is types.PeerChannel: + input_peer = types.InputPeerChannel( channel_id=resolved_peer.chats[0].id, access_hash=resolved_peer.chats[0].access_hash ) @@ -866,7 +860,7 @@ class Client: """ if type(peer_id) is str: if peer_id in ("self", "me"): - return InputPeerSelf() + return types.InputPeerSelf() peer_id = peer_id.lower().strip("@+") @@ -913,7 +907,7 @@ class Client: """ return self.send( functions.users.GetFullUser( - InputPeerSelf() + types.InputPeerSelf() ) ) @@ -2323,7 +2317,7 @@ class Client: ) ) - channel = InputPeerChannel( + channel = types.InputPeerChannel( channel_id=resolved_peer.chats[0].id, access_hash=resolved_peer.chats[0].access_hash ) From e12a81ebb6bf2660a63a5314d31b9dd192a3e899 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 10 Mar 2018 10:50:51 +0100 Subject: [PATCH 105/285] Attempt to fix cyclic imports --- pyrogram/client/types/animation.py | 5 +-- pyrogram/client/types/chat.py | 7 ++--- pyrogram/client/types/chat_member.py | 5 +-- pyrogram/client/types/document.py | 5 +-- pyrogram/client/types/game.py | 5 +-- pyrogram/client/types/message.py | 41 ++++++++++++------------- pyrogram/client/types/message_entity.py | 5 +-- pyrogram/client/types/sticker.py | 7 ++--- pyrogram/client/types/update.py | 11 +++---- pyrogram/client/types/venue.py | 5 +-- pyrogram/client/types/video.py | 5 +-- pyrogram/client/types/video_note.py | 5 +-- 12 files changed, 35 insertions(+), 71 deletions(-) diff --git a/pyrogram/client/types/animation.py b/pyrogram/client/types/animation.py index a3693396..ea04beaf 100644 --- a/pyrogram/client/types/animation.py +++ b/pyrogram/client/types/animation.py @@ -1,10 +1,7 @@ -from . import PhotoSize - - class Animation: def __init__(self, file_id: str, - thumb: PhotoSize = None, + thumb: "PhotoSize" = None, file_name: str = None, mime_type: str = None, file_size: int = None): diff --git a/pyrogram/client/types/chat.py b/pyrogram/client/types/chat.py index 78853bf4..6a09215d 100644 --- a/pyrogram/client/types/chat.py +++ b/pyrogram/client/types/chat.py @@ -1,6 +1,3 @@ -from . import Message, ChatPhoto - - class Chat: def __init__(self, id: int, @@ -10,10 +7,10 @@ class Chat: first_name: str = None, last_name: str = None, all_members_are_administrators: bool = None, - photo: ChatPhoto = None, + photo: "ChatPhoto" = None, description: str = None, invite_link: str = None, - pinned_message: Message = None, + pinned_message: "Message" = None, sticker_set_name: str = None, can_set_sticker_set: bool = None): self.id = id diff --git a/pyrogram/client/types/chat_member.py b/pyrogram/client/types/chat_member.py index e8e4d9be..2497e10b 100644 --- a/pyrogram/client/types/chat_member.py +++ b/pyrogram/client/types/chat_member.py @@ -1,9 +1,6 @@ -from . import User - - class ChatMember: def __init__(self, - user: User, + user: "User", status: str, until_date: int = None, can_be_edited: bool = None, diff --git a/pyrogram/client/types/document.py b/pyrogram/client/types/document.py index cc74c4e7..49a9c79d 100644 --- a/pyrogram/client/types/document.py +++ b/pyrogram/client/types/document.py @@ -1,10 +1,7 @@ -from . import PhotoSize - - class Document: def __init__(self, file_id: str, - thumb: PhotoSize = None, + thumb: "PhotoSize" = None, file_name: str = None, mime_type: str = None, file_size: int = None): diff --git a/pyrogram/client/types/game.py b/pyrogram/client/types/game.py index 387af3f4..905d9ef5 100644 --- a/pyrogram/client/types/game.py +++ b/pyrogram/client/types/game.py @@ -1,6 +1,3 @@ -from . import Animation - - class Game: def __init__(self, title: str, @@ -8,7 +5,7 @@ class Game: photo: list, text: str = None, text_entities: list = None, - animation: Animation = None): + animation: "Animation" = None): self.title = title self.description = description self.photo = photo diff --git a/pyrogram/client/types/message.py b/pyrogram/client/types/message.py index 02cfe74c..0f7a9167 100644 --- a/pyrogram/client/types/message.py +++ b/pyrogram/client/types/message.py @@ -1,14 +1,11 @@ -from . import User, Chat, Audio - - class Message: def __init__(self, message_id: int, date: int, - chat: Chat, - from_user: User = None, - forward_from: User = None, - forward_from_chat: Chat = None, + chat: "Chat", + from_user: "User" = None, + forward_from: "User" = None, + forward_from_chat: "Chat" = None, forward_from_message_id: int = None, forward_signature: str = None, forward_date: int = None, @@ -19,20 +16,20 @@ class Message: text: str = None, entities: list = None, caption_entities: list = None, - audio: Audio = None, - document=None, - game=None, - photo=None, - sticker=None, - video=None, - voice=None, - video_note=None, + audio: "Audio" = None, + document: "Document" = None, + game: "Game" = None, + photo: list = None, + sticker: "Sticker" = None, + video: "Video" = None, + voice: "Voice" = None, + video_note: "VideoNote" = None, caption: str = None, - contact=None, - location=None, - venue=None, + contact: "Contact" = None, + location: "Location" = None, + venue: "Venue" = None, new_chat_members: list = None, - left_chat_member: User = None, + left_chat_member: "User" = None, new_chat_title: str = None, new_chat_photo: list = None, delete_chat_photo: bool = None, @@ -42,9 +39,9 @@ class Message: migrate_to_chat_id: int = None, migrate_from_chat_id: int = None, pinned_message: "Message" = None, - invoice=None, - successful_payment=None, - connected_website=None): + invoice: "Invoice" = None, + successful_payment: "SuccessfulPayment" = None, + connected_website: str = None): self.message_id = message_id self.date = date self.chat = chat diff --git a/pyrogram/client/types/message_entity.py b/pyrogram/client/types/message_entity.py index bdc686c0..97541ef3 100644 --- a/pyrogram/client/types/message_entity.py +++ b/pyrogram/client/types/message_entity.py @@ -1,13 +1,10 @@ -from . import User - - class MessageEntity: def __init__(self, type: str, offset: int, length: int, url: str = None, - user: User = None): + user: "User" = None): self.type = type self.offset = offset self.length = length diff --git a/pyrogram/client/types/sticker.py b/pyrogram/client/types/sticker.py index 1e87fd84..fd3910de 100644 --- a/pyrogram/client/types/sticker.py +++ b/pyrogram/client/types/sticker.py @@ -1,15 +1,12 @@ -from . import PhotoSize, MaskPosition - - class Sticker: def __init__(self, file_id: str, width: int, height: int, - thumb: PhotoSize = None, + thumb: "PhotoSize" = None, emoji: str = None, set_name: str = None, - mask_position: MaskPosition = None, + mask_position: "MaskPosition" = None, file_size: int = None): self.file_id = file_id self.width = width diff --git a/pyrogram/client/types/update.py b/pyrogram/client/types/update.py index f367afc3..cde53690 100644 --- a/pyrogram/client/types/update.py +++ b/pyrogram/client/types/update.py @@ -1,6 +1,3 @@ -from . import Message - - class Update: """This object represents an incoming update. At most one of the optional parameters can be present in any given update. @@ -18,10 +15,10 @@ class Update: """ def __init__(self, - message: Message = None, - edited_message: Message = None, - channel_post: Message = None, - edited_channel_post: Message = None): + message: "Message" = None, + edited_message: "Message" = None, + channel_post: "Message" = None, + edited_channel_post: "Message" = None): self.message = message self.edited_message = edited_message self.channel_post = channel_post diff --git a/pyrogram/client/types/venue.py b/pyrogram/client/types/venue.py index f1c058fd..2b07f132 100644 --- a/pyrogram/client/types/venue.py +++ b/pyrogram/client/types/venue.py @@ -1,9 +1,6 @@ -from . import Location - - class Venue: def __init__(self, - location: Location, + location: "Location", title: str, address: str, foursquare_id: str = None): diff --git a/pyrogram/client/types/video.py b/pyrogram/client/types/video.py index 37d0a8c8..87731593 100644 --- a/pyrogram/client/types/video.py +++ b/pyrogram/client/types/video.py @@ -1,13 +1,10 @@ -from . import PhotoSize - - class Video: def __init__(self, file_id: str, width: int, height: int, duration: int, - thumb: PhotoSize = None, + thumb: "PhotoSize" = None, mime_type: str = None, file_size: int = None): self.file_id = file_id diff --git a/pyrogram/client/types/video_note.py b/pyrogram/client/types/video_note.py index 3969c2d9..5969624d 100644 --- a/pyrogram/client/types/video_note.py +++ b/pyrogram/client/types/video_note.py @@ -1,12 +1,9 @@ -from . import PhotoSize - - class VideoNote: def __init__(self, file_id: str, length: int, duration: int, - thumb: PhotoSize = None, + thumb: "PhotoSize" = None, file_size: int = None): self.file_id = file_id self.length = length From 4f4b89b7c2715f8cfa20a33bf4dc840bdd946183 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 11 Mar 2018 17:24:26 +0100 Subject: [PATCH 106/285] Add get_messages method to docs --- docs/source/pyrogram/Client.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 2b864799..7c31cdf0 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -46,4 +46,5 @@ Client add_contacts delete_contacts get_inline_bot_results - send_inline_bot_result \ No newline at end of file + send_inline_bot_result + get_messages \ No newline at end of file From 4f23f8bae451b9049ac78ba60edf4474ba5d5589 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 14 Mar 2018 10:52:27 +0100 Subject: [PATCH 107/285] Add download_photo method to docs --- docs/source/pyrogram/Client.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 7c31cdf0..97e3c376 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -33,6 +33,7 @@ Client send_chat_action send_sticker download_media + download_photo get_user_profile_photos edit_message_text edit_message_caption From 5d5b22c2d8445c5d105bd38199088c0055510d32 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 14 Mar 2018 11:17:46 +0100 Subject: [PATCH 108/285] Update version in docs --- docs/source/start/QuickInstallation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index cafa7f0d..f99d4d1e 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -40,6 +40,6 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.6.2' + '0.6.3' .. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file From 6eaf7cabb5c662b0a1cae51705d2da19c1bafae1 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 20 Mar 2018 14:25:19 +0100 Subject: [PATCH 109/285] Add theme-color --- docs/source/_templates/layout.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html index 72eaeda4..664d6988 100644 --- a/docs/source/_templates/layout.html +++ b/docs/source/_templates/layout.html @@ -1,5 +1,7 @@ {% extends "!layout.html" %} {% block extrahead %} + + -{% endblock %} \ No newline at end of file + + {% endblock %} + + + + + {% block extrabody %} {% endblock %} +

+ + {# SIDE NAV, TOGGLES ON MOBILE #} + + +
+ + {# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #} + + + +
+ {%- block content %} + {% if theme_style_external_links|tobool %} + + +
+ +
+ {% include "versions.html" %} + + {% if not embedded %} + + + {%- for scriptfile in script_files %} + + {%- endfor %} + + {% endif %} + + {# RTD hosts this file, so just load on non RTD builds #} + {% if not READTHEDOCS %} + + {% endif %} + + + + {%- block footer %} {% endblock %} + + + \ No newline at end of file diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index f99d4d1e..0ba07849 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -40,6 +40,6 @@ If no errors show up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.6.3' + '0.6.4' .. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file From deb5c176129673d851b46ade1a69d58d749ffb6a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 23 Mar 2018 13:48:07 +0100 Subject: [PATCH 115/285] Add custom theme.css --- docs/source/_static/css/theme.css | 4698 +++++++++++++++++++++++++++++ 1 file changed, 4698 insertions(+) create mode 100644 docs/source/_static/css/theme.css diff --git a/docs/source/_static/css/theme.css b/docs/source/_static/css/theme.css new file mode 100644 index 00000000..f15475bb --- /dev/null +++ b/docs/source/_static/css/theme.css @@ -0,0 +1,4698 @@ +*{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{ + display:block +} +audio,canvas,video{ + display:inline-block; + *display:inline; + *zoom:1 +} +audio:not([controls]){ + display:none +} +[hidden]{ + display:none +} +*{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +html{ + font-size:100%; + -webkit-text-size-adjust:100%; + -ms-text-size-adjust:100% +} +body{ + margin:0 +} +a:hover,a:active{ + outline:0 +} +abbr[title]{ + border-bottom:1px dotted +} +b,strong{ + font-weight:bold +} +blockquote{ + margin:0 +} +dfn{ + font-style:italic +} +ins{ + background:#ff9; + color:#000; + text-decoration:none +} +mark{ + background:#ff0; + color:#000; + font-style:italic; + font-weight:bold +} +pre,code,.rst-content tt,.rst-content code,kbd,samp{ + font-family:monospace,serif; + _font-family:"courier new",monospace; + font-size:1em +} +pre{ + white-space:pre +} +q{ + quotes:none +} +q:before,q:after{ + content:""; + content:none +} +small{ + font-size:85% +} +sub,sup{ + font-size:75%; + line-height:0; + position:relative; + vertical-align:baseline +} +sup{ + top:-0.5em +} +sub{ + bottom:-0.25em +} +ul,ol,dl{ + margin:0; + padding:0; + list-style:none; + list-style-image:none +} +li{ + list-style:none +} +dd{ + margin:0 +} +img{ + border:0; + -ms-interpolation-mode:bicubic; + vertical-align:middle; + max-width:100% +} +svg:not(:root){ + overflow:hidden +} +figure{ + margin:0 +} +form{ + margin:0 +} +fieldset{ + border:0; + margin:0; + padding:0 +} +label{ + cursor:pointer +} +legend{ + border:0; + *margin-left:-7px; + padding:0; + white-space:normal +} +button,input,select,textarea{ + font-size:100%; + margin:0; + vertical-align:baseline; + *vertical-align:middle +} +button,input{ + line-height:normal +} +button,input[type="button"],input[type="reset"],input[type="submit"]{ + cursor:pointer; + -webkit-appearance:button; + *overflow:visible +} +button[disabled],input[disabled]{ + cursor:default +} +input[type="checkbox"],input[type="radio"]{ + box-sizing:border-box; + padding:0; + *width:13px; + *height:13px +} +input[type="search"]{ + -webkit-appearance:textfield; + -moz-box-sizing:content-box; + -webkit-box-sizing:content-box; + box-sizing:content-box +} +input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{ + -webkit-appearance:none +} +button::-moz-focus-inner,input::-moz-focus-inner{ + border:0; + padding:0 +} +textarea{ + overflow:auto; + vertical-align:top; + resize:vertical +} +table{ + border-collapse:collapse; + border-spacing:0 +} +td{ + vertical-align:top +} +.chromeframe{ + margin:0.2em 0; + background:#ccc; + color:#000; + padding:0.2em 0 +} +.ir{ + display:block; + border:0; + text-indent:-999em; + overflow:hidden; + background-color:transparent; + background-repeat:no-repeat; + text-align:left; + direction:ltr; + *line-height:0 +} +.ir br{ + display:none +} +.hidden{ + display:none !important; + visibility:hidden +} +.visuallyhidden{ + border:0; + clip:rect(0 0 0 0); + height:1px; + margin:-1px; + overflow:hidden; + padding:0; + position:absolute; + width:1px +} +.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{ + clip:auto; + height:auto; + margin:0; + overflow:visible; + position:static; + width:auto +} +.invisible{ + visibility:hidden +} +.relative{ + position:relative +} +big,small{ + font-size:100% +} +@media print{ + html,body,section{ + background:none !important + } + *{ + box-shadow:none !important; + text-shadow:none !important; + filter:none !important; + -ms-filter:none !important + } + a,a:visited{ + text-decoration:underline + } + .ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{ + content:"" + } + pre,blockquote{ + page-break-inside:avoid + } + thead{ + display:table-header-group + } + tr,img{ + page-break-inside:avoid + } + img{ + max-width:100% !important + } + @page{ + margin:0.5cm + } + p,h2,.rst-content .toctree-wrapper p.caption,h3{ + orphans:3; + widows:3 + } + h2,.rst-content .toctree-wrapper p.caption,h3{ + page-break-after:avoid + } +} +.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{ + -webkit-font-smoothing:antialiased +} +.clearfix{ + *zoom:1 +} +.clearfix:before,.clearfix:after{ + display:table; + content:"" +} +.clearfix:after{ + clear:both +} +/*! * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ +@font-face{ + font-family:'FontAwesome'; + src:url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.eot"); + src:url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.eot") format("embedded-opentype"),url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.woff2") format("woff2"),url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.woff") format("woff"),url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.ttf") format("truetype"),url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.svg") format("svg"); + font-weight:normal; + font-style:normal +} +.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{ + display:inline-block; + font:normal normal normal 14px/1 FontAwesome; + font-size:inherit; + text-rendering:auto; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale +} +.fa-lg{ + font-size:1.33333em; + line-height:.75em; + vertical-align:-15% +} +.fa-2x{ + font-size:2em +} +.fa-3x{ + font-size:3em +} +.fa-4x{ + font-size:4em +} +.fa-5x{ + font-size:5em +} +.fa-fw{ + width:1.28571em; + text-align:center +} +.fa-ul{ + padding-left:0; + margin-left:2.14286em; + list-style-type:none +} +.fa-ul>li{ + position:relative +} +.fa-li{ + position:absolute; + left:-2.14286em; + width:2.14286em; + top:.14286em; + text-align:center +} +.fa-li.fa-lg{ + left:-1.85714em +} +.fa-border{ + padding:.2em .25em .15em; + border:solid 0.08em #eee; + border-radius:.1em +} +.fa-pull-left{ + float:left +} +.fa-pull-right{ + float:right +} +.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{ + margin-right:.3em +} +.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{ + margin-left:.3em +} +.pull-right{ + float:right +} +.pull-left{ + float:left +} +.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{ + margin-right:.3em +} +.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{ + margin-left:.3em +} +.fa-spin{ + -webkit-animation:fa-spin 2s infinite linear; + animation:fa-spin 2s infinite linear +} +.fa-pulse{ + -webkit-animation:fa-spin 1s infinite steps(8); + animation:fa-spin 1s infinite steps(8) +} +@-webkit-keyframes fa-spin{ + 0%{ + -webkit-transform:rotate(0deg); + transform:rotate(0deg) + } + 100%{ + -webkit-transform:rotate(359deg); + transform:rotate(359deg) + } +} +@keyframes fa-spin{ + 0%{ + -webkit-transform:rotate(0deg); + transform:rotate(0deg) + } + 100%{ + -webkit-transform:rotate(359deg); + transform:rotate(359deg) + } +} +.fa-rotate-90{ + -ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform:rotate(90deg); + -ms-transform:rotate(90deg); + transform:rotate(90deg) +} +.fa-rotate-180{ + -ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform:rotate(180deg); + -ms-transform:rotate(180deg); + transform:rotate(180deg) +} +.fa-rotate-270{ + -ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform:rotate(270deg); + -ms-transform:rotate(270deg); + transform:rotate(270deg) +} +.fa-flip-horizontal{ + -ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform:scale(-1, 1); + -ms-transform:scale(-1, 1); + transform:scale(-1, 1) +} +.fa-flip-vertical{ + -ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform:scale(1, -1); + -ms-transform:scale(1, -1); + transform:scale(1, -1) +} +:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{ + filter:none +} +.fa-stack{ + position:relative; + display:inline-block; + width:2em; + height:2em; + line-height:2em; + vertical-align:middle +} +.fa-stack-1x,.fa-stack-2x{ + position:absolute; + left:0; + width:100%; + text-align:center +} +.fa-stack-1x{ + line-height:inherit +} +.fa-stack-2x{ + font-size:2em +} +.fa-inverse{ + color:#fff +} +.fa-glass:before{ + content:"" +} +.fa-music:before{ + content:"" +} +.fa-search:before,.icon-search:before{ + content:"" +} +.fa-envelope-o:before{ + content:"" +} +.fa-heart:before{ + content:"" +} +.fa-star:before{ + content:"" +} +.fa-star-o:before{ + content:"" +} +.fa-user:before{ + content:"" +} +.fa-film:before{ + content:"" +} +.fa-th-large:before{ + content:"" +} +.fa-th:before{ + content:"" +} +.fa-th-list:before{ + content:"" +} +.fa-check:before{ + content:"" +} +.fa-remove:before,.fa-close:before,.fa-times:before{ + content:"" +} +.fa-search-plus:before{ + content:"" +} +.fa-search-minus:before{ + content:"" +} +.fa-power-off:before{ + content:"" +} +.fa-signal:before{ + content:"" +} +.fa-gear:before,.fa-cog:before{ + content:"" +} +.fa-trash-o:before{ + content:"" +} +.fa-home:before,.icon-home:before{ + content:"" +} +.fa-file-o:before{ + content:"" +} +.fa-clock-o:before{ + content:"" +} +.fa-road:before{ + content:"" +} +.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{ + content:"" +} +.fa-arrow-circle-o-down:before{ + content:"" +} +.fa-arrow-circle-o-up:before{ + content:"" +} +.fa-inbox:before{ + content:"" +} +.fa-play-circle-o:before{ + content:"" +} +.fa-rotate-right:before,.fa-repeat:before{ + content:"" +} +.fa-refresh:before{ + content:"" +} +.fa-list-alt:before{ + content:"" +} +.fa-lock:before{ + content:"" +} +.fa-flag:before{ + content:"" +} +.fa-headphones:before{ + content:"" +} +.fa-volume-off:before{ + content:"" +} +.fa-volume-down:before{ + content:"" +} +.fa-volume-up:before{ + content:"" +} +.fa-qrcode:before{ + content:"" +} +.fa-barcode:before{ + content:"" +} +.fa-tag:before{ + content:"" +} +.fa-tags:before{ + content:"" +} +.fa-book:before,.icon-book:before{ + content:"" +} +.fa-bookmark:before{ + content:"" +} +.fa-print:before{ + content:"" +} +.fa-camera:before{ + content:"" +} +.fa-font:before{ + content:"" +} +.fa-bold:before{ + content:"" +} +.fa-italic:before{ + content:"" +} +.fa-text-height:before{ + content:"" +} +.fa-text-width:before{ + content:"" +} +.fa-align-left:before{ + content:"" +} +.fa-align-center:before{ + content:"" +} +.fa-align-right:before{ + content:"" +} +.fa-align-justify:before{ + content:"" +} +.fa-list:before{ + content:"" +} +.fa-dedent:before,.fa-outdent:before{ + content:"" +} +.fa-indent:before{ + content:"" +} +.fa-video-camera:before{ + content:"" +} +.fa-photo:before,.fa-image:before,.fa-picture-o:before{ + content:"" +} +.fa-pencil:before{ + content:"" +} +.fa-map-marker:before{ + content:"" +} +.fa-adjust:before{ + content:"" +} +.fa-tint:before{ + content:"" +} +.fa-edit:before,.fa-pencil-square-o:before{ + content:"" +} +.fa-share-square-o:before{ + content:"" +} +.fa-check-square-o:before{ + content:"" +} +.fa-arrows:before{ + content:"" +} +.fa-step-backward:before{ + content:"" +} +.fa-fast-backward:before{ + content:"" +} +.fa-backward:before{ + content:"" +} +.fa-play:before{ + content:"" +} +.fa-pause:before{ + content:"" +} +.fa-stop:before{ + content:"" +} +.fa-forward:before{ + content:"" +} +.fa-fast-forward:before{ + content:"" +} +.fa-step-forward:before{ + content:"" +} +.fa-eject:before{ + content:"" +} +.fa-chevron-left:before{ + content:"" +} +.fa-chevron-right:before{ + content:"" +} +.fa-plus-circle:before{ + content:"" +} +.fa-minus-circle:before{ + content:"" +} +.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{ + content:"" +} +.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{ + content:"" +} +.fa-question-circle:before{ + content:"" +} +.fa-info-circle:before{ + content:"" +} +.fa-crosshairs:before{ + content:"" +} +.fa-times-circle-o:before{ + content:"" +} +.fa-check-circle-o:before{ + content:"" +} +.fa-ban:before{ + content:"" +} +.fa-arrow-left:before{ + content:"" +} +.fa-arrow-right:before{ + content:"" +} +.fa-arrow-up:before{ + content:"" +} +.fa-arrow-down:before{ + content:"" +} +.fa-mail-forward:before,.fa-share:before{ + content:"" +} +.fa-expand:before{ + content:"" +} +.fa-compress:before{ + content:"" +} +.fa-plus:before{ + content:"" +} +.fa-minus:before{ + content:"" +} +.fa-asterisk:before{ + content:"" +} +.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{ + content:"" +} +.fa-gift:before{ + content:"" +} +.fa-leaf:before{ + content:"" +} +.fa-fire:before,.icon-fire:before{ + content:"" +} +.fa-eye:before{ + content:"" +} +.fa-eye-slash:before{ + content:"" +} +.fa-warning:before,.fa-exclamation-triangle:before{ + content:"" +} +.fa-plane:before{ + content:"" +} +.fa-calendar:before{ + content:"" +} +.fa-random:before{ + content:"" +} +.fa-comment:before{ + content:"" +} +.fa-magnet:before{ + content:"" +} +.fa-chevron-up:before{ + content:"" +} +.fa-chevron-down:before{ + content:"" +} +.fa-retweet:before{ + content:"" +} +.fa-shopping-cart:before{ + content:"" +} +.fa-folder:before{ + content:"" +} +.fa-folder-open:before{ + content:"" +} +.fa-arrows-v:before{ + content:"" +} +.fa-arrows-h:before{ + content:"" +} +.fa-bar-chart-o:before,.fa-bar-chart:before{ + content:"" +} +.fa-twitter-square:before{ + content:"" +} +.fa-facebook-square:before{ + content:"" +} +.fa-camera-retro:before{ + content:"" +} +.fa-key:before{ + content:"" +} +.fa-gears:before,.fa-cogs:before{ + content:"" +} +.fa-comments:before{ + content:"" +} +.fa-thumbs-o-up:before{ + content:"" +} +.fa-thumbs-o-down:before{ + content:"" +} +.fa-star-half:before{ + content:"" +} +.fa-heart-o:before{ + content:"" +} +.fa-sign-out:before{ + content:"" +} +.fa-linkedin-square:before{ + content:"" +} +.fa-thumb-tack:before{ + content:"" +} +.fa-external-link:before{ + content:"" +} +.fa-sign-in:before{ + content:"" +} +.fa-trophy:before{ + content:"" +} +.fa-github-square:before{ + content:"" +} +.fa-upload:before{ + content:"" +} +.fa-lemon-o:before{ + content:"" +} +.fa-phone:before{ + content:"" +} +.fa-square-o:before{ + content:"" +} +.fa-bookmark-o:before{ + content:"" +} +.fa-phone-square:before{ + content:"" +} +.fa-twitter:before{ + content:"" +} +.fa-facebook-f:before,.fa-facebook:before{ + content:"" +} +.fa-github:before,.icon-github:before{ + content:"" +} +.fa-unlock:before{ + content:"" +} +.fa-credit-card:before{ + content:"" +} +.fa-feed:before,.fa-rss:before{ + content:"" +} +.fa-hdd-o:before{ + content:"" +} +.fa-bullhorn:before{ + content:"" +} +.fa-bell:before{ + content:"" +} +.fa-certificate:before{ + content:"" +} +.fa-hand-o-right:before{ + content:"" +} +.fa-hand-o-left:before{ + content:"" +} +.fa-hand-o-up:before{ + content:"" +} +.fa-hand-o-down:before{ + content:"" +} +.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{ + content:"" +} +.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{ + content:"" +} +.fa-arrow-circle-up:before{ + content:"" +} +.fa-arrow-circle-down:before{ + content:"" +} +.fa-globe:before{ + content:"" +} +.fa-wrench:before{ + content:"" +} +.fa-tasks:before{ + content:"" +} +.fa-filter:before{ + content:"" +} +.fa-briefcase:before{ + content:"" +} +.fa-arrows-alt:before{ + content:"" +} +.fa-group:before,.fa-users:before{ + content:"" +} +.fa-chain:before,.fa-link:before,.icon-link:before{ + content:"" +} +.fa-cloud:before{ + content:"" +} +.fa-flask:before{ + content:"" +} +.fa-cut:before,.fa-scissors:before{ + content:"" +} +.fa-copy:before,.fa-files-o:before{ + content:"" +} +.fa-paperclip:before{ + content:"" +} +.fa-save:before,.fa-floppy-o:before{ + content:"" +} +.fa-square:before{ + content:"" +} +.fa-navicon:before,.fa-reorder:before,.fa-bars:before{ + content:"" +} +.fa-list-ul:before{ + content:"" +} +.fa-list-ol:before{ + content:"" +} +.fa-strikethrough:before{ + content:"" +} +.fa-underline:before{ + content:"" +} +.fa-table:before{ + content:"" +} +.fa-magic:before{ + content:"" +} +.fa-truck:before{ + content:"" +} +.fa-pinterest:before{ + content:"" +} +.fa-pinterest-square:before{ + content:"" +} +.fa-google-plus-square:before{ + content:"" +} +.fa-google-plus:before{ + content:"" +} +.fa-money:before{ + content:"" +} +.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{ + content:"" +} +.fa-caret-up:before{ + content:"" +} +.fa-caret-left:before{ + content:"" +} +.fa-caret-right:before{ + content:"" +} +.fa-columns:before{ + content:"" +} +.fa-unsorted:before,.fa-sort:before{ + content:"" +} +.fa-sort-down:before,.fa-sort-desc:before{ + content:"" +} +.fa-sort-up:before,.fa-sort-asc:before{ + content:"" +} +.fa-envelope:before{ + content:"" +} +.fa-linkedin:before{ + content:"" +} +.fa-rotate-left:before,.fa-undo:before{ + content:"" +} +.fa-legal:before,.fa-gavel:before{ + content:"" +} +.fa-dashboard:before,.fa-tachometer:before{ + content:"" +} +.fa-comment-o:before{ + content:"" +} +.fa-comments-o:before{ + content:"" +} +.fa-flash:before,.fa-bolt:before{ + content:"" +} +.fa-sitemap:before{ + content:"" +} +.fa-umbrella:before{ + content:"" +} +.fa-paste:before,.fa-clipboard:before{ + content:"" +} +.fa-lightbulb-o:before{ + content:"" +} +.fa-exchange:before{ + content:"" +} +.fa-cloud-download:before{ + content:"" +} +.fa-cloud-upload:before{ + content:"" +} +.fa-user-md:before{ + content:"" +} +.fa-stethoscope:before{ + content:"" +} +.fa-suitcase:before{ + content:"" +} +.fa-bell-o:before{ + content:"" +} +.fa-coffee:before{ + content:"" +} +.fa-cutlery:before{ + content:"" +} +.fa-file-text-o:before{ + content:"" +} +.fa-building-o:before{ + content:"" +} +.fa-hospital-o:before{ + content:"" +} +.fa-ambulance:before{ + content:"" +} +.fa-medkit:before{ + content:"" +} +.fa-fighter-jet:before{ + content:"" +} +.fa-beer:before{ + content:"" +} +.fa-h-square:before{ + content:"" +} +.fa-plus-square:before{ + content:"" +} +.fa-angle-double-left:before{ + content:"" +} +.fa-angle-double-right:before{ + content:"" +} +.fa-angle-double-up:before{ + content:"" +} +.fa-angle-double-down:before{ + content:"" +} +.fa-angle-left:before{ + content:"" +} +.fa-angle-right:before{ + content:"" +} +.fa-angle-up:before{ + content:"" +} +.fa-angle-down:before{ + content:"" +} +.fa-desktop:before{ + content:"" +} +.fa-laptop:before{ + content:"" +} +.fa-tablet:before{ + content:"" +} +.fa-mobile-phone:before,.fa-mobile:before{ + content:"" +} +.fa-circle-o:before{ + content:"" +} +.fa-quote-left:before{ + content:"" +} +.fa-quote-right:before{ + content:"" +} +.fa-spinner:before{ + content:"" +} +.fa-circle:before{ + content:"" +} +.fa-mail-reply:before,.fa-reply:before{ + content:"" +} +.fa-github-alt:before{ + content:"" +} +.fa-folder-o:before{ + content:"" +} +.fa-folder-open-o:before{ + content:"" +} +.fa-smile-o:before{ + content:"" +} +.fa-frown-o:before{ + content:"" +} +.fa-meh-o:before{ + content:"" +} +.fa-gamepad:before{ + content:"" +} +.fa-keyboard-o:before{ + content:"" +} +.fa-flag-o:before{ + content:"" +} +.fa-flag-checkered:before{ + content:"" +} +.fa-terminal:before{ + content:"" +} +.fa-code:before{ + content:"" +} +.fa-mail-reply-all:before,.fa-reply-all:before{ + content:"" +} +.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{ + content:"" +} +.fa-location-arrow:before{ + content:"" +} +.fa-crop:before{ + content:"" +} +.fa-code-fork:before{ + content:"" +} +.fa-unlink:before,.fa-chain-broken:before{ + content:"" +} +.fa-question:before{ + content:"" +} +.fa-info:before{ + content:"" +} +.fa-exclamation:before{ + content:"" +} +.fa-superscript:before{ + content:"" +} +.fa-subscript:before{ + content:"" +} +.fa-eraser:before{ + content:"" +} +.fa-puzzle-piece:before{ + content:"" +} +.fa-microphone:before{ + content:"" +} +.fa-microphone-slash:before{ + content:"" +} +.fa-shield:before{ + content:"" +} +.fa-calendar-o:before{ + content:"" +} +.fa-fire-extinguisher:before{ + content:"" +} +.fa-rocket:before{ + content:"" +} +.fa-maxcdn:before{ + content:"" +} +.fa-chevron-circle-left:before{ + content:"" +} +.fa-chevron-circle-right:before{ + content:"" +} +.fa-chevron-circle-up:before{ + content:"" +} +.fa-chevron-circle-down:before{ + content:"" +} +.fa-html5:before{ + content:"" +} +.fa-css3:before{ + content:"" +} +.fa-anchor:before{ + content:"" +} +.fa-unlock-alt:before{ + content:"" +} +.fa-bullseye:before{ + content:"" +} +.fa-ellipsis-h:before{ + content:"" +} +.fa-ellipsis-v:before{ + content:"" +} +.fa-rss-square:before{ + content:"" +} +.fa-play-circle:before{ + content:"" +} +.fa-ticket:before{ + content:"" +} +.fa-minus-square:before{ + content:"" +} +.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{ + content:"" +} +.fa-level-up:before{ + content:"" +} +.fa-level-down:before{ + content:"" +} +.fa-check-square:before{ + content:"" +} +.fa-pencil-square:before{ + content:"" +} +.fa-external-link-square:before{ + content:"" +} +.fa-share-square:before{ + content:"" +} +.fa-compass:before{ + content:"" +} +.fa-toggle-down:before,.fa-caret-square-o-down:before{ + content:"" +} +.fa-toggle-up:before,.fa-caret-square-o-up:before{ + content:"" +} +.fa-toggle-right:before,.fa-caret-square-o-right:before{ + content:"" +} +.fa-euro:before,.fa-eur:before{ + content:"" +} +.fa-gbp:before{ + content:"" +} +.fa-dollar:before,.fa-usd:before{ + content:"" +} +.fa-rupee:before,.fa-inr:before{ + content:"" +} +.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{ + content:"" +} +.fa-ruble:before,.fa-rouble:before,.fa-rub:before{ + content:"" +} +.fa-won:before,.fa-krw:before{ + content:"" +} +.fa-bitcoin:before,.fa-btc:before{ + content:"" +} +.fa-file:before{ + content:"" +} +.fa-file-text:before{ + content:"" +} +.fa-sort-alpha-asc:before{ + content:"" +} +.fa-sort-alpha-desc:before{ + content:"" +} +.fa-sort-amount-asc:before{ + content:"" +} +.fa-sort-amount-desc:before{ + content:"" +} +.fa-sort-numeric-asc:before{ + content:"" +} +.fa-sort-numeric-desc:before{ + content:"" +} +.fa-thumbs-up:before{ + content:"" +} +.fa-thumbs-down:before{ + content:"" +} +.fa-youtube-square:before{ + content:"" +} +.fa-youtube:before{ + content:"" +} +.fa-xing:before{ + content:"" +} +.fa-xing-square:before{ + content:"" +} +.fa-youtube-play:before{ + content:"" +} +.fa-dropbox:before{ + content:"" +} +.fa-stack-overflow:before{ + content:"" +} +.fa-instagram:before{ + content:"" +} +.fa-flickr:before{ + content:"" +} +.fa-adn:before{ + content:"" +} +.fa-bitbucket:before,.icon-bitbucket:before{ + content:"" +} +.fa-bitbucket-square:before{ + content:"" +} +.fa-tumblr:before{ + content:"" +} +.fa-tumblr-square:before{ + content:"" +} +.fa-long-arrow-down:before{ + content:"" +} +.fa-long-arrow-up:before{ + content:"" +} +.fa-long-arrow-left:before{ + content:"" +} +.fa-long-arrow-right:before{ + content:"" +} +.fa-apple:before{ + content:"" +} +.fa-windows:before{ + content:"" +} +.fa-android:before{ + content:"" +} +.fa-linux:before{ + content:"" +} +.fa-dribbble:before{ + content:"" +} +.fa-skype:before{ + content:"" +} +.fa-foursquare:before{ + content:"" +} +.fa-trello:before{ + content:"" +} +.fa-female:before{ + content:"" +} +.fa-male:before{ + content:"" +} +.fa-gittip:before,.fa-gratipay:before{ + content:"" +} +.fa-sun-o:before{ + content:"" +} +.fa-moon-o:before{ + content:"" +} +.fa-archive:before{ + content:"" +} +.fa-bug:before{ + content:"" +} +.fa-vk:before{ + content:"" +} +.fa-weibo:before{ + content:"" +} +.fa-renren:before{ + content:"" +} +.fa-pagelines:before{ + content:"" +} +.fa-stack-exchange:before{ + content:"" +} +.fa-arrow-circle-o-right:before{ + content:"" +} +.fa-arrow-circle-o-left:before{ + content:"" +} +.fa-toggle-left:before,.fa-caret-square-o-left:before{ + content:"" +} +.fa-dot-circle-o:before{ + content:"" +} +.fa-wheelchair:before{ + content:"" +} +.fa-vimeo-square:before{ + content:"" +} +.fa-turkish-lira:before,.fa-try:before{ + content:"" +} +.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{ + content:"" +} +.fa-space-shuttle:before{ + content:"" +} +.fa-slack:before{ + content:"" +} +.fa-envelope-square:before{ + content:"" +} +.fa-wordpress:before{ + content:"" +} +.fa-openid:before{ + content:"" +} +.fa-institution:before,.fa-bank:before,.fa-university:before{ + content:"" +} +.fa-mortar-board:before,.fa-graduation-cap:before{ + content:"" +} +.fa-yahoo:before{ + content:"" +} +.fa-google:before{ + content:"" +} +.fa-reddit:before{ + content:"" +} +.fa-reddit-square:before{ + content:"" +} +.fa-stumbleupon-circle:before{ + content:"" +} +.fa-stumbleupon:before{ + content:"" +} +.fa-delicious:before{ + content:"" +} +.fa-digg:before{ + content:"" +} +.fa-pied-piper-pp:before{ + content:"" +} +.fa-pied-piper-alt:before{ + content:"" +} +.fa-drupal:before{ + content:"" +} +.fa-joomla:before{ + content:"" +} +.fa-language:before{ + content:"" +} +.fa-fax:before{ + content:"" +} +.fa-building:before{ + content:"" +} +.fa-child:before{ + content:"" +} +.fa-paw:before{ + content:"" +} +.fa-spoon:before{ + content:"" +} +.fa-cube:before{ + content:"" +} +.fa-cubes:before{ + content:"" +} +.fa-behance:before{ + content:"" +} +.fa-behance-square:before{ + content:"" +} +.fa-steam:before{ + content:"" +} +.fa-steam-square:before{ + content:"" +} +.fa-recycle:before{ + content:"" +} +.fa-automobile:before,.fa-car:before{ + content:"" +} +.fa-cab:before,.fa-taxi:before{ + content:"" +} +.fa-tree:before{ + content:"" +} +.fa-spotify:before{ + content:"" +} +.fa-deviantart:before{ + content:"" +} +.fa-soundcloud:before{ + content:"" +} +.fa-database:before{ + content:"" +} +.fa-file-pdf-o:before{ + content:"" +} +.fa-file-word-o:before{ + content:"" +} +.fa-file-excel-o:before{ + content:"" +} +.fa-file-powerpoint-o:before{ + content:"" +} +.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{ + content:"" +} +.fa-file-zip-o:before,.fa-file-archive-o:before{ + content:"" +} +.fa-file-sound-o:before,.fa-file-audio-o:before{ + content:"" +} +.fa-file-movie-o:before,.fa-file-video-o:before{ + content:"" +} +.fa-file-code-o:before{ + content:"" +} +.fa-vine:before{ + content:"" +} +.fa-codepen:before{ + content:"" +} +.fa-jsfiddle:before{ + content:"" +} +.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{ + content:"" +} +.fa-circle-o-notch:before{ + content:"" +} +.fa-ra:before,.fa-resistance:before,.fa-rebel:before{ + content:"" +} +.fa-ge:before,.fa-empire:before{ + content:"" +} +.fa-git-square:before{ + content:"" +} +.fa-git:before{ + content:"" +} +.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{ + content:"" +} +.fa-tencent-weibo:before{ + content:"" +} +.fa-qq:before{ + content:"" +} +.fa-wechat:before,.fa-weixin:before{ + content:"" +} +.fa-send:before,.fa-paper-plane:before{ + content:"" +} +.fa-send-o:before,.fa-paper-plane-o:before{ + content:"" +} +.fa-history:before{ + content:"" +} +.fa-circle-thin:before{ + content:"" +} +.fa-header:before{ + content:"" +} +.fa-paragraph:before{ + content:"" +} +.fa-sliders:before{ + content:"" +} +.fa-share-alt:before{ + content:"" +} +.fa-share-alt-square:before{ + content:"" +} +.fa-bomb:before{ + content:"" +} +.fa-soccer-ball-o:before,.fa-futbol-o:before{ + content:"" +} +.fa-tty:before{ + content:"" +} +.fa-binoculars:before{ + content:"" +} +.fa-plug:before{ + content:"" +} +.fa-slideshare:before{ + content:"" +} +.fa-twitch:before{ + content:"" +} +.fa-yelp:before{ + content:"" +} +.fa-newspaper-o:before{ + content:"" +} +.fa-wifi:before{ + content:"" +} +.fa-calculator:before{ + content:"" +} +.fa-paypal:before{ + content:"" +} +.fa-google-wallet:before{ + content:"" +} +.fa-cc-visa:before{ + content:"" +} +.fa-cc-mastercard:before{ + content:"" +} +.fa-cc-discover:before{ + content:"" +} +.fa-cc-amex:before{ + content:"" +} +.fa-cc-paypal:before{ + content:"" +} +.fa-cc-stripe:before{ + content:"" +} +.fa-bell-slash:before{ + content:"" +} +.fa-bell-slash-o:before{ + content:"" +} +.fa-trash:before{ + content:"" +} +.fa-copyright:before{ + content:"" +} +.fa-at:before{ + content:"" +} +.fa-eyedropper:before{ + content:"" +} +.fa-paint-brush:before{ + content:"" +} +.fa-birthday-cake:before{ + content:"" +} +.fa-area-chart:before{ + content:"" +} +.fa-pie-chart:before{ + content:"" +} +.fa-line-chart:before{ + content:"" +} +.fa-lastfm:before{ + content:"" +} +.fa-lastfm-square:before{ + content:"" +} +.fa-toggle-off:before{ + content:"" +} +.fa-toggle-on:before{ + content:"" +} +.fa-bicycle:before{ + content:"" +} +.fa-bus:before{ + content:"" +} +.fa-ioxhost:before{ + content:"" +} +.fa-angellist:before{ + content:"" +} +.fa-cc:before{ + content:"" +} +.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{ + content:"" +} +.fa-meanpath:before{ + content:"" +} +.fa-buysellads:before{ + content:"" +} +.fa-connectdevelop:before{ + content:"" +} +.fa-dashcube:before{ + content:"" +} +.fa-forumbee:before{ + content:"" +} +.fa-leanpub:before{ + content:"" +} +.fa-sellsy:before{ + content:"" +} +.fa-shirtsinbulk:before{ + content:"" +} +.fa-simplybuilt:before{ + content:"" +} +.fa-skyatlas:before{ + content:"" +} +.fa-cart-plus:before{ + content:"" +} +.fa-cart-arrow-down:before{ + content:"" +} +.fa-diamond:before{ + content:"" +} +.fa-ship:before{ + content:"" +} +.fa-user-secret:before{ + content:"" +} +.fa-motorcycle:before{ + content:"" +} +.fa-street-view:before{ + content:"" +} +.fa-heartbeat:before{ + content:"" +} +.fa-venus:before{ + content:"" +} +.fa-mars:before{ + content:"" +} +.fa-mercury:before{ + content:"" +} +.fa-intersex:before,.fa-transgender:before{ + content:"" +} +.fa-transgender-alt:before{ + content:"" +} +.fa-venus-double:before{ + content:"" +} +.fa-mars-double:before{ + content:"" +} +.fa-venus-mars:before{ + content:"" +} +.fa-mars-stroke:before{ + content:"" +} +.fa-mars-stroke-v:before{ + content:"" +} +.fa-mars-stroke-h:before{ + content:"" +} +.fa-neuter:before{ + content:"" +} +.fa-genderless:before{ + content:"" +} +.fa-facebook-official:before{ + content:"" +} +.fa-pinterest-p:before{ + content:"" +} +.fa-whatsapp:before{ + content:"" +} +.fa-server:before{ + content:"" +} +.fa-user-plus:before{ + content:"" +} +.fa-user-times:before{ + content:"" +} +.fa-hotel:before,.fa-bed:before{ + content:"" +} +.fa-viacoin:before{ + content:"" +} +.fa-train:before{ + content:"" +} +.fa-subway:before{ + content:"" +} +.fa-medium:before{ + content:"" +} +.fa-yc:before,.fa-y-combinator:before{ + content:"" +} +.fa-optin-monster:before{ + content:"" +} +.fa-opencart:before{ + content:"" +} +.fa-expeditedssl:before{ + content:"" +} +.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{ + content:"" +} +.fa-battery-3:before,.fa-battery-three-quarters:before{ + content:"" +} +.fa-battery-2:before,.fa-battery-half:before{ + content:"" +} +.fa-battery-1:before,.fa-battery-quarter:before{ + content:"" +} +.fa-battery-0:before,.fa-battery-empty:before{ + content:"" +} +.fa-mouse-pointer:before{ + content:"" +} +.fa-i-cursor:before{ + content:"" +} +.fa-object-group:before{ + content:"" +} +.fa-object-ungroup:before{ + content:"" +} +.fa-sticky-note:before{ + content:"" +} +.fa-sticky-note-o:before{ + content:"" +} +.fa-cc-jcb:before{ + content:"" +} +.fa-cc-diners-club:before{ + content:"" +} +.fa-clone:before{ + content:"" +} +.fa-balance-scale:before{ + content:"" +} +.fa-hourglass-o:before{ + content:"" +} +.fa-hourglass-1:before,.fa-hourglass-start:before{ + content:"" +} +.fa-hourglass-2:before,.fa-hourglass-half:before{ + content:"" +} +.fa-hourglass-3:before,.fa-hourglass-end:before{ + content:"" +} +.fa-hourglass:before{ + content:"" +} +.fa-hand-grab-o:before,.fa-hand-rock-o:before{ + content:"" +} +.fa-hand-stop-o:before,.fa-hand-paper-o:before{ + content:"" +} +.fa-hand-scissors-o:before{ + content:"" +} +.fa-hand-lizard-o:before{ + content:"" +} +.fa-hand-spock-o:before{ + content:"" +} +.fa-hand-pointer-o:before{ + content:"" +} +.fa-hand-peace-o:before{ + content:"" +} +.fa-trademark:before{ + content:"" +} +.fa-registered:before{ + content:"" +} +.fa-creative-commons:before{ + content:"" +} +.fa-gg:before{ + content:"" +} +.fa-gg-circle:before{ + content:"" +} +.fa-tripadvisor:before{ + content:"" +} +.fa-odnoklassniki:before{ + content:"" +} +.fa-odnoklassniki-square:before{ + content:"" +} +.fa-get-pocket:before{ + content:"" +} +.fa-wikipedia-w:before{ + content:"" +} +.fa-safari:before{ + content:"" +} +.fa-chrome:before{ + content:"" +} +.fa-firefox:before{ + content:"" +} +.fa-opera:before{ + content:"" +} +.fa-internet-explorer:before{ + content:"" +} +.fa-tv:before,.fa-television:before{ + content:"" +} +.fa-contao:before{ + content:"" +} +.fa-500px:before{ + content:"" +} +.fa-amazon:before{ + content:"" +} +.fa-calendar-plus-o:before{ + content:"" +} +.fa-calendar-minus-o:before{ + content:"" +} +.fa-calendar-times-o:before{ + content:"" +} +.fa-calendar-check-o:before{ + content:"" +} +.fa-industry:before{ + content:"" +} +.fa-map-pin:before{ + content:"" +} +.fa-map-signs:before{ + content:"" +} +.fa-map-o:before{ + content:"" +} +.fa-map:before{ + content:"" +} +.fa-commenting:before{ + content:"" +} +.fa-commenting-o:before{ + content:"" +} +.fa-houzz:before{ + content:"" +} +.fa-vimeo:before{ + content:"" +} +.fa-black-tie:before{ + content:"" +} +.fa-fonticons:before{ + content:"" +} +.fa-reddit-alien:before{ + content:"" +} +.fa-edge:before{ + content:"" +} +.fa-credit-card-alt:before{ + content:"" +} +.fa-codiepie:before{ + content:"" +} +.fa-modx:before{ + content:"" +} +.fa-fort-awesome:before{ + content:"" +} +.fa-usb:before{ + content:"" +} +.fa-product-hunt:before{ + content:"" +} +.fa-mixcloud:before{ + content:"" +} +.fa-scribd:before{ + content:"" +} +.fa-pause-circle:before{ + content:"" +} +.fa-pause-circle-o:before{ + content:"" +} +.fa-stop-circle:before{ + content:"" +} +.fa-stop-circle-o:before{ + content:"" +} +.fa-shopping-bag:before{ + content:"" +} +.fa-shopping-basket:before{ + content:"" +} +.fa-hashtag:before{ + content:"" +} +.fa-bluetooth:before{ + content:"" +} +.fa-bluetooth-b:before{ + content:"" +} +.fa-percent:before{ + content:"" +} +.fa-gitlab:before,.icon-gitlab:before{ + content:"" +} +.fa-wpbeginner:before{ + content:"" +} +.fa-wpforms:before{ + content:"" +} +.fa-envira:before{ + content:"" +} +.fa-universal-access:before{ + content:"" +} +.fa-wheelchair-alt:before{ + content:"" +} +.fa-question-circle-o:before{ + content:"" +} +.fa-blind:before{ + content:"" +} +.fa-audio-description:before{ + content:"" +} +.fa-volume-control-phone:before{ + content:"" +} +.fa-braille:before{ + content:"" +} +.fa-assistive-listening-systems:before{ + content:"" +} +.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{ + content:"" +} +.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{ + content:"" +} +.fa-glide:before{ + content:"" +} +.fa-glide-g:before{ + content:"" +} +.fa-signing:before,.fa-sign-language:before{ + content:"" +} +.fa-low-vision:before{ + content:"" +} +.fa-viadeo:before{ + content:"" +} +.fa-viadeo-square:before{ + content:"" +} +.fa-snapchat:before{ + content:"" +} +.fa-snapchat-ghost:before{ + content:"" +} +.fa-snapchat-square:before{ + content:"" +} +.fa-pied-piper:before{ + content:"" +} +.fa-first-order:before{ + content:"" +} +.fa-yoast:before{ + content:"" +} +.fa-themeisle:before{ + content:"" +} +.fa-google-plus-circle:before,.fa-google-plus-official:before{ + content:"" +} +.fa-fa:before,.fa-font-awesome:before{ + content:"" +} +.fa-handshake-o:before{ + content:"" +} +.fa-envelope-open:before{ + content:"" +} +.fa-envelope-open-o:before{ + content:"" +} +.fa-linode:before{ + content:"" +} +.fa-address-book:before{ + content:"" +} +.fa-address-book-o:before{ + content:"" +} +.fa-vcard:before,.fa-address-card:before{ + content:"" +} +.fa-vcard-o:before,.fa-address-card-o:before{ + content:"" +} +.fa-user-circle:before{ + content:"" +} +.fa-user-circle-o:before{ + content:"" +} +.fa-user-o:before{ + content:"" +} +.fa-id-badge:before{ + content:"" +} +.fa-drivers-license:before,.fa-id-card:before{ + content:"" +} +.fa-drivers-license-o:before,.fa-id-card-o:before{ + content:"" +} +.fa-quora:before{ + content:"" +} +.fa-free-code-camp:before{ + content:"" +} +.fa-telegram:before{ + content:"" +} +.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{ + content:"" +} +.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{ + content:"" +} +.fa-thermometer-2:before,.fa-thermometer-half:before{ + content:"" +} +.fa-thermometer-1:before,.fa-thermometer-quarter:before{ + content:"" +} +.fa-thermometer-0:before,.fa-thermometer-empty:before{ + content:"" +} +.fa-shower:before{ + content:"" +} +.fa-bathtub:before,.fa-s15:before,.fa-bath:before{ + content:"" +} +.fa-podcast:before{ + content:"" +} +.fa-window-maximize:before{ + content:"" +} +.fa-window-minimize:before{ + content:"" +} +.fa-window-restore:before{ + content:"" +} +.fa-times-rectangle:before,.fa-window-close:before{ + content:"" +} +.fa-times-rectangle-o:before,.fa-window-close-o:before{ + content:"" +} +.fa-bandcamp:before{ + content:"" +} +.fa-grav:before{ + content:"" +} +.fa-etsy:before{ + content:"" +} +.fa-imdb:before{ + content:"" +} +.fa-ravelry:before{ + content:"" +} +.fa-eercast:before{ + content:"" +} +.fa-microchip:before{ + content:"" +} +.fa-snowflake-o:before{ + content:"" +} +.fa-superpowers:before{ + content:"" +} +.fa-wpexplorer:before{ + content:"" +} +.fa-meetup:before{ + content:"" +} +.sr-only{ + position:absolute; + width:1px; + height:1px; + padding:0; + margin:-1px; + overflow:hidden; + clip:rect(0, 0, 0, 0); + border:0 +} +.sr-only-focusable:active,.sr-only-focusable:focus{ + position:static; + width:auto; + height:auto; + margin:0; + overflow:visible; + clip:auto +} +.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{ + font-family:inherit +} +.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{ + font-family:"FontAwesome"; + display:inline-block; + font-style:normal; + font-weight:normal; + line-height:1; + text-decoration:inherit +} +a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content table>caption .headerlink,.rst-content table>caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{ + display:inline-block; + text-decoration:inherit +} +.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content table>caption .headerlink,.rst-content table>caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content table>caption .headerlink,.rst-content table>caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{ + display:inline +} +.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{ + line-height:0.9em +} +.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{ + display:inline-block +} +.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{ + opacity:0.5; + -webkit-transition:opacity 0.05s ease-in; + -moz-transition:opacity 0.05s ease-in; + transition:opacity 0.05s ease-in +} +.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{ + opacity:1 +} +.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{ + font-size:14px; + vertical-align:-15% +} +.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition{ + padding:12px; + line-height:24px; + margin-bottom:24px; + background:#e7f2fa +} +.wy-alert-title,.rst-content .admonition-title{ + color:#fff; + font-weight:bold; + display:block; + color:#fff; + background:#6ab0de; + margin:-12px; + padding:6px 12px; + margin-bottom:12px +} +.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.admonition{ + background:#fdf3f2 +} +.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition .admonition-title{ + background:#f29f97 +} +.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo,.rst-content .wy-alert-warning.admonition{ + background:#ffedcc +} +.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title,.rst-content .wy-alert-warning.admonition .admonition-title{ + background:#f0b37e +} +.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.admonition{ + background:#e7f2fa +} +.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition .admonition-title{ + background:#6ab0de +} +.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.admonition{ + background:#dbfaf4 +} +.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition .admonition-title{ + background:#1abc9c +} +.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.admonition{ + background:#f3f6f6 +} +.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition .admonition-title{ + color:#404040; + background:#e1e4e5 +} +.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a{ + color:#2980B9 +} +.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child{ + margin-bottom:0 +} +.wy-tray-container{ + position:fixed; + bottom:0px; + left:0; + z-index:600 +} +.wy-tray-container li{ + display:block; + width:300px; + background:transparent; + color:#fff; + text-align:center; + box-shadow:0 5px 5px 0 rgba(0,0,0,0.1); + padding:0 24px; + min-width:20%; + opacity:0; + height:0; + line-height:56px; + overflow:hidden; + -webkit-transition:all 0.3s ease-in; + -moz-transition:all 0.3s ease-in; + transition:all 0.3s ease-in +} +.wy-tray-container li.wy-tray-item-success{ + background:#27AE60 +} +.wy-tray-container li.wy-tray-item-info{ + background:#2980B9 +} +.wy-tray-container li.wy-tray-item-warning{ + background:#E67E22 +} +.wy-tray-container li.wy-tray-item-danger{ + background:#E74C3C +} +.wy-tray-container li.on{ + opacity:1; + height:56px +} +@media screen and (max-width: 768px){ + .wy-tray-container{ + bottom:auto; + top:0; + width:100% + } + .wy-tray-container li{ + width:100% + } +} +button{ + font-size:100%; + margin:0; + vertical-align:baseline; + *vertical-align:middle; + cursor:pointer; + line-height:normal; + -webkit-appearance:button; + *overflow:visible +} +button::-moz-focus-inner,input::-moz-focus-inner{ + border:0; + padding:0 +} +button[disabled]{ + cursor:default +} +.btn{ + display:inline-block; + border-radius:2px; + line-height:normal; + white-space:nowrap; + text-align:center; + cursor:pointer; + font-size:100%; + padding:6px 12px 8px 12px; + color:#fff; + border:1px solid rgba(0,0,0,0.1); + background-color:#27AE60; + text-decoration:none; + font-weight:normal; + font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset; + outline-none:false; + vertical-align:middle; + *display:inline; + zoom:1; + -webkit-user-drag:none; + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + -webkit-transition:all 0.1s linear; + -moz-transition:all 0.1s linear; + transition:all 0.1s linear +} +.btn-hover{ + background:#2e8ece; + color:#fff +} +.btn:hover{ + background:#2cc36b; + color:#fff +} +.btn:focus{ + background:#2cc36b; + outline:0 +} +.btn:active{ + box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset; + padding:8px 12px 6px 12px +} +.btn:visited{ + color:#fff +} +.btn:disabled{ + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled = false); + filter:alpha(opacity=40); + opacity:0.4; + cursor:not-allowed; + box-shadow:none +} +.btn-disabled{ + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled = false); + filter:alpha(opacity=40); + opacity:0.4; + cursor:not-allowed; + box-shadow:none +} +.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{ + background-image:none; + filter:progid:DXImageTransform.Microsoft.gradient(enabled = false); + filter:alpha(opacity=40); + opacity:0.4; + cursor:not-allowed; + box-shadow:none +} +.btn::-moz-focus-inner{ + padding:0; + border:0 +} +.btn-small{ + font-size:80% +} +.btn-info{ + background-color:#2980B9 !important +} +.btn-info:hover{ + background-color:#2e8ece !important +} +.btn-neutral{ + background-color:#f3f6f6 !important; + color:#404040 !important +} +.btn-neutral:hover{ + background-color:#e5ebeb !important; + color:#404040 +} +.btn-neutral:visited{ + color:#404040 !important +} +.btn-success{ + background-color:#27AE60 !important +} +.btn-success:hover{ + background-color:#295 !important +} +.btn-danger{ + background-color:#E74C3C !important +} +.btn-danger:hover{ + background-color:#ea6153 !important +} +.btn-warning{ + background-color:#E67E22 !important +} +.btn-warning:hover{ + background-color:#e98b39 !important +} +.btn-invert{ + background-color:#222 +} +.btn-invert:hover{ + background-color:#2f2f2f !important +} +.btn-link{ + background-color:transparent !important; + color:#2980B9; + box-shadow:none; + border-color:transparent !important +} +.btn-link:hover{ + background-color:transparent !important; + color:#409ad5 !important; + box-shadow:none +} +.btn-link:active{ + background-color:transparent !important; + color:#409ad5 !important; + box-shadow:none +} +.btn-link:visited{ + color:#9B59B6 +} +.wy-btn-group .btn,.wy-control .btn{ + vertical-align:middle +} +.wy-btn-group{ + margin-bottom:24px; + *zoom:1 +} +.wy-btn-group:before,.wy-btn-group:after{ + display:table; + content:"" +} +.wy-btn-group:after{ + clear:both +} +.wy-dropdown{ + position:relative; + display:inline-block +} +.wy-dropdown-active .wy-dropdown-menu{ + display:block +} +.wy-dropdown-menu{ + position:absolute; + left:0; + display:none; + float:left; + top:100%; + min-width:100%; + background:#fcfcfc; + z-index:100; + border:solid 1px #cfd7dd; + box-shadow:0 2px 2px 0 rgba(0,0,0,0.1); + padding:12px +} +.wy-dropdown-menu>dd>a{ + display:block; + clear:both; + color:#404040; + white-space:nowrap; + font-size:90%; + padding:0 12px; + cursor:pointer +} +.wy-dropdown-menu>dd>a:hover{ + background:#2980B9; + color:#fff +} +.wy-dropdown-menu>dd.divider{ + border-top:solid 1px #cfd7dd; + margin:6px 0 +} +.wy-dropdown-menu>dd.search{ + padding-bottom:12px +} +.wy-dropdown-menu>dd.search input[type="search"]{ + width:100% +} +.wy-dropdown-menu>dd.call-to-action{ + background:#e3e3e3; + text-transform:uppercase; + font-weight:500; + font-size:80% +} +.wy-dropdown-menu>dd.call-to-action:hover{ + background:#e3e3e3 +} +.wy-dropdown-menu>dd.call-to-action .btn{ + color:#fff +} +.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{ + bottom:100%; + top:auto; + left:auto; + right:0 +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{ + background:#fcfcfc; + margin-top:2px +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{ + padding:6px 12px +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{ + background:#2980B9; + color:#fff +} +.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{ + right:0; + left:auto; + text-align:right +} +.wy-dropdown-arrow:before{ + content:" "; + border-bottom:5px solid #f5f5f5; + border-left:5px solid transparent; + border-right:5px solid transparent; + position:absolute; + display:block; + top:-4px; + left:50%; + margin-left:-3px +} +.wy-dropdown-arrow.wy-dropdown-arrow-left:before{ + left:11px +} +.wy-form-stacked select{ + display:block +} +.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{ + display:inline-block; + *display:inline; + *zoom:1; + vertical-align:middle +} +.wy-form-aligned .wy-control-group>label{ + display:inline-block; + vertical-align:middle; + width:10em; + margin:6px 12px 0 0; + float:left +} +.wy-form-aligned .wy-control{ + float:left +} +.wy-form-aligned .wy-control label{ + display:block +} +.wy-form-aligned .wy-control select{ + margin-top:6px +} +fieldset{ + border:0; + margin:0; + padding:0 +} +legend{ + display:block; + width:100%; + border:0; + padding:0; + white-space:normal; + margin-bottom:24px; + font-size:150%; + *margin-left:-7px +} +label{ + display:block; + margin:0 0 .3125em 0; + color:#333; + font-size:90% +} +input,select,textarea{ + font-size:100%; + margin:0; + vertical-align:baseline; + *vertical-align:middle +} +.wy-control-group{ + margin-bottom:24px; + *zoom:1; + max-width:68em; + margin-left:auto; + margin-right:auto; + *zoom:1 +} +.wy-control-group:before,.wy-control-group:after{ + display:table; + content:"" +} +.wy-control-group:after{ + clear:both +} +.wy-control-group:before,.wy-control-group:after{ + display:table; + content:"" +} +.wy-control-group:after{ + clear:both +} +.wy-control-group.wy-control-group-required>label:after{ + content:" *"; + color:#E74C3C +} +.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{ + padding-bottom:12px +} +.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{ + width:100% +} +.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{ + width:100% +} +.wy-control-group .wy-form-full{ + float:left; + display:block; + margin-right:2.35765%; + width:100%; + margin-right:0 +} +.wy-control-group .wy-form-full:last-child{ + margin-right:0 +} +.wy-control-group .wy-form-halves{ + float:left; + display:block; + margin-right:2.35765%; + width:48.82117% +} +.wy-control-group .wy-form-halves:last-child{ + margin-right:0 +} +.wy-control-group .wy-form-halves:nth-of-type(2n){ + margin-right:0 +} +.wy-control-group .wy-form-halves:nth-of-type(2n+1){ + clear:left +} +.wy-control-group .wy-form-thirds{ + float:left; + display:block; + margin-right:2.35765%; + width:31.76157% +} +.wy-control-group .wy-form-thirds:last-child{ + margin-right:0 +} +.wy-control-group .wy-form-thirds:nth-of-type(3n){ + margin-right:0 +} +.wy-control-group .wy-form-thirds:nth-of-type(3n+1){ + clear:left +} +.wy-control-group.wy-control-group-no-input .wy-control{ + margin:6px 0 0 0; + font-size:90% +} +.wy-control-no-input{ + display:inline-block; + margin:6px 0 0 0; + font-size:90% +} +.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{ + width:100% +} +.wy-form-message-inline{ + display:inline-block; + padding-left:0.3em; + color:#666; + vertical-align:middle; + font-size:90% +} +.wy-form-message{ + display:block; + color:#999; + font-size:70%; + margin-top:.3125em; + font-style:italic +} +.wy-form-message p{ + font-size:inherit; + font-style:italic; + margin-bottom:6px +} +.wy-form-message p:last-child{ + margin-bottom:0 +} +input{ + line-height:normal +} +input[type="button"],input[type="reset"],input[type="submit"]{ + -webkit-appearance:button; + cursor:pointer; + font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + *overflow:visible +} +input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{ + -webkit-appearance:none; + padding:6px; + display:inline-block; + border:1px solid #ccc; + font-size:80%; + font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + box-shadow:inset 0 1px 3px #ddd; + border-radius:0; + -webkit-transition:border 0.3s linear; + -moz-transition:border 0.3s linear; + transition:border 0.3s linear +} +input[type="datetime-local"]{ + padding:.34375em .625em +} +input[disabled]{ + cursor:default +} +input[type="checkbox"],input[type="radio"]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + padding:0; + margin-right:.3125em; + *height:13px; + *width:13px +} +input[type="search"]{ + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box +} +input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{ + -webkit-appearance:none +} +input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{ + outline:0; + outline:thin dotted \9; + border-color:#333 +} +input.no-focus:focus{ + border-color:#ccc !important +} +input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{ + outline:thin dotted #333; + outline:1px auto #129FEA +} +input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{ + cursor:not-allowed; + background-color:#fafafa +} +input:focus:invalid,textarea:focus:invalid,select:focus:invalid{ + color:#E74C3C; + border:1px solid #E74C3C +} +input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{ + border-color:#E74C3C +} +input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{ + outline-color:#E74C3C +} +input.wy-input-large{ + padding:12px; + font-size:100% +} +textarea{ + overflow:auto; + vertical-align:top; + width:100%; + font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif +} +select,textarea{ + padding:.5em .625em; + display:inline-block; + border:1px solid #ccc; + font-size:80%; + box-shadow:inset 0 1px 3px #ddd; + -webkit-transition:border 0.3s linear; + -moz-transition:border 0.3s linear; + transition:border 0.3s linear +} +select{ + border:1px solid #ccc; + background-color:#fff +} +select[multiple]{ + height:auto +} +select:focus,textarea:focus{ + outline:0 +} +select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{ + cursor:not-allowed; + background-color:#fafafa +} +input[type="radio"][disabled],input[type="checkbox"][disabled]{ + cursor:not-allowed +} +.wy-checkbox,.wy-radio{ + margin:6px 0; + color:#404040; + display:block +} +.wy-checkbox input,.wy-radio input{ + vertical-align:baseline +} +.wy-form-message-inline{ + display:inline-block; + *display:inline; + *zoom:1; + vertical-align:middle +} +.wy-input-prefix,.wy-input-suffix{ + white-space:nowrap; + padding:6px +} +.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{ + line-height:27px; + padding:0 8px; + display:inline-block; + font-size:80%; + background-color:#f3f6f6; + border:solid 1px #ccc; + color:#999 +} +.wy-input-suffix .wy-input-context{ + border-left:0 +} +.wy-input-prefix .wy-input-context{ + border-right:0 +} +.wy-switch{ + position:relative; + display:block; + height:24px; + margin-top:12px; + cursor:pointer +} +.wy-switch:before{ + position:absolute; + content:""; + display:block; + left:0; + top:0; + width:36px; + height:12px; + border-radius:4px; + background:#ccc; + -webkit-transition:all 0.2s ease-in-out; + -moz-transition:all 0.2s ease-in-out; + transition:all 0.2s ease-in-out +} +.wy-switch:after{ + position:absolute; + content:""; + display:block; + width:18px; + height:18px; + border-radius:4px; + background:#999; + left:-3px; + top:-3px; + -webkit-transition:all 0.2s ease-in-out; + -moz-transition:all 0.2s ease-in-out; + transition:all 0.2s ease-in-out +} +.wy-switch span{ + position:absolute; + left:48px; + display:block; + font-size:12px; + color:#ccc; + line-height:1 +} +.wy-switch.active:before{ + background:#1e8449 +} +.wy-switch.active:after{ + left:24px; + background:#27AE60 +} +.wy-switch.disabled{ + cursor:not-allowed; + opacity:0.8 +} +.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{ + color:#E74C3C +} +.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{ + border:solid 1px #E74C3C +} +.wy-control-group.wy-control-group-error textarea{ + border:solid 1px #E74C3C +} +.wy-inline-validate{ + white-space:nowrap +} +.wy-inline-validate .wy-input-context{ + padding:.5em .625em; + display:inline-block; + font-size:80% +} +.wy-inline-validate.wy-inline-validate-success .wy-input-context{ + color:#27AE60 +} +.wy-inline-validate.wy-inline-validate-danger .wy-input-context{ + color:#E74C3C +} +.wy-inline-validate.wy-inline-validate-warning .wy-input-context{ + color:#E67E22 +} +.wy-inline-validate.wy-inline-validate-info .wy-input-context{ + color:#2980B9 +} +.rotate-90{ + -webkit-transform:rotate(90deg); + -moz-transform:rotate(90deg); + -ms-transform:rotate(90deg); + -o-transform:rotate(90deg); + transform:rotate(90deg) +} +.rotate-180{ + -webkit-transform:rotate(180deg); + -moz-transform:rotate(180deg); + -ms-transform:rotate(180deg); + -o-transform:rotate(180deg); + transform:rotate(180deg) +} +.rotate-270{ + -webkit-transform:rotate(270deg); + -moz-transform:rotate(270deg); + -ms-transform:rotate(270deg); + -o-transform:rotate(270deg); + transform:rotate(270deg) +} +.mirror{ + -webkit-transform:scaleX(-1); + -moz-transform:scaleX(-1); + -ms-transform:scaleX(-1); + -o-transform:scaleX(-1); + transform:scaleX(-1) +} +.mirror.rotate-90{ + -webkit-transform:scaleX(-1) rotate(90deg); + -moz-transform:scaleX(-1) rotate(90deg); + -ms-transform:scaleX(-1) rotate(90deg); + -o-transform:scaleX(-1) rotate(90deg); + transform:scaleX(-1) rotate(90deg) +} +.mirror.rotate-180{ + -webkit-transform:scaleX(-1) rotate(180deg); + -moz-transform:scaleX(-1) rotate(180deg); + -ms-transform:scaleX(-1) rotate(180deg); + -o-transform:scaleX(-1) rotate(180deg); + transform:scaleX(-1) rotate(180deg) +} +.mirror.rotate-270{ + -webkit-transform:scaleX(-1) rotate(270deg); + -moz-transform:scaleX(-1) rotate(270deg); + -ms-transform:scaleX(-1) rotate(270deg); + -o-transform:scaleX(-1) rotate(270deg); + transform:scaleX(-1) rotate(270deg) +} +@media only screen and (max-width: 480px){ + .wy-form button[type="submit"]{ + margin:0.7em 0 0 + } + .wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{ + margin-bottom:0.3em; + display:block + } + .wy-form label{ + margin-bottom:0.3em; + display:block + } + .wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{ + margin-bottom:0 + } + .wy-form-aligned .wy-control-group label{ + margin-bottom:0.3em; + text-align:left; + display:block; + width:100% + } + .wy-form-aligned .wy-control{ + margin:1.5em 0 0 0 + } + .wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{ + display:block; + font-size:80%; + padding:6px 0 + } +} +@media screen and (max-width: 768px){ + .tablet-hide{ + display:none + } +} +@media screen and (max-width: 480px){ + .mobile-hide{ + display:none + } +} +.float-left{ + float:left +} +.float-right{ + float:right +} +.full-width{ + width:100% +} +.wy-table,.rst-content table.docutils,.rst-content table.field-list{ + border-collapse:collapse; + border-spacing:0; + empty-cells:show; + margin-bottom:24px +} +.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{ + color:#000; + font:italic 85%/1 arial,sans-serif; + padding:1em 0; + text-align:center +} +.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{ + font-size:90%; + margin:0; + overflow:visible; + padding:8px 16px +} +.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{ + border-left-width:0 +} +.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{ + color:#000; + text-align:left; + vertical-align:bottom; + white-space:nowrap +} +.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{ + font-weight:bold; + border-bottom:solid 2px #e1e4e5 +} +.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{ + background-color:transparent; + vertical-align:middle +} +.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{ + line-height:18px +} +.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{ + margin-bottom:0 +} +.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{ + width:1%; + padding-right:0 +} +.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{ + margin:0 +} +.wy-table-secondary{ + color:gray; + font-size:90% +} +.wy-table-tertiary{ + color:gray; + font-size:80% +} +.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{ + background-color:#f3f6f6 +} +.wy-table-backed{ + background-color:#f3f6f6 +} +.wy-table-bordered-all,.rst-content table.docutils{ + border:1px solid #e1e4e5 +} +.wy-table-bordered-all td,.rst-content table.docutils td{ + border-bottom:1px solid #e1e4e5; + border-left:1px solid #e1e4e5 +} +.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{ + border-bottom-width:0 +} +.wy-table-bordered{ + border:1px solid #e1e4e5 +} +.wy-table-bordered-rows td{ + border-bottom:1px solid #e1e4e5 +} +.wy-table-bordered-rows tbody>tr:last-child td{ + border-bottom-width:0 +} +.wy-table-horizontal tbody>tr:last-child td{ + border-bottom-width:0 +} +.wy-table-horizontal td,.wy-table-horizontal th{ + border-width:0 0 1px 0; + border-bottom:1px solid #e1e4e5 +} +.wy-table-horizontal tbody>tr:last-child td{ + border-bottom-width:0 +} +.wy-table-responsive{ + margin-bottom:24px; + max-width:100%; + overflow:auto +} +.wy-table-responsive table{ + margin-bottom:0 !important +} +.wy-table-responsive table td,.wy-table-responsive table th{ + white-space:nowrap +} +a{ + color:#2980B9; + text-decoration:none; + cursor:pointer +} +a:hover{ + color:#3091d1 +} +a:visited{ + color:#9B59B6 +} +html{ + height:100%; + overflow-x:hidden +} +body{ + font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + font-weight:normal; + color:#404040; + min-height:100%; + overflow-x:hidden; + background:#edf0f2 +} +.wy-text-left{ + text-align:left +} +.wy-text-center{ + text-align:center +} +.wy-text-right{ + text-align:right +} +.wy-text-large{ + font-size:120% +} +.wy-text-normal{ + font-size:100% +} +.wy-text-small,small{ + font-size:80% +} +.wy-text-strike{ + text-decoration:line-through +} +.wy-text-warning{ + color:#E67E22 !important +} +a.wy-text-warning:hover{ + color:#eb9950 !important +} +.wy-text-info{ + color:#2980B9 !important +} +a.wy-text-info:hover{ + color:#409ad5 !important +} +.wy-text-success{ + color:#27AE60 !important +} +a.wy-text-success:hover{ + color:#36d278 !important +} +.wy-text-danger{ + color:#E74C3C !important +} +a.wy-text-danger:hover{ + color:#ed7669 !important +} +.wy-text-neutral{ + color:#404040 !important +} +a.wy-text-neutral:hover{ + color:#595959 !important +} +h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{ + margin-top:0; + font-weight:700; + font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif +} +p{ + line-height:24px; + margin:0; + font-size:16px; + margin-bottom:24px +} +h1{ + font-size:175% +} +h2,.rst-content .toctree-wrapper p.caption{ + font-size:150% +} +h3{ + font-size:125% +} +h4{ + font-size:115% +} +h5{ + font-size:110% +} +h6{ + font-size:100% +} +hr{ + display:block; + height:1px; + border:0; + border-top:1px solid #e1e4e5; + margin:24px 0; + padding:0 +} +code,.rst-content tt,.rst-content code{ + white-space:nowrap; + max-width:100%; + background:#fff; + border:solid 1px #e1e4e5; + font-size:85%; + padding:0 5px; + font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + color:#E74C3C; + overflow-x:auto +} +code.code-large,.rst-content tt.code-large{ + font-size:90% +} +.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{ + list-style:disc; + line-height:24px; + margin-bottom:24px +} +.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{ + list-style:disc; + margin-left:24px +} +.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{ + margin-bottom:0 +} +.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{ + margin-bottom:0 +} +.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{ + list-style:circle +} +.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{ + list-style:square +} +.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{ + list-style:decimal +} +.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{ + list-style:decimal; + line-height:24px; + margin-bottom:24px +} +.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{ + list-style:decimal; + margin-left:24px +} +.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{ + margin-bottom:0 +} +.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{ + margin-bottom:0 +} +.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{ + list-style:disc +} +.wy-breadcrumbs{ + *zoom:1 +} +.wy-breadcrumbs:before,.wy-breadcrumbs:after{ + display:table; + content:"" +} +.wy-breadcrumbs:after{ + clear:both +} +.wy-breadcrumbs li{ + display:inline-block +} +.wy-breadcrumbs li.wy-breadcrumbs-aside{ + float:right +} +.wy-breadcrumbs li a{ + display:inline-block; + padding:5px +} +.wy-breadcrumbs li a:first-child{ + padding-left:0 +} +.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{ + padding:5px; + border:none; + background:none +} +.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{ + color:#404040 +} +.wy-breadcrumbs-extra{ + margin-bottom:0; + color:#b3b3b3; + font-size:80%; + display:inline-block +} +@media screen and (max-width: 480px){ + .wy-breadcrumbs-extra{ + display:none + } + .wy-breadcrumbs li.wy-breadcrumbs-aside{ + display:none + } +} +@media print{ + .wy-breadcrumbs li.wy-breadcrumbs-aside{ + display:none + } +} +.wy-affix{ + position:fixed; + top:1.618em +} +.wy-menu a:hover{ + text-decoration:none +} +.wy-menu-horiz{ + *zoom:1 +} +.wy-menu-horiz:before,.wy-menu-horiz:after{ + display:table; + content:"" +} +.wy-menu-horiz:after{ + clear:both +} +.wy-menu-horiz ul,.wy-menu-horiz li{ + display:inline-block +} +.wy-menu-horiz li:hover{ + background:rgba(255,255,255,0.1) +} +.wy-menu-horiz li.divide-left{ + border-left:solid 1px #404040 +} +.wy-menu-horiz li.divide-right{ + border-right:solid 1px #404040 +} +.wy-menu-horiz a{ + height:32px; + display:inline-block; + line-height:32px; + padding:0 16px +} +.wy-menu-vertical{ + width:300px +} +.wy-menu-vertical header,.wy-menu-vertical p.caption{ + height:32px; + display:inline-block; + line-height:32px; + padding:0 1.618em; + margin-bottom:0; + display:block; + font-weight:bold; + text-transform:uppercase; + font-size:80%; + color:#6f6f6f; + white-space:nowrap +} +.wy-menu-vertical ul{ + margin-bottom:0 +} +.wy-menu-vertical li.divide-top{ + border-top:solid 1px #404040 +} +.wy-menu-vertical li.divide-bottom{ + border-bottom:solid 1px #404040 +} +.wy-menu-vertical li.current{ + background:#e3e3e3 +} +.wy-menu-vertical li.current a{ + color:gray; + border-right:solid 1px #c9c9c9; + padding:.4045em 2.427em +} +.wy-menu-vertical li.current a:hover{ + background:#d6d6d6 +} +.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{ + border:none; + background:inherit; + color:inherit; + padding-left:0; + padding-right:0 +} +.wy-menu-vertical li span.toctree-expand{ + display:block; + float:left; + margin-left:-1.2em; + font-size:.8em; + line-height:1.6em; + color:#4d4d4d +} +.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{ + color:#404040; + padding:.4045em 1.618em; + font-weight:bold; + position:relative; + background:#fcfcfc; + border:none; + padding-left:1.618em -4px +} +.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{ + background:#fcfcfc +} +.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{ + color:gray +} +.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{ + display:block; + font-size:.8em; + line-height:1.6em; + color:#333 +} +.wy-menu-vertical li.toctree-l1.current>a{ + border-bottom:solid 1px #c9c9c9; + border-top:solid 1px #c9c9c9 +} +.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{ + display:none +} +.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{ + display:block +} +.wy-menu-vertical li.toctree-l2.current>a{ + background:#c9c9c9; + padding:.4045em 2.427em +} +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{ + display:block; + background:#c9c9c9; + padding:.4045em 4.045em +} +.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{ + color:gray +} +.wy-menu-vertical li.toctree-l2 span.toctree-expand{ + color:#a3a3a3 +} +.wy-menu-vertical li.toctree-l3{ + font-size:.9em +} +.wy-menu-vertical li.toctree-l3.current>a{ + background:#bdbdbd; + padding:.4045em 4.045em +} +.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{ + display:block; + background:#bdbdbd; + padding:.4045em 5.663em +} +.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{ + color:gray +} +.wy-menu-vertical li.toctree-l3 span.toctree-expand{ + color:#969696 +} +.wy-menu-vertical li.toctree-l4{ + font-size:.9em +} +.wy-menu-vertical li.current ul{ + display:block +} +.wy-menu-vertical li ul{ + margin-bottom:0; + display:none +} +.wy-menu-vertical .local-toc li ul{ + display:block +} +.wy-menu-vertical li ul li a{ + margin-bottom:0; + color:#b3b3b3; + font-weight:normal +} +.wy-menu-vertical a{ + display:inline-block; + line-height:18px; + padding:.4045em 1.618em; + display:block; + position:relative; + font-size:90%; + color:#b3b3b3 +} +.wy-menu-vertical a:hover{ + background-color:#4e4a4a; + cursor:pointer +} +.wy-menu-vertical a:hover span.toctree-expand{ + color:#b3b3b3 +} +.wy-menu-vertical a:active{ + background-color:#2980B9; + cursor:pointer; + color:#fff +} +.wy-menu-vertical a:active span.toctree-expand{ + color:#fff +} +.wy-side-nav-search{ + display:block; + width:300px; + padding:.809em; + margin-bottom:.809em; + z-index:200; + background-color:#2980B9; + text-align:center; + padding:.809em; + display:block; + color:#fcfcfc; + margin-bottom:.809em +} +.wy-side-nav-search input[type=text]{ + width:100%; + border-radius:50px; + padding:6px 12px; + border-color:#2472a4 +} +.wy-side-nav-search img{ + display:block; + margin:auto auto .809em auto; + height:45px; + width:45px; + background-color:#2980B9; + padding:5px; + border-radius:100% +} +.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{ + color:#fcfcfc; + font-size:100%; + font-weight:bold; + display:inline-block; + padding:4px 6px; + margin-bottom:.809em +} +.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{ + background:rgba(255,255,255,0.1) +} +.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{ + display:block; + margin:0 auto; + height:auto; + width:auto; + border-radius:0; + max-width:100%; + background:transparent +} +.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{ + margin-top:.85em +} +.wy-side-nav-search>div.version{ + margin-top:-.4045em; + margin-bottom:.809em; + font-weight:normal; + color:rgba(255,255,255,0.3) +} +.wy-nav .wy-menu-vertical header{ + color:#2980B9 +} +.wy-nav .wy-menu-vertical a{ + color:#b3b3b3 +} +.wy-nav .wy-menu-vertical a:hover{ + background-color:#2980B9; + color:#fff +} +[data-menu-wrap]{ + -webkit-transition:all .2s ease-in; + -moz-transition:all .2s ease-in; + transition:all .2s ease-in; + position:absolute; + opacity:1; + width:100%; + opacity:0 +} +[data-menu-wrap].move-center{ + left:0; + right:auto; + opacity:1 +} +[data-menu-wrap].move-left{ + right:auto; + left:-100%; + opacity:0 +} +[data-menu-wrap].move-right{ + right:-100%; + left:auto; + opacity:0 +} +.wy-body-for-nav{ + background:#fcfcfc +} +.wy-grid-for-nav{ + position:absolute; + width:100%; + height:100% +} +.wy-nav-side{ + position:fixed; + top:0; + bottom:0; + left:0; + padding-bottom:2em; + width:300px; + overflow-x:hidden; + overflow-y:hidden; + min-height:100%; + background:#343131; + z-index:200 +} +.wy-side-scroll{ + width:320px; + position:relative; + overflow-x:hidden; + overflow-y:scroll; + height:100% +} +.wy-nav-top{ + display:none; + background:#2980B9; + color:#fff; + padding:.4045em .809em; + position:relative; + line-height:50px; + text-align:center; + font-size:100%; + *zoom:1 +} +.wy-nav-top:before,.wy-nav-top:after{ + display:table; + content:"" +} +.wy-nav-top:after{ + clear:both +} +.wy-nav-top a{ + color:#fff; + font-weight:bold +} +.wy-nav-top img{ + margin-right:12px; + height:45px; + width:45px; + background-color:#2980B9; + padding:5px; + border-radius:100% +} +.wy-nav-top i{ + font-size:30px; + float:left; + cursor:pointer; + padding-top:inherit +} +.wy-nav-content-wrap{ + margin-left:300px; + background:#fcfcfc; + min-height:100% +} +.wy-nav-content{ + padding:1.618em 3.236em; + height:100%; + max-width:800px; + margin:auto +} +.wy-body-mask{ + position:fixed; + width:100%; + height:100%; + background:rgba(0,0,0,0.2); + display:none; + z-index:499 +} +.wy-body-mask.on{ + display:block +} +footer{ + color:gray +} +footer p{ + margin-bottom:12px +} +footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{ + padding:0px; + font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + font-size:1em; + background:none; + border:none; + color:gray +} +.rst-footer-buttons{ + *zoom:1 +} +.rst-footer-buttons:before,.rst-footer-buttons:after{ + width:100% +} +.rst-footer-buttons:before,.rst-footer-buttons:after{ + display:table; + content:"" +} +.rst-footer-buttons:after{ + clear:both +} +.rst-breadcrumbs-buttons{ + margin-top:12px; + *zoom:1 +} +.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{ + display:table; + content:"" +} +.rst-breadcrumbs-buttons:after{ + clear:both +} +#search-results .search li{ + margin-bottom:24px; + border-bottom:solid 1px #e1e4e5; + padding-bottom:24px +} +#search-results .search li:first-child{ + border-top:solid 1px #e1e4e5; + padding-top:24px +} +#search-results .search li a{ + font-size:120%; + margin-bottom:12px; + display:inline-block +} +#search-results .context{ + color:gray; + font-size:90% +} +@media screen and (max-width: 768px){ + .wy-body-for-nav{ + background:#fcfcfc + } + .wy-nav-top{ + display:block + } + .wy-nav-side{ + left:-300px + } + .wy-nav-side.shift{ + width:85%; + left:0 + } + .wy-side-scroll{ + width:auto + } + .wy-side-nav-search{ + width:auto + } + .wy-menu.wy-menu-vertical{ + width:auto + } + .wy-nav-content-wrap{ + margin-left:0 + } + .wy-nav-content-wrap .wy-nav-content{ + padding:1.618em + } + .wy-nav-content-wrap.shift{ + position:fixed; + min-width:100%; + left:85%; + top:0; + height:100%; + overflow:hidden + } +} +@media screen and (min-width: 1100px){ + .wy-nav-content-wrap{ + background:rgba(0,0,0,0.05) + } + .wy-nav-content{ + margin:0; + background:#fcfcfc + } +} +@media print{ + .rst-versions,footer,.wy-nav-side{ + display:none + } + .wy-nav-content-wrap{ + margin-left:0 + } +} +.rst-versions{ + position:fixed; + bottom:0; + left:0; + width:300px; + color:#fcfcfc; + background:#1f1d1d; + border-top:solid 10px #343131; + font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + z-index:400 +} +.rst-versions a{ + color:#2980B9; + text-decoration:none +} +.rst-versions .rst-badge-small{ + display:none +} +.rst-versions .rst-current-version{ + padding:12px; + background-color:#272525; + display:block; + text-align:right; + font-size:90%; + cursor:pointer; + color:#27AE60; + *zoom:1 +} +.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{ + display:table; + content:"" +} +.rst-versions .rst-current-version:after{ + clear:both +} +.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{ + color:#fcfcfc +} +.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{ + float:left +} +.rst-versions .rst-current-version .icon-book{ + float:left +} +.rst-versions .rst-current-version.rst-out-of-date{ + background-color:#E74C3C; + color:#fff +} +.rst-versions .rst-current-version.rst-active-old-version{ + background-color:#F1C40F; + color:#000 +} +.rst-versions.shift-up .rst-other-versions{ + display:block +} +.rst-versions .rst-other-versions{ + font-size:90%; + padding:12px; + color:gray; + display:none +} +.rst-versions .rst-other-versions hr{ + display:block; + height:1px; + border:0; + margin:20px 0; + padding:0; + border-top:solid 1px #413d3d +} +.rst-versions .rst-other-versions dd{ + display:inline-block; + margin:0 +} +.rst-versions .rst-other-versions dd a{ + display:inline-block; + padding:6px; + color:#fcfcfc +} +.rst-versions.rst-badge{ + width:auto; + bottom:20px; + right:20px; + left:auto; + border:none; + max-width:300px +} +.rst-versions.rst-badge .icon-book{ + float:none +} +.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{ + float:none +} +.rst-versions.rst-badge.shift-up .rst-current-version{ + text-align:right +} +.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{ + float:left +} +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{ + float:left +} +.rst-versions.rst-badge .rst-current-version{ + width:auto; + height:30px; + line-height:30px; + padding:0 6px; + display:block; + text-align:center +} +@media screen and (max-width: 768px){ + .rst-versions{ + width:85%; + display:none + } + .rst-versions.shift{ + display:block + } +} +.rst-content img{ + max-width:100%; + height:auto +} +.rst-content div.figure{ + margin-bottom:24px +} +.rst-content div.figure p.caption{ + font-style:italic +} +.rst-content div.figure p:last-child.caption{ + margin-bottom:0px +} +.rst-content div.figure.align-center{ + text-align:center +} +.rst-content .section>img,.rst-content .section>a>img{ + margin-bottom:24px +} +.rst-content abbr[title]{ + text-decoration:none +} +.rst-content.style-external-links a.reference.external:after{ + font-family:FontAwesome; + content:""; + color:#b3b3b3; + vertical-align:super; + font-size:60%; + margin:0 .2em +} +.rst-content blockquote{ + margin-left:24px; + line-height:24px; + margin-bottom:24px +} +.rst-content pre.literal-block,.rst-content div[class^='highlight']{ + border:1px solid #e1e4e5; + padding:0px; + overflow-x:auto; + margin:1px 0 24px 0 +} +.rst-content pre.literal-block div[class^='highlight'],.rst-content div[class^='highlight'] div[class^='highlight']{ + border:none; + margin:0 +} +.rst-content div[class^='highlight'] td.code{ + width:100% +} +.rst-content .linenodiv pre{ + border-right:solid 1px #e6e9ea; + margin:0; + padding:12px 12px; + font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace +} +.rst-content div[class^='highlight'] pre{ + white-space:pre; + margin:0; + padding:12px 12px; + font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + display:block; + overflow:auto +} +.rst-content pre.literal-block,.rst-content div[class^='highlight'] pre,.rst-content .linenodiv pre{ + font-size:14px; + line-height:normal +} +@media print{ + .rst-content .codeblock,.rst-content div[class^='highlight'],.rst-content div[class^='highlight'] pre{ + white-space:pre-wrap + } +} +.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last,.rst-content .admonition .last{ + margin-bottom:0 +} +.rst-content .admonition-title:before{ + margin-right:4px +} +.rst-content .admonition table{ + border-color:rgba(0,0,0,0.1) +} +.rst-content .admonition table td,.rst-content .admonition table th{ + background:transparent !important; + border-color:rgba(0,0,0,0.1) !important +} +.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{ + list-style:lower-alpha +} +.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{ + list-style:upper-alpha +} +.rst-content .section ol p,.rst-content .section ul p{ + margin-bottom:12px +} +.rst-content .line-block{ + margin-left:0px; + margin-bottom:24px +} +.rst-content .line-block .line-block{ + margin-left:24px; + margin-bottom:0px +} +.rst-content .topic-title{ + font-weight:bold; + margin-bottom:12px +} +.rst-content .toc-backref{ + color:#404040 +} +.rst-content .align-right{ + float:right; + margin:0px 0px 24px 24px +} +.rst-content .align-left{ + float:left; + margin:0px 24px 24px 0px +} +.rst-content .align-center{ + margin:auto; + display:block +} +.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink{ + visibility:hidden; + font-size:14px +} +.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after{ + content:""; + font-family:FontAwesome +} +.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content .toctree-wrapper p.caption:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after{ + visibility:visible +} +.rst-content table>caption .headerlink:after{ + font-size:12px +} +.rst-content .centered{ + text-align:center +} +.rst-content .sidebar{ + float:right; + width:40%; + display:block; + margin:0 0 24px 24px; + padding:24px; + background:#f3f6f6; + border:solid 1px #e1e4e5 +} +.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{ + font-size:90% +} +.rst-content .sidebar .last{ + margin-bottom:0 +} +.rst-content .sidebar .sidebar-title{ + display:block; + font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif; + font-weight:bold; + background:#e1e4e5; + padding:6px 12px; + margin:-24px; + margin-bottom:24px; + font-size:100% +} +.rst-content .highlighted{ + background:#F1C40F; + display:inline-block; + font-weight:bold; + padding:0 6px +} +.rst-content .footnote-reference,.rst-content .citation-reference{ + vertical-align:baseline; + position:relative; + top:-0.4em; + line-height:0; + font-size:90% +} +.rst-content table.docutils.citation,.rst-content table.docutils.footnote{ + background:none; + border:none; + color:gray +} +.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{ + border:none; + background-color:transparent !important; + white-space:normal +} +.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{ + padding-left:0; + padding-right:0; + vertical-align:top +} +.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{ + color:#555 +} +.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{ + margin-bottom:0 +} +.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){ + margin-top:24px +} +.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{ + margin-bottom:24px +} +.rst-content table.field-list{ + border:none +} +.rst-content table.field-list td{ + border:none +} +.rst-content table.field-list td>strong{ + display:inline-block +} +.rst-content table.field-list .field-name{ + padding-right:10px; + text-align:left; + white-space:nowrap +} +.rst-content table.field-list .field-body{ + text-align:left +} +.rst-content tt,.rst-content tt,.rst-content code{ + color:#000; + padding:2px 5px +} +.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{ + font-size:100% !important; + line-height:normal +} +.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{ + color:#E74C3C +} +.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{ + font-weight:bold; + color:#404040 +} +.rst-content a tt,.rst-content a tt,.rst-content a code{ + color:#2980B9 +} +.rst-content dl{ + margin-bottom:24px +} +.rst-content dl dt{ + font-weight:bold +} +.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{ + margin-bottom:12px !important +} +.rst-content dl dd{ + margin:0 0 12px 24px +} +.rst-content dl:not(.docutils){ + margin-bottom:24px +} +.rst-content dl:not(.docutils) dt{ + display:table; + margin:6px 0; + font-size:90%; + line-height:normal; + background:#e7f2fa; + color:#2980B9; + border-top:solid 3px #6ab0de; + padding:6px; + position:relative +} +.rst-content dl:not(.docutils) dt:before{ + color:#6ab0de +} +.rst-content dl:not(.docutils) dt .headerlink{ + color:#404040; + font-size:100% !important +} +.rst-content dl:not(.docutils) dl dt{ + margin-bottom:6px; + border:none; + border-left:solid 3px #ccc; + background:#f0f0f0; + color:#555 +} +.rst-content dl:not(.docutils) dl dt .headerlink{ + color:#404040; + font-size:100% !important +} +.rst-content dl:not(.docutils) dt:first-child{ + margin-top:0 +} +.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{ + font-weight:bold +} +.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{ + background-color:transparent; + border:none; + padding:0; + font-size:100% !important +} +.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{ + font-weight:bold +} +.rst-content dl:not(.docutils) .optional{ + display:inline-block; + padding:0 4px; + color:#000; + font-weight:bold +} +.rst-content dl:not(.docutils) .property{ + display:inline-block; + padding-right:8px +} +.rst-content .viewcode-link,.rst-content .viewcode-back{ + display:inline-block; + color:#27AE60; + font-size:80%; + padding-left:24px +} +.rst-content .viewcode-back{ + display:block; + float:right +} +.rst-content p.rubric{ + margin-bottom:12px; + font-weight:bold +} +.rst-content tt.download,.rst-content code.download{ + background:inherit; + padding:inherit; + font-weight:normal; + font-family:inherit; + font-size:inherit; + color:inherit; + border:inherit; + white-space:inherit +} +.rst-content tt.download span:first-child,.rst-content code.download span:first-child{ + -webkit-font-smoothing:subpixel-antialiased +} +.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{ + margin-right:4px +} +.rst-content .guilabel{ + border:1px solid #7fbbe3; + background:#e7f2fa; + font-size:80%; + font-weight:700; + border-radius:4px; + padding:2.4px 6px; + margin:auto 2px +} +.rst-content .versionmodified{ + font-style:italic +} +@media screen and (max-width: 480px){ + .rst-content .sidebar{ + width:100% + } +} +span[id*='MathJax-Span']{ + color:#404040 +} +.math{ + text-align:center +} +/*@font-face{*/ + /*font-family:"Inconsolata";*/ + /*font-style:normal;*/ + /*font-weight:400;*/ + /*src:local("Inconsolata"),local("Inconsolata-Regular"),url(https://media.readthedocs.org/fonts/Inconsolata-Regular.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Inconsolata";*/ + /*font-style:normal;*/ + /*font-weight:700;*/ + /*src:local("Inconsolata Bold"),local("Inconsolata-Bold"),url(https://media.readthedocs.org/fonts/Inconsolata-Bold.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Lato";*/ + /*font-style:normal;*/ + /*font-weight:400;*/ + /*src:local("Lato Regular"),local("Lato-Regular"),url(https://media.readthedocs.org/fonts/Lato-Regular.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Lato";*/ + /*font-style:normal;*/ + /*font-weight:700;*/ + /*src:local("Lato Bold"),local("Lato-Bold"),url(https://media.readthedocs.org/fonts/Lato-Bold.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Lato";*/ + /*font-style:italic;*/ + /*font-weight:400;*/ + /*src:local("Lato Italic"),local("Lato-Italic"),url(https://media.readthedocs.org/fonts/Lato-Italic.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Lato";*/ + /*font-style:italic;*/ + /*font-weight:700;*/ + /*src:local("Lato Bold Italic"),local("Lato-BoldItalic"),url(https://media.readthedocs.org/fonts/Lato-BoldItalic.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Roboto Slab";*/ + /*font-style:normal;*/ + /*font-weight:400;*/ + /*src:local("Roboto Slab Regular"),local("RobotoSlab-Regular"),url(https://media.readthedocs.org/fonts/RobotoSlab-Regular.ttf) format("truetype")*/ +/*}*/ +/*@font-face{*/ + /*font-family:"Roboto Slab";*/ + /*font-style:normal;*/ + /*font-weight:700;*/ + /*src:local("Roboto Slab Bold"),local("RobotoSlab-Bold"),url(https://media.readthedocs.org/fonts/RobotoSlab-Bold.ttf) format("truetype")*/ +/*}*/ From 4049886e7a0d8af6721b554e7efc68767646fc1c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 24 Mar 2018 14:47:20 +0100 Subject: [PATCH 116/285] Update docs --- docs/source/_static/css/theme.css | 17 +++---- docs/source/_templates/layout.html | 2 +- docs/source/index.rst | 58 ++++++++--------------- docs/source/resources/BotsInteraction.rst | 4 +- docs/source/start/QuickInstallation.rst | 14 ------ 5 files changed, 33 insertions(+), 62 deletions(-) diff --git a/docs/source/_static/css/theme.css b/docs/source/_static/css/theme.css index f15475bb..bcb9b6f5 100644 --- a/docs/source/_static/css/theme.css +++ b/docs/source/_static/css/theme.css @@ -2659,7 +2659,7 @@ button[disabled]{ background-color:#27AE60; text-decoration:none; font-weight:normal; - font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset; outline-none:false; vertical-align:middle; @@ -3056,7 +3056,7 @@ input{ input[type="button"],input[type="reset"],input[type="submit"]{ -webkit-appearance:button; cursor:pointer; - font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; *overflow:visible } input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{ @@ -3065,7 +3065,7 @@ input[type="text"],input[type="password"],input[type="email"],input[type="url"], display:inline-block; border:1px solid #ccc; font-size:80%; - font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; box-shadow:inset 0 1px 3px #ddd; border-radius:0; -webkit-transition:border 0.3s linear; @@ -3129,7 +3129,7 @@ textarea{ overflow:auto; vertical-align:top; width:100%; - font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif + font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif } select,textarea{ padding:.5em .625em; @@ -3490,7 +3490,7 @@ html{ overflow-x:hidden } body{ - font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; font-weight:normal; color:#404040; min-height:100%; @@ -3606,7 +3606,8 @@ code.code-large,.rst-content tt.code-large{ } .wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{ list-style:disc; - margin-left:24px + margin-left:24px; + margin-bottom: 6px; } .wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{ margin-bottom:0 @@ -4143,7 +4144,7 @@ footer span.commit code,footer span.commit .rst-content tt,.rst-content footer s margin-left:0 } .wy-nav-content-wrap .wy-nav-content{ - padding:1.618em + padding:1.2em } .wy-nav-content-wrap.shift{ position:fixed; @@ -4179,7 +4180,7 @@ footer span.commit code,footer span.commit .rst-content tt,.rst-content footer s color:#fcfcfc; background:#1f1d1d; border-top:solid 10px #343131; - font-family:"Open Sans","proxima-nova","Helvetica Neue",Arial,sans-serif; + font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; z-index:400 } .rst-versions a{ diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html index d8d59c8c..99380056 100644 --- a/docs/source/_templates/layout.html +++ b/docs/source/_templates/layout.html @@ -87,7 +87,7 @@ - + - - - {% endblock %} - - - - - {% block extrabody %} {% endblock %} -
- - {# SIDE NAV, TOGGLES ON MOBILE #} - - -
- - {# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #} - - - -
- {%- block content %} - {% if theme_style_external_links|tobool %} - - -
- -
- {% include "versions.html" %} - - {% if not embedded %} - - - {%- for scriptfile in script_files %} - - {%- endfor %} - - {% endif %} - - {# RTD hosts this file, so just load on non RTD builds #} - {% if not READTHEDOCS %} - - {% endif %} - - - - {%- block footer %} {% endblock %} - - - \ No newline at end of file From 846fb0b637120c523c057f6764158fee9bed68ff Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 31 Mar 2018 12:20:22 +0200 Subject: [PATCH 141/285] Add photo field --- pyrogram/client/utils.py | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index fa5ff41a..b47f81b5 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -1,4 +1,5 @@ from base64 import b64encode, b64decode +from struct import pack import pyrogram from pyrogram.api import types @@ -103,6 +104,45 @@ def parse_message(message: types.Message, users: dict, chats: dict): forward_from_message_id = forward_header.channel_post forward_signature = forward_header.post_author + photo = 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)): + location = size.location + + if isinstance(location, types.FileLocation): + photo_size = pyrogram.PhotoSize( + file_id=encode( + pack( + " Date: Sun, 1 Apr 2018 12:17:38 +0200 Subject: [PATCH 142/285] Fix chat parsing --- pyrogram/client/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index b47f81b5..b353250b 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -48,7 +48,7 @@ def parse_user(user: types.User): def parse_chat(message: types.Message, users: dict, chats: dict): if isinstance(message.to_id, types.PeerUser): - return parse_user_chat(users[message.from_id]) + 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: @@ -120,6 +120,11 @@ def parse_message(message: types.Message, users: dict, chats: dict): if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): location = size.location + if isinstance(size, types.PhotoSize): + file_size = size.size + else: + file_size = len(size.bytes) + if isinstance(location, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( @@ -136,7 +141,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): ), width=size.w, height=size.h, - file_size=size.size + file_size=file_size ) photo_sizes.append(photo_size) @@ -196,7 +201,7 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic return pyrogram.Message( message_id=message.id, date=message.date, - chat=parse_chat(message.to_id, users, chats), + chat=parse_chat(message, users, chats), from_user=parse_user(users.get(message.from_id, None)), new_chat_members=new_chat_members, left_chat_member=left_chat_member, From 44a3a4d69bc81804dde015627d8dc72e28b6f3b3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 1 Apr 2018 13:08:37 +0200 Subject: [PATCH 143/285] Add pyrogram.txt template --- compiler/api/template/{class.txt => mtproto.txt} | 0 compiler/api/template/pyrogram.txt | 10 ++++++++++ 2 files changed, 10 insertions(+) rename compiler/api/template/{class.txt => mtproto.txt} (100%) create mode 100644 compiler/api/template/pyrogram.txt diff --git a/compiler/api/template/class.txt b/compiler/api/template/mtproto.txt similarity index 100% rename from compiler/api/template/class.txt rename to compiler/api/template/mtproto.txt diff --git a/compiler/api/template/pyrogram.txt b/compiler/api/template/pyrogram.txt new file mode 100644 index 00000000..f5f7a96a --- /dev/null +++ b/compiler/api/template/pyrogram.txt @@ -0,0 +1,10 @@ +{notice} + + +class {class_name}: + """{docstring_args} + """ + ID = {object_id} + + def __init__(self{arguments}): + {fields} From c54730b8ab77f319c8acf4f68811ce459423b370 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 1 Apr 2018 13:09:32 +0200 Subject: [PATCH 144/285] Don't inherit from Object --- compiler/api/compiler.py | 47 ++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index a62e7bbe..7ee707a9 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -144,8 +144,11 @@ def start(): open("{}/source/pyrogram.tl".format(HOME), encoding="utf-8") as pyrogram: schema = (auth.read() + system.read() + api.read() + pyrogram.read()).splitlines() - with open("{}/template/class.txt".format(HOME), encoding="utf-8") as f: - template = f.read() + with open("{}/template/mtproto.txt".format(HOME), encoding="utf-8") as f: + mtproto_template = f.read() + + with open("{}/template/pyrogram.txt".format(HOME), encoding="utf-8") as f: + pyrogram_template = f.read() with open(NOTICE_PATH, encoding="utf-8") as f: notice = [] @@ -398,21 +401,33 @@ def start(): 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") From 1bbb282dab0f2aa184e498ed93b542d8f77add5e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 1 Apr 2018 13:27:05 +0200 Subject: [PATCH 145/285] Change type/desc separator --- compiler/api/compiler.py | 7 +++---- compiler/api/source/pyrogram.tl | 34 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 7ee707a9..3e15a2bd 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -268,6 +268,7 @@ def start(): ) if c.args else "pass" docstring_args = [] + docs = c.docs.split("|")[1:] if c.docs else None for i, arg in enumerate(sorted_args): arg_name, arg_type = arg @@ -275,15 +276,13 @@ def start(): flag_number = is_optional.group(1) if is_optional else -1 arg_type = arg_type.split("?")[-1] - docs = c.docs.split("|")[1:] if c.docs else None - if docs: docstring_args.append( "{} ({}{}):\n {}\n".format( arg_name, get_docstring_arg_type(arg_type), ", optional" if "Optional" in docs[i] else "", - re.sub("Optional\. ", "", docs[i].split(":")[1]) + re.sub("Optional\. ", "", docs[i].split("§")[1].rstrip(".") + ".") ) ) else: @@ -397,7 +396,7 @@ def start(): read_types += "{} = Object.read(b)\n ".format(arg_name) if c.docs: - description = c.docs.split("|")[0].split(":")[1] + 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: diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 911d3ed5..491e890c 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -2,20 +2,20 @@ ---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; // Docs: Update:This object represents an incoming update.At most one of the optional parameters can be present in any given update.|update_id:The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you're using Webhooks, since it allows you to ignore repeated updates or to restore the correct update sequence, should they get out of order. If there are no new updates for at least a week, then identifier of the next update will be chosen randomly instead of sequentially.|message:Optional. New incoming message of any kind � text, photo, sticker, etc.|edited_message:Optional. New version of a message that is known to the bot and was edited|channel_post:Optional. New incoming channel post of any kind � text, photo, sticker, etc.|edited_channel_post:Optional. New version of a channel post that is known to the bot and was edited|inline_query:Optional. New incoming inline query|chosen_inline_result:Optional. The result of an inline query that was chosen by a user and sent to their chat partner. Please see our documentation on the feedback collecting for details on how to enable these updates for your bot.|callback_query:Optional. New incoming callback query|shipping_query:Optional. New incoming shipping query. Only for invoices with flexible price|pre_checkout_query:Optional. New incoming pre-checkout query. Contains full information about checkout -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 = pyrogram.User; // Docs: User:This object represents a Telegram user or bot.|id:Unique identifier for this user or bot|is_bot:True, if this user is a bot|first_name:User's or bot's first name|last_name:Optional. User's or bot's last name|username:Optional. User's or bot's username|language_code:Optional. IETF language tag of the user's language -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; // Docs: Chat:This object represents a chat.|id:Unique identifier for this chat. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|type:Type of chat, can be either "private", "group", "supergroup" or "channel"|title:Optional. Title, for supergroups, channels and group chats|username:Optional. Username, for private chats, supergroups and channels if available|first_name:Optional. First name of the other party in a private chat|last_name:Optional. Last name of the other party in a private chat|all_members_are_administrators:Optional. True if a group has 'All Members Are Admins' enabled.|photo:Optional. Chat photo. Returned only in getChat.|description:Optional. Description, for supergroups and channel chats. Returned only in getChat.|invite_link:Optional. Chat invite link, for supergroups and channel chats. Returned only in getChat.|pinned_message:Optional. Pinned message, for supergroups and channel chats. Returned only in getChat.|sticker_set_name:Optional. For supergroups, name of group sticker set. Returned only in getChat.|can_set_sticker_set:Optional. True, if the bot can change the group sticker set. Returned only in getChat. -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 = pyrogram.Message; // Docs: Message:This object represents a message.|message_id:Unique message identifier inside this chat|from_user:Optional. Sender, empty for messages sent to channels|date:Date the message was sent in Unix time|chat:Conversation the message belongs to|forward_from:Optional. For forwarded messages, sender of the original message|forward_from_chat:Optional. For messages forwarded from channels, information about the original channel|forward_from_message_id:Optional. For messages forwarded from channels, identifier of the original message in the channel|forward_signature:Optional. For messages forwarded from channels, signature of the post author if present|forward_date:Optional. For forwarded messages, date the original message was sent in Unix time|reply_to_message:Optional. For replies, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply.|edit_date:Optional. Date the message was last edited in Unix time|media_group_id:Optional. The unique identifier of a media message group this message belongs to|author_signature:Optional. Signature of the post author for messages in channels|text:Optional. For text messages, the actual UTF-8 text of the message, 0-4096 characters.|entities:Optional. For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text|caption_entities:Optional. For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption|audio:Optional. Message is an audio file, information about the file|document:Optional. Message is a general file, information about the file|game:Optional. Message is a game, information about the game. More about games|photo:Optional. Message is a photo, available sizes of the photo|sticker:Optional. Message is a sticker, information about the sticker|video:Optional. Message is a video, information about the video|voice:Optional. Message is a voice message, information about the file|video_note:Optional. Message is a video note, information about the video message|caption:Optional. Caption for the audio, document, photo, video or voice, 0-200 characters|contact:Optional. Message is a shared contact, information about the contact|location:Optional. Message is a shared location, information about the location|venue:Optional. Message is a venue, information about the venue|new_chat_members:Optional. New members that were added to the group or supergroup and information about them (the bot itself may be one of these members)|left_chat_member:Optional. A member was removed from the group, information about them (this member may be the bot itself)|new_chat_title:Optional. A chat title was changed to this value|new_chat_photo:Optional. A chat photo was change to this value|delete_chat_photo:Optional. Service message: the chat photo was deleted|group_chat_created:Optional. Service message: the group has been created|supergroup_chat_created:Optional. Service message: the supergroup has been created. This field can't be received in a message coming through updates, because bot can't be a member of a supergroup when it is created. It can only be found in reply_to_message if someone replies to a very first message in a directly created supergroup.|channel_chat_created:Optional. Service message: the channel has been created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in reply_to_message if someone replies to a very first message in a channel.|migrate_to_chat_id:Optional. The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|migrate_from_chat_id:Optional. The supergroup has been migrated from a group with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|pinned_message:Optional. Specified message was pinned. Note that the Message object in this field will not contain further reply_to_message fields even if it is itself a reply.|invoice:Optional. Message is an invoice for a payment, information about the invoice. More about payments|successful_payment:Optional. Message is a service message about a successful payment, information about the payment. More about payments|connected_website:Optional. The domain name of the website on which the user has logged in. More about Telegram Login -pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; // Docs: MessageEntity:This object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc.|type:Type of the entity. Can be mention (@username), hashtag, bot_command, url, email, bold (bold text), italic (italic text), code (monowidth string), pre (monowidth block), text_link (for clickable text URLs), text_mention (for users without usernames)|offset:Offset in UTF-16 code units to the start of the entity|length:Length of the entity in UTF-16 code units|url:Optional. For "text_link" only, url that will be opened after user taps on the text|user:Optional. For "text_mention" only, the mentioned user -pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; // Docs: PhotoSize:This object represents one size of a photo or a file / sticker thumbnail.|file_id:Unique identifier for this file|width:Photo width|height:Photo height|file_size:Optional. File size -pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; // Docs: Audio:This object represents an audio file to be treated as music by the Telegram clients.|file_id:Unique identifier for this file|duration:Duration of the audio in seconds as defined by sender|performer:Optional. Performer of the audio as defined by sender or by audio tags|title:Optional. Title of the audio as defined by sender or by audio tags|mime_type:Optional. MIME type of the file as defined by sender|file_size:Optional. File size -pyrogram.document#b0700007 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Document; // Docs: Document:This object represents a general file (as opposed to photos, voice messages and audio files).|file_id:Unique file identifier|thumb:Optional. Document thumbnail as defined by sender|file_name:Optional. Original filename as defined by sender|mime_type:Optional. MIME type of the file as defined by sender|file_size:Optional. File size -pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; // Docs: Video:This object represents a video file.|file_id:Unique identifier for this file|width:Video width as defined by sender|height:Video height as defined by sender|duration:Duration of the video in seconds as defined by sender|thumb:Optional. Video thumbnail|mime_type:Optional. Mime type of a file as defined by sender|file_size:Optional. File size -pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; // Docs: Voice:This object represents a voice note.|file_id:Unique identifier for this file|duration:Duration of the audio in seconds as defined by sender|mime_type:Optional. MIME type of the file as defined by sender|file_size:Optional. File size -pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; // Docs: VideoNote:This object represents a video message (available in Telegram apps as of v.4.0).|file_id:Unique identifier for this file|length:Video width and height as defined by sender|duration:Duration of the video in seconds as defined by sender|thumb:Optional. Video thumbnail|file_size:Optional. File size -pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact; // Docs: Contact:This object represents a phone contact.|phone_number:Contact's phone number|first_name:Contact's first name|last_name:Optional. Contact's last name|user_id:Optional. Contact's user identifier in Telegram -pyrogram.location#b0700012 longitude:double latitude:double = pyrogram.Location; // Docs: Location:This object represents a point on the map.|longitude:Longitude as defined by sender|latitude:Latitude as defined by sender -pyrogram.venue#b0700013 flags:# location:Location title:string address:string foursquare_id:flags.0?string = pyrogram.Venue; // Docs: Venue:This object represents a venue.|location:Venue location|title:Name of the venue|address:Address of the venue|foursquare_id:Optional. Foursquare identifier of the venue -pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector> = pyrogram.UserProfilePhotos; // Docs: UserProfilePhotos:This object represent a user's profile pictures.|total_count:Total number of profile pictures the target user has|photos:Requested profile pictures (in up to 4 sizes each) -pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto; // Docs: ChatPhoto:This object represents a chat photo.|small_file_id:Unique file identifier of small (160x160) chat photo. This file_id can be used only for photo download.|big_file_id:Unique file identifier of big (640x640) chat photo. This file_id can be used only for photo download. -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; // Docs: ChatMember:This object contains information about one member of a chat.|user:Information about the user|status:The member's status in the chat. Can be "creator", "administrator", "member", "restricted", "left" or "kicked"|until_date:Optional. Restricted and kicked only. Date when restrictions will be lifted for this user, unix time|can_be_edited:Optional. Administrators only. True, if the bot is allowed to edit administrator privileges of that user|can_change_info:Optional. Administrators only. True, if the administrator can change the chat title, photo and other settings|can_post_messages:Optional. Administrators only. True, if the administrator can post in the channel, channels only|can_edit_messages:Optional. Administrators only. True, if the administrator can edit messages of other users and can pin messages, channels only|can_delete_messages:Optional. Administrators only. True, if the administrator can delete messages of other users|can_invite_users:Optional. Administrators only. True, if the administrator can invite new users to the chat|can_restrict_members:Optional. Administrators only. True, if the administrator can restrict, ban or unban chat members|can_pin_messages:Optional. Administrators only. True, if the administrator can pin messages, supergroups only|can_promote_members:Optional. Administrators only. True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by the user)|can_send_messages:Optional. Restricted only. True, if the user can send text messages, contacts, locations and venues|can_send_media_messages:Optional. Restricted only. True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages|can_send_other_messages:Optional. Restricted only. True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages|can_add_web_page_previews:Optional. Restricted only. True, if user may add web page previews to his messages, implies can_send_media_messages +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; // Docs: Update§This object represents an incoming update.At most one of the optional parameters can be present in any given update.|update_id§The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you're using Webhooks, since it allows you to ignore repeated updates or to restore the correct update sequence, should they get out of order. If there are no new updates for at least a week, then identifier of the next update will be chosen randomly instead of sequentially.|message§Optional. New incoming message of any kind — text, photo, sticker, etc.|edited_message§Optional. New version of a message that is known to the bot and was edited|channel_post§Optional. New incoming channel post of any kind — text, photo, sticker, etc.|edited_channel_post§Optional. New version of a channel post that is known to the bot and was edited|inline_query§Optional. New incoming inline query|chosen_inline_result§Optional. The result of an inline query that was chosen by a user and sent to their chat partner. Please see our documentation on the feedback collecting for details on how to enable these updates for your bot.|callback_query§Optional. New incoming callback query|shipping_query§Optional. New incoming shipping query. Only for invoices with flexible price|pre_checkout_query§Optional. New incoming pre-checkout query. Contains full information about checkout +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 = pyrogram.User; // Docs: User§This object represents a Telegram user or bot.|id§Unique identifier for this user or bot|is_bot§True, if this user is a bot|first_name§User's or bot's first name|last_name§Optional. User's or bot's last name|username§Optional. User's or bot's username|language_code§Optional. IETF language tag of the user's language +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; // Docs: Chat§This object represents a chat.|id§Unique identifier for this chat. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|type§Type of chat, can be either "private", "group", "supergroup" or "channel"|title§Optional. Title, for supergroups, channels and group chats|username§Optional. Username, for private chats, supergroups and channels if available|first_name§Optional. First name of the other party in a private chat|last_name§Optional. Last name of the other party in a private chat|all_members_are_administrators§Optional. True if a group has 'All Members Are Admins' enabled.|photo§Optional. Chat photo. Returned only in getChat.|description§Optional. Description, for supergroups and channel chats. Returned only in getChat.|invite_link§Optional. Chat invite link, for supergroups and channel chats. Returned only in getChat.|pinned_message§Optional. Pinned message, for supergroups and channel chats. Returned only in getChat.|sticker_set_name§Optional. For supergroups, name of group sticker set. Returned only in getChat.|can_set_sticker_set§Optional. True, if the bot can change the group sticker set. Returned only in getChat. +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 = pyrogram.Message; // Docs: Message§This object represents a message.|message_id§Unique message identifier inside this chat|from_user§Optional. Sender, empty for messages sent to channels|date§Date the message was sent in Unix time|chat§Conversation the message belongs to|forward_from§Optional. For forwarded messages, sender of the original message|forward_from_chat§Optional. For messages forwarded from channels, information about the original channel|forward_from_message_id§Optional. For messages forwarded from channels, identifier of the original message in the channel|forward_signature§Optional. For messages forwarded from channels, signature of the post author if present|forward_date§Optional. For forwarded messages, date the original message was sent in Unix time|reply_to_message§Optional. For replies, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply.|edit_date§Optional. Date the message was last edited in Unix time|media_group_id§Optional. The unique identifier of a media message group this message belongs to|author_signature§Optional. Signature of the post author for messages in channels|text§Optional. For text messages, the actual UTF-8 text of the message, 0-4096 characters.|entities§Optional. For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text|caption_entities§Optional. For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption|audio§Optional. Message is an audio file, information about the file|document§Optional. Message is a general file, information about the file|game§Optional. Message is a game, information about the game. More about games|photo§Optional. Message is a photo, available sizes of the photo|sticker§Optional. Message is a sticker, information about the sticker|video§Optional. Message is a video, information about the video|voice§Optional. Message is a voice message, information about the file|video_note§Optional. Message is a video note, information about the video message|caption§Optional. Caption for the audio, document, photo, video or voice, 0-200 characters|contact§Optional. Message is a shared contact, information about the contact|location§Optional. Message is a shared location, information about the location|venue§Optional. Message is a venue, information about the venue|new_chat_members§Optional. New members that were added to the group or supergroup and information about them (the bot itself may be one of these members)|left_chat_member§Optional. A member was removed from the group, information about them (this member may be the bot itself)|new_chat_title§Optional. A chat title was changed to this value|new_chat_photo§Optional. A chat photo was change to this value|delete_chat_photo§Optional. Service message: the chat photo was deleted|group_chat_created§Optional. Service message: the group has been created|supergroup_chat_created§Optional. Service message: the supergroup has been created. This field can't be received in a message coming through updates, because bot can't be a member of a supergroup when it is created. It can only be found in reply_to_message if someone replies to a very first message in a directly created supergroup.|channel_chat_created§Optional. Service message: the channel has been created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in reply_to_message if someone replies to a very first message in a channel.|migrate_to_chat_id§Optional. The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|migrate_from_chat_id§Optional. The supergroup has been migrated from a group with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|pinned_message§Optional. Specified message was pinned. Note that the Message object in this field will not contain further reply_to_message fields even if it is itself a reply.|invoice§Optional. Message is an invoice for a payment, information about the invoice. More about payments|successful_payment§Optional. Message is a service message about a successful payment, information about the payment. More about payments|connected_website§Optional. The domain name of the website on which the user has logged in. More about Telegram Login +pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; // Docs: MessageEntity§This object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc.|type§Type of the entity. Can be mention (@username), hashtag, bot_command, url, email, bold (bold text), italic (italic text), code (monowidth string), pre (monowidth block), text_link (for clickable text URLs), text_mention (for users without usernames)|offset§Offset in UTF-16 code units to the start of the entity|length§Length of the entity in UTF-16 code units|url§Optional. For "text_link" only, url that will be opened after user taps on the text|user§Optional. For "text_mention" only, the mentioned user +pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; // Docs: PhotoSize§This object represents one size of a photo or a file / sticker thumbnail.|file_id§Unique identifier for this file|width§Photo width|height§Photo height|file_size§Optional. File size +pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; // Docs: Audio§This object represents an audio file to be treated as music by the Telegram clients.|file_id§Unique identifier for this file|duration§Duration of the audio in seconds as defined by sender|performer§Optional. Performer of the audio as defined by sender or by audio tags|title§Optional. Title of the audio as defined by sender or by audio tags|mime_type§Optional. MIME type of the file as defined by sender|file_size§Optional. File size +pyrogram.document#b0700007 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Document; // Docs: Document§This object represents a general file (as opposed to photos, voice messages and audio files).|file_id§Unique file identifier|thumb§Optional. Document thumbnail as defined by sender|file_name§Optional. Original filename as defined by sender|mime_type§Optional. MIME type of the file as defined by sender|file_size§Optional. File size +pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; // Docs: Video§This object represents a video file.|file_id§Unique identifier for this file|width§Video width as defined by sender|height§Video height as defined by sender|duration§Duration of the video in seconds as defined by sender|thumb§Optional. Video thumbnail|mime_type§Optional. Mime type of a file as defined by sender|file_size§Optional. File size +pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; // Docs: Voice§This object represents a voice note.|file_id§Unique identifier for this file|duration§Duration of the audio in seconds as defined by sender|mime_type§Optional. MIME type of the file as defined by sender|file_size§Optional. File size +pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; // Docs: VideoNote§This object represents a video message (available in Telegram apps as of v.4.0).|file_id§Unique identifier for this file|length§Video width and height as defined by sender|duration§Duration of the video in seconds as defined by sender|thumb§Optional. Video thumbnail|file_size§Optional. File size +pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact; // Docs: Contact§This object represents a phone contact.|phone_number§Contact's phone number|first_name§Contact's first name|last_name§Optional. Contact's last name|user_id§Optional. Contact's user identifier in Telegram +pyrogram.location#b0700012 longitude:double latitude:double = pyrogram.Location; // Docs: Location§This object represents a point on the map.|longitude§Longitude as defined by sender|latitude§Latitude as defined by sender +pyrogram.venue#b0700013 flags:# location:Location title:string address:string foursquare_id:flags.0?string = pyrogram.Venue; // Docs: Venue§This object represents a venue.|location§Venue location|title§Name of the venue|address§Address of the venue|foursquare_id§Optional. Foursquare identifier of the venue +pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector> = pyrogram.UserProfilePhotos; // Docs: UserProfilePhotos§This object represent a user's profile pictures.|total_count§Total number of profile pictures the target user has|photos§Requested profile pictures (in up to 4 sizes each) +pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto; // Docs: ChatPhoto§This object represents a chat photo.|small_file_id§Unique file identifier of small (160x160) chat photo. This file_id can be used only for photo download.|big_file_id§Unique file identifier of big (640x640) chat photo. This file_id can be used only for photo download. +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; // Docs: ChatMember§This object contains information about one member of a chat.|user§Information about the user|status§The member's status in the chat. Can be "creator", "administrator", "member", "restricted", "left" or "kicked"|until_date§Optional. Restricted and kicked only. Date when restrictions will be lifted for this user, unix time|can_be_edited§Optional. Administrators only. True, if the bot is allowed to edit administrator privileges of that user|can_change_info§Optional. Administrators only. True, if the administrator can change the chat title, photo and other settings|can_post_messages§Optional. Administrators only. True, if the administrator can post in the channel, channels only|can_edit_messages§Optional. Administrators only. True, if the administrator can edit messages of other users and can pin messages, channels only|can_delete_messages§Optional. Administrators only. True, if the administrator can delete messages of other users|can_invite_users§Optional. Administrators only. True, if the administrator can invite new users to the chat|can_restrict_members§Optional. Administrators only. True, if the administrator can restrict, ban or unban chat members|can_pin_messages§Optional. Administrators only. True, if the administrator can pin messages, supergroups only|can_promote_members§Optional. Administrators only. True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by the user)|can_send_messages§Optional. Restricted only. True, if the user can send text messages, contacts, locations and venues|can_send_media_messages§Optional. Restricted only. True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages|can_send_other_messages§Optional. Restricted only. True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages|can_add_web_page_previews§Optional. Restricted only. True, if user may add web page previews to his messages, implies can_send_media_messages From 22c51fdd70b1e501b9707b7eef324c6b06c92ede Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 1 Apr 2018 14:34:29 +0200 Subject: [PATCH 146/285] Fix docstrings generation for Pyrogram types --- compiler/api/compiler.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 3e15a2bd..3d5af589 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -38,7 +38,7 @@ types_to_functions = {} constructors_to_functions = {} -def get_docstring_arg_type(t: str, is_list: bool = False): +def get_docstring_arg_type(t: str, is_list: bool = False, is_pyrogram_type: bool = False): if t in core_types: if t == "long": return ":obj:`int` :obj:`64-bit`" @@ -58,13 +58,20 @@ def get_docstring_arg_type(t: str, is_list: bool = False): 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], is_list=True) + 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:`{0} `".format(i) + ":obj:`{1} `".format( + "pyrogram." if is_pyrogram_type else "", + i.lstrip("pyrogram.") + ) for i in t ) @@ -280,7 +287,7 @@ def start(): docstring_args.append( "{} ({}{}):\n {}\n".format( arg_name, - get_docstring_arg_type(arg_type), + 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(".") + ".") ) From 124ec403007fb53cf7f080bc0fa5f80d2857f522 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 3 Apr 2018 10:54:29 +0200 Subject: [PATCH 147/285] Inherit again from Object --- compiler/api/template/pyrogram.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/api/template/pyrogram.txt b/compiler/api/template/pyrogram.txt index f5f7a96a..adbe4151 100644 --- a/compiler/api/template/pyrogram.txt +++ b/compiler/api/template/pyrogram.txt @@ -1,7 +1,8 @@ {notice} +from pyrogram.api.core import Object -class {class_name}: +class {class_name}(Object): """{docstring_args} """ ID = {object_id} From 88292cf7d6231ccfe3177a698f6d5a01d78f01dc Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 3 Apr 2018 14:44:24 +0200 Subject: [PATCH 148/285] Implement __bool__ --- pyrogram/api/core/object.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyrogram/api/core/object.py b/pyrogram/api/core/object.py index a74eb876..24c1dcf1 100644 --- a/pyrogram/api/core/object.py +++ b/pyrogram/api/core/object.py @@ -37,6 +37,9 @@ class Object: def __str__(self) -> str: return dumps(self, cls=Encoder, indent=4) + def __bool__(self) -> bool: + return True + def __eq__(self, other) -> bool: return self.__dict__ == other.__dict__ From 6201f6b1f71c8e8b12d352d464248b152465bd4a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 20:15:59 +0200 Subject: [PATCH 149/285] Add a bunch of TODOs --- pyrogram/client/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index b353250b..73247722 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -15,6 +15,7 @@ ENTITIES = { types.MessageEntityCode.ID: "code", types.MessageEntityPre.ID: "pre", types.MessageEntityTextUrl.ID: "text_link", + # TODO: text_mention } @@ -83,6 +84,7 @@ def parse_channel_chat(channel: types.Channel): ) +# TODO: Reorganize code, maybe split parts as well def parse_message(message: types.Message, users: dict, chats: dict): entities = parse_entities(message.entities) From 7ba523600e244da0c7840e84553180d5d5b0284a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 20:21:07 +0200 Subject: [PATCH 150/285] Handle Location type --- pyrogram/client/utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 73247722..45d15489 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -107,6 +107,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): forward_signature = forward_header.post_author photo = None + location = None media = message.media @@ -149,6 +150,14 @@ def parse_message(message: types.Message, users: dict, chats: dict): photo_sizes.append(photo_size) photo = photo_sizes + elif isinstance(media, types.MessageMediaGeo): + geo_point = media.geo + + if isinstance(geo_point, types.GeoPoint): + location = pyrogram.Location( + longitude=geo_point.long, + latitude=geo_point.lat + ) return pyrogram.Message( message_id=message.id, @@ -166,7 +175,8 @@ def parse_message(message: types.Message, users: dict, chats: dict): forward_signature=forward_signature, forward_date=forward_date, edit_date=message.edit_date, - photo=photo + photo=photo, + location=location ) From 156afd980554ff179ac03372401076ee729aa270 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 20:28:05 +0200 Subject: [PATCH 151/285] Handle Contact type --- pyrogram/client/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 45d15489..98f605a5 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -108,6 +108,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): photo = None location = None + contact = None media = message.media @@ -158,6 +159,13 @@ def parse_message(message: types.Message, users: dict, chats: dict): longitude=geo_point.long, latitude=geo_point.lat ) + elif isinstance(media, types.MessageMediaContact): + contact = pyrogram.Contact( + phone_number=media.phone_number, + first_name=media.first_name, + last_name=media.last_name, + user_id=media.user_id + ) return pyrogram.Message( message_id=message.id, @@ -176,7 +184,8 @@ def parse_message(message: types.Message, users: dict, chats: dict): forward_date=forward_date, edit_date=message.edit_date, photo=photo, - location=location + location=location, + contact=contact ) From c49c8c0ce665b2f5e66f99e009490547451ca4e8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 22:23:20 +0200 Subject: [PATCH 152/285] Handle Audio type --- pyrogram/client/utils.py | 70 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 98f605a5..196e795b 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -84,6 +84,35 @@ def parse_channel_chat(channel: types.Channel): ) +def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize): + 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( + " Date: Wed, 4 Apr 2018 22:24:09 +0200 Subject: [PATCH 153/285] Handle Video and VideoNote type --- pyrogram/client/utils.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 196e795b..1f24373d 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -139,6 +139,8 @@ def parse_message(message: types.Message, users: dict, chats: dict): location = None contact = None audio = None + video = None + video_note = None media = message.media @@ -221,6 +223,43 @@ def parse_message(message: types.Message, users: dict, chats: dict): mime_type=doc.mime_type, file_size=doc.size ) + elif types.DocumentAttributeVideo in attributes: + video_attributes = attributes[types.DocumentAttributeVideo] + + if video_attributes.round_message: + video_note = pyrogram.VideoNote( + file_id=encode( + pack( + " Date: Wed, 4 Apr 2018 22:27:05 +0200 Subject: [PATCH 154/285] Handle Voice type --- pyrogram/client/utils.py | 50 +++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 1f24373d..93f8b4ee 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -139,6 +139,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): location = None contact = None audio = None + voice = None video = None video_note = None @@ -207,22 +208,38 @@ def parse_message(message: types.Message, users: dict, chats: dict): if types.DocumentAttributeAudio in attributes: audio_attributes = attributes[types.DocumentAttributeAudio] - audio = pyrogram.Audio( - file_id=encode( - pack( - " Date: Wed, 4 Apr 2018 22:37:22 +0200 Subject: [PATCH 155/285] Add Sticker type --- compiler/api/source/pyrogram.tl | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 491e890c..0f580808 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -19,3 +19,4 @@ pyrogram.venue#b0700013 flags:# location:Location title:string address:string fo pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector> = pyrogram.UserProfilePhotos; // Docs: UserProfilePhotos§This object represent a user's profile pictures.|total_count§Total number of profile pictures the target user has|photos§Requested profile pictures (in up to 4 sizes each) pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto; // Docs: ChatPhoto§This object represents a chat photo.|small_file_id§Unique file identifier of small (160x160) chat photo. This file_id can be used only for photo download.|big_file_id§Unique file identifier of big (640x640) chat photo. This file_id can be used only for photo download. 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; // Docs: ChatMember§This object contains information about one member of a chat.|user§Information about the user|status§The member's status in the chat. Can be "creator", "administrator", "member", "restricted", "left" or "kicked"|until_date§Optional. Restricted and kicked only. Date when restrictions will be lifted for this user, unix time|can_be_edited§Optional. Administrators only. True, if the bot is allowed to edit administrator privileges of that user|can_change_info§Optional. Administrators only. True, if the administrator can change the chat title, photo and other settings|can_post_messages§Optional. Administrators only. True, if the administrator can post in the channel, channels only|can_edit_messages§Optional. Administrators only. True, if the administrator can edit messages of other users and can pin messages, channels only|can_delete_messages§Optional. Administrators only. True, if the administrator can delete messages of other users|can_invite_users§Optional. Administrators only. True, if the administrator can invite new users to the chat|can_restrict_members§Optional. Administrators only. True, if the administrator can restrict, ban or unban chat members|can_pin_messages§Optional. Administrators only. True, if the administrator can pin messages, supergroups only|can_promote_members§Optional. Administrators only. True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by the user)|can_send_messages§Optional. Restricted only. True, if the user can send text messages, contacts, locations and venues|can_send_media_messages§Optional. Restricted only. True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages|can_send_other_messages§Optional. Restricted only. True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages|can_add_web_page_previews§Optional. Restricted only. True, if user may add web page previews to his messages, implies can_send_media_messages +pyrogram.sticker#b0700017 flags:# file_id:string width:int height:int thumb:flags.0?PhotoSize emoji:flags.1?string set_name:flags.2?string mask_position:flags.3?MaskPosition file_size:flags.4?int = pyrogram.Sticker; // Docs: Sticker§This object represents a sticker.|file_id§Unique identifier for this file|width§Sticker width|height§Sticker height|thumb§Optional. Sticker thumbnail in the .webp or .jpg format|emoji§Optional. Emoji associated with the sticker|set_name§Optional. Name of the sticker set to which the sticker belongs|mask_position§Optional. For mask stickers, the position where the mask should be placed|file_size§Optional. File size From d5fe82687cb60815eb4fe1b56aeafa934bd4b91a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 22:37:40 +0200 Subject: [PATCH 156/285] Handle Sticker type --- pyrogram/client/utils.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 93f8b4ee..6a5042a8 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -142,6 +142,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): voice = None video = None video_note = None + sticker = None media = message.media @@ -277,6 +278,25 @@ def parse_message(message: types.Message, users: dict, chats: dict): mime_type=doc.mime_type, file_size=doc.size ) + elif types.DocumentAttributeSticker in attributes: + image_size_attribute = attributes[types.DocumentAttributeImageSize] + + sticker = pyrogram.Sticker( + file_id=encode( + pack( + " Date: Wed, 4 Apr 2018 22:42:30 +0200 Subject: [PATCH 157/285] Handle media_group_id --- pyrogram/client/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 6a5042a8..6b98ad93 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -314,6 +314,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): forward_signature=forward_signature, forward_date=forward_date, edit_date=message.edit_date, + media_group_id=message.grouped_id, photo=photo, location=location, contact=contact, From 2acb38649d84f2231239f769f1117a9acc0e8d78 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 22:47:52 +0200 Subject: [PATCH 158/285] Add some TODOs --- pyrogram/client/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 6b98ad93..80c9bd11 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -364,10 +364,14 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic new_chat_members=new_chat_members, left_chat_member=left_chat_member, new_chat_title=new_chat_title, + # TODO: new_chat_photo delete_chat_photo=delete_chat_photo, migrate_to_chat_id=migrate_to_chat_id, migrate_from_chat_id=migrate_from_chat_id, group_chat_created=group_chat_created + # TODO: supergroup_chat_created + # TODO: channel_chat_created + # TODO: pinned_message ) From f1a8cd1038c2fcec885501df17f9132e5bade805 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 22:48:14 +0200 Subject: [PATCH 159/285] Rename to image_size_attributes --- pyrogram/client/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 80c9bd11..283dc48f 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -279,7 +279,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): file_size=doc.size ) elif types.DocumentAttributeSticker in attributes: - image_size_attribute = attributes[types.DocumentAttributeImageSize] + image_size_attributes = attributes[types.DocumentAttributeImageSize] sticker = pyrogram.Sticker( file_id=encode( @@ -291,8 +291,8 @@ def parse_message(message: types.Message, users: dict, chats: dict): doc.access_hash ) ), - width=image_size_attribute.w, - height=image_size_attribute.h, + width=image_size_attributes.w, + height=image_size_attributes.h, thumb=parse_thumb(doc.thumb), # TODO: Emoji, set_name and mask_position file_size=doc.size, From 2fcd8ea54e8e72ef3ac25f259e2964c1ccc00c9c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 4 Apr 2018 23:59:30 +0200 Subject: [PATCH 160/285] Handle GIF and Document type --- pyrogram/client/utils.py | 46 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 283dc48f..29b7e038 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -143,6 +143,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): video = None video_note = None sticker = None + document = None media = message.media @@ -241,7 +242,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): mime_type=doc.mime_type, file_size=doc.size ) - elif types.DocumentAttributeVideo in attributes: + elif types.DocumentAttributeVideo in attributes and types.DocumentAttributeAnimated not in attributes: video_attributes = attributes[types.DocumentAttributeVideo] if video_attributes.round_message: @@ -297,6 +298,46 @@ def parse_message(message: types.Message, users: dict, chats: dict): # TODO: Emoji, set_name and mask_position file_size=doc.size, ) + elif types.DocumentAttributeAnimated in attributes: + document = pyrogram.Document( + file_id=encode( + pack( + " Date: Thu, 5 Apr 2018 00:19:13 +0200 Subject: [PATCH 161/285] Move pyrogram types import on the top --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 53686f94..41c743e3 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -26,10 +26,10 @@ __license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)" __version__ = "0.6.5" from .api.errors import Error +from .api.types.pyrogram import * from .client import ChatAction from .client import Client from .client import ParseMode from .client.input_media import InputMedia from .client.input_phone_contact import InputPhoneContact from .client import Emoji -from .api.types.pyrogram import * From feece7e633f3010c22c85b73847628d8c15b735a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 00:23:38 +0200 Subject: [PATCH 162/285] Add return types (function annotations) --- pyrogram/client/utils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 29b7e038..da86a104 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -19,7 +19,7 @@ ENTITIES = { } -def parse_entities(entities: list): +def parse_entities(entities: list) -> list: output_entities = [] for entity in entities: @@ -36,7 +36,7 @@ def parse_entities(entities: list): return output_entities -def parse_user(user: types.User): +def parse_user(user: types.User) -> pyrogram.User or None: return pyrogram.User( id=user.id, is_bot=user.bot, @@ -47,7 +47,7 @@ def parse_user(user: types.User): ) if user else None -def parse_chat(message: types.Message, users: dict, chats: dict): +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): @@ -56,7 +56,7 @@ def parse_chat(message: types.Message, users: dict, chats: dict): return parse_channel_chat(chats[message.to_id.channel_id]) -def parse_user_chat(user: types.User): +def parse_user_chat(user: types.User) -> pyrogram.Chat: return pyrogram.Chat( id=user.id, type="private", @@ -66,7 +66,7 @@ def parse_user_chat(user: types.User): ) -def parse_chat_chat(chat: types.Chat): +def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: return pyrogram.Chat( id=-chat.id, type="group", @@ -75,7 +75,7 @@ def parse_chat_chat(chat: types.Chat): ) -def parse_channel_chat(channel: types.Channel): +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", @@ -84,7 +84,7 @@ def parse_channel_chat(channel: types.Channel): ) -def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize): +def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): loc = thumb.location @@ -114,7 +114,7 @@ def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize): # TODO: Reorganize code, maybe split parts as well -def parse_message(message: types.Message, users: dict, chats: dict): +def parse_message(message: types.Message, users: dict, chats: dict) -> pyrogram.Message: entities = parse_entities(message.entities) forward_from = None @@ -368,7 +368,7 @@ def parse_message(message: types.Message, users: dict, chats: dict): ) -def parse_message_service(message: types.MessageService, users: dict, chats: dict): +def parse_message_service(message: types.MessageService, users: dict, chats: dict) -> pyrogram.Message or None: action = message.action new_chat_members = None @@ -417,7 +417,7 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic ) -def decode(s): +def decode(s: str) -> bytes: s = b64decode(s + "=" * (-len(s) % 4), "-_") r = b"" @@ -436,7 +436,7 @@ def decode(s): return r -def encode(s): +def encode(s: bytes) -> str: r = b"" n = 0 From 4adc55a0704df7e1a9feea4ebba90047aa63d56b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 08:22:27 +0200 Subject: [PATCH 163/285] Move GIF handling before Video to avoid an extra check --- pyrogram/client/utils.py | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index da86a104..43dba6d4 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -242,7 +242,27 @@ def parse_message(message: types.Message, users: dict, chats: dict) -> pyrogram. mime_type=doc.mime_type, file_size=doc.size ) - elif types.DocumentAttributeVideo in attributes and types.DocumentAttributeAnimated not in attributes: + elif types.DocumentAttributeAnimated in attributes: + document = pyrogram.Document( + file_id=encode( + pack( + " pyrogram. # TODO: Emoji, set_name and mask_position file_size=doc.size, ) - elif types.DocumentAttributeAnimated in attributes: - document = pyrogram.Document( - file_id=encode( - pack( - " Date: Thu, 5 Apr 2018 08:26:50 +0200 Subject: [PATCH 164/285] Don't return None This will break Message Pin handling --- pyrogram/client/client.py | 3 --- pyrogram/client/utils.py | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 290a8ba9..ebb87dd3 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -730,9 +730,6 @@ class Client: elif isinstance(message, types.MessageService): m = utils.parse_message_service(message, users, chats) - if m is None: - continue - if isinstance(message.action, types.MessageActionPinMessage): pm = self.get_messages(m.chat.id, [message.reply_to_msg_id]) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 43dba6d4..68af6b84 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -368,7 +368,7 @@ def parse_message(message: types.Message, users: dict, chats: dict) -> pyrogram. ) -def parse_message_service(message: types.MessageService, users: dict, chats: dict) -> pyrogram.Message or None: +def parse_message_service(message: types.MessageService, users: dict, chats: dict) -> pyrogram.Message: action = message.action new_chat_members = None @@ -395,8 +395,6 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic migrate_from_chat_id = action.chat_id elif isinstance(action, types.MessageActionChatCreate): group_chat_created = True - else: - return None return pyrogram.Message( message_id=message.id, @@ -413,7 +411,6 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic group_chat_created=group_chat_created # TODO: supergroup_chat_created # TODO: channel_chat_created - # TODO: pinned_message ) From 195cd22ebb0360c70f58b913b11ec94ed779a4b3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 10:49:20 +0200 Subject: [PATCH 165/285] Better handling of reply and pin messages --- pyrogram/client/client.py | 32 ++------------------------ pyrogram/client/utils.py | 48 +++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index ebb87dd3..9afd7b53 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -712,37 +712,9 @@ class Client: message = update.message if isinstance(message, types.Message): - m = utils.parse_message(message, users, chats) - - if message.reply_to_msg_id: - rm = self.get_messages(m.chat.id, [message.reply_to_msg_id]) - - message = rm.messages[0] - - if isinstance(message, types.Message): - m.reply_to_message = utils.parse_message( - message, - {i.id: i for i in rm.users}, - {i.id: i for i in rm.chats} - ) - else: - continue + m = utils.parse_message(self, message, users, chats) elif isinstance(message, types.MessageService): - m = utils.parse_message_service(message, users, chats) - - if isinstance(message.action, types.MessageActionPinMessage): - pm = self.get_messages(m.chat.id, [message.reply_to_msg_id]) - - message = pm.messages[0] - - if isinstance(message, types.Message): - m.pinned_message = utils.parse_message( - message, - {i.id: i for i in pm.users}, - {i.id: i for i in pm.chats} - ) - else: - continue + m = utils.parse_message_service(self, message, users, chats) else: continue else: diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 68af6b84..5fe04ee5 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -114,7 +114,13 @@ def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.Pho # TODO: Reorganize code, maybe split parts as well -def parse_message(message: types.Message, users: dict, chats: dict) -> pyrogram.Message: +def parse_message( + client, + message: types.Message, + users: dict, + chats: dict, + replies: int = 1 +) -> pyrogram.Message: entities = parse_entities(message.entities) forward_from = None @@ -339,7 +345,7 @@ def parse_message(message: types.Message, users: dict, chats: dict) -> pyrogram. file_size=doc.size ) - return pyrogram.Message( + m = pyrogram.Message( message_id=message.id, date=message.date, chat=parse_chat(message, users, chats), @@ -367,8 +373,27 @@ def parse_message(message: types.Message, users: dict, chats: dict) -> pyrogram. document=document ) + if message.reply_to_msg_id and replies: + reply_to_message = client.get_messages(m.chat.id, [message.reply_to_msg_id]) -def parse_message_service(message: types.MessageService, users: dict, chats: dict) -> pyrogram.Message: + message = reply_to_message.messages[0] + users = {i.id: i for i in reply_to_message.users} + chats = {i.id: i for i in reply_to_message.chats} + + if isinstance(message, types.Message): + m.reply_to_message = parse_message(client, message, users, chats, replies - 1) + elif isinstance(message, types.MessageService): + m.reply_to_message = parse_message_service(client, message, users, chats) + + return m + + +def parse_message_service( + client, + message: types.MessageService, + users: dict, + chats: dict +) -> pyrogram.Message: action = message.action new_chat_members = None @@ -396,7 +421,7 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic elif isinstance(action, types.MessageActionChatCreate): group_chat_created = True - return pyrogram.Message( + m = pyrogram.Message( message_id=message.id, date=message.date, chat=parse_chat(message, users, chats), @@ -413,6 +438,21 @@ def parse_message_service(message: types.MessageService, users: dict, chats: dic # TODO: channel_chat_created ) + if isinstance(action, types.MessageActionPinMessage): + pin_message = client.get_messages(m.chat.id, [message.reply_to_msg_id]) + + message = pin_message.messages[0] + users = {i.id: i for i in pin_message.users} + chats = {i.id: i for i in pin_message.chats} + + if isinstance(message, types.Message): + m.pinned_message = parse_message(client, message, users, chats) + elif isinstance(message, types.MessageService): + # TODO: We can't pin a service message, can we? + m.pinned_message = parse_message_service(client, message, users, chats) + + return m + def decode(s: str) -> bytes: s = b64decode(s + "=" * (-len(s) % 4), "-_") From 7483d3df37d4916de64dec4a50d1223b44c5ba1c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 10:59:12 +0200 Subject: [PATCH 166/285] Use correct chat ids --- pyrogram/client/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 5fe04ee5..25511426 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -431,8 +431,8 @@ def parse_message_service( new_chat_title=new_chat_title, # TODO: new_chat_photo delete_chat_photo=delete_chat_photo, - migrate_to_chat_id=migrate_to_chat_id, - migrate_from_chat_id=migrate_from_chat_id, + migrate_to_chat_id=int("-100" + str(migrate_to_chat_id)) if migrate_to_chat_id else None, + migrate_from_chat_id=-migrate_from_chat_id if migrate_from_chat_id else None, group_chat_created=group_chat_created # TODO: supergroup_chat_created # TODO: channel_chat_created From 65e3852706b73fe352093da3a8f5dd7a92f87002 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 10:59:44 +0200 Subject: [PATCH 167/285] Add channel_chat_created field --- pyrogram/client/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 25511426..f6a3e5c2 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -403,6 +403,7 @@ def parse_message_service( migrate_to_chat_id = None migrate_from_chat_id = None group_chat_created = None + channel_chat_created = None if isinstance(action, types.MessageActionChatAddUser): new_chat_members = [parse_user(users[i]) for i in action.users] @@ -420,6 +421,8 @@ def parse_message_service( 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 m = pyrogram.Message( message_id=message.id, @@ -433,9 +436,9 @@ def parse_message_service( delete_chat_photo=delete_chat_photo, migrate_to_chat_id=int("-100" + str(migrate_to_chat_id)) if migrate_to_chat_id else None, migrate_from_chat_id=-migrate_from_chat_id if migrate_from_chat_id else None, - group_chat_created=group_chat_created + group_chat_created=group_chat_created, + channel_chat_created=channel_chat_created # TODO: supergroup_chat_created - # TODO: channel_chat_created ) if isinstance(action, types.MessageActionPinMessage): From fad0e7a26d4084ec252cbe2bafd65fd092a6cbbd Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 11:17:40 +0200 Subject: [PATCH 168/285] Add new_chat_photo field --- pyrogram/client/utils.py | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index f6a3e5c2..60c11978 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -404,6 +404,7 @@ def parse_message_service( 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] @@ -423,6 +424,44 @@ def parse_message_service( 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( + " Date: Thu, 5 Apr 2018 20:18:04 +0200 Subject: [PATCH 169/285] Keep InputMediaPhoto separated --- pyrogram/client/input_media_photo.py | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pyrogram/client/input_media_photo.py diff --git a/pyrogram/client/input_media_photo.py b/pyrogram/client/input_media_photo.py new file mode 100644 index 00000000..f1fe6acb --- /dev/null +++ b/pyrogram/client/input_media_photo.py @@ -0,0 +1,44 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + + +class InputMediaPhoto: + """This object represents a photo to be sent inside an album. + It is intended to be used with :obj:`send_media_group `. + + Args: + media (:obj:`str`): + Photo file to send. + Pass a file path as string to send a photo that exists on your local machine. + + caption (:obj:`str`): + Caption of the photo to be sent, 0-200 characters + + parse_mode (:obj:`str`): + Use :obj:`MARKDOWN ` or :obj:`HTML ` + if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption. + Defaults to Markdown. + """ + + def __init__(self, + media: str, + caption: str = "", + parse_mode: str = ""): + self.media = media + self.caption = caption + self.parse_mode = parse_mode From 35938869171d183878619e40bb00162faeb3c4b4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 20:19:37 +0200 Subject: [PATCH 170/285] Keep InputMediaVideo separated as well --- pyrogram/client/input_media_video.py | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 pyrogram/client/input_media_video.py diff --git a/pyrogram/client/input_media_video.py b/pyrogram/client/input_media_video.py new file mode 100644 index 00000000..c14767e5 --- /dev/null +++ b/pyrogram/client/input_media_video.py @@ -0,0 +1,64 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + + +class InputMediaVideo: + """This object represents a video to be sent inside an album. + It is intended to be used with :obj:`send_media_group `. + + Args: + media (:obj:`str`): + Video file to send. + Pass a file path as string to send a video that exists on your local machine. + + caption (:obj:`str`, optional): + Caption of the video to be sent, 0-200 characters + + parse_mode (:obj:`str`, optional): + Use :obj:`MARKDOWN ` or :obj:`HTML ` + if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption. + Defaults to Markdown. + + width (:obj:`int`, optional): + Video width. + + height (:obj:`int`, optional): + Video height. + + duration (:obj:`int`, optional): + Video duration. + + supports_streaming (:obj:`bool`, optional): + Pass True, if the uploaded video is suitable for streaming. + """ + + def __init__(self, + media: str, + caption: str = "", + parse_mode: str = "", + width: int = 0, + height: int = 0, + duration: int = 0, + supports_streaming: bool = True): + self.media = media + self.caption = caption + self.parse_mode = parse_mode + self.width = width + self.height = height + self.duration = duration + self.supports_streaming = supports_streaming From 1fe2b11a0a8856575c05d73c99919e1c4bbbd7dc Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 20:21:02 +0200 Subject: [PATCH 171/285] Update imports --- pyrogram/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 41c743e3..0d6595fa 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -30,6 +30,7 @@ from .api.types.pyrogram import * from .client import ChatAction from .client import Client from .client import ParseMode -from .client.input_media import InputMedia +from .client.input_media_photo import InputMediaPhoto +from .client.input_media_video import InputMediaVideo from .client.input_phone_contact import InputPhoneContact from .client import Emoji From 23c0d2b9ceb3890e0ce7fa2e853647f1929159ed Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 20:35:25 +0200 Subject: [PATCH 172/285] Handle Venue type --- pyrogram/client/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 60c11978..cf81c68d 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -144,6 +144,7 @@ def parse_message( photo = None location = None contact = None + venue = None audio = None voice = None video = None @@ -207,6 +208,16 @@ def parse_message( last_name=media.last_name, user_id=media.user_id ) + elif isinstance(media, types.MessageMediaVenue): + venue = pyrogram.Venue( + location=pyrogram.Location( + longitude=media.geo.long, + latitude=media.geo.lat + ), + title=media.title, + address=media.address, + foursquare_id=media.venue_id + ) elif isinstance(media, types.MessageMediaDocument): doc = media.document @@ -365,6 +376,7 @@ def parse_message( photo=photo, location=location, contact=contact, + venue=venue, audio=audio, voice=voice, video=video, From 3229d36556f0bf6e33edbd6221bde55f65cdb46d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 20:43:21 +0200 Subject: [PATCH 173/285] Remove docstrings for now --- compiler/api/source/pyrogram.tl | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 0f580808..e28b7c27 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -2,21 +2,21 @@ ---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; // Docs: Update§This object represents an incoming update.At most one of the optional parameters can be present in any given update.|update_id§The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you're using Webhooks, since it allows you to ignore repeated updates or to restore the correct update sequence, should they get out of order. If there are no new updates for at least a week, then identifier of the next update will be chosen randomly instead of sequentially.|message§Optional. New incoming message of any kind — text, photo, sticker, etc.|edited_message§Optional. New version of a message that is known to the bot and was edited|channel_post§Optional. New incoming channel post of any kind — text, photo, sticker, etc.|edited_channel_post§Optional. New version of a channel post that is known to the bot and was edited|inline_query§Optional. New incoming inline query|chosen_inline_result§Optional. The result of an inline query that was chosen by a user and sent to their chat partner. Please see our documentation on the feedback collecting for details on how to enable these updates for your bot.|callback_query§Optional. New incoming callback query|shipping_query§Optional. New incoming shipping query. Only for invoices with flexible price|pre_checkout_query§Optional. New incoming pre-checkout query. Contains full information about checkout -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 = pyrogram.User; // Docs: User§This object represents a Telegram user or bot.|id§Unique identifier for this user or bot|is_bot§True, if this user is a bot|first_name§User's or bot's first name|last_name§Optional. User's or bot's last name|username§Optional. User's or bot's username|language_code§Optional. IETF language tag of the user's language -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; // Docs: Chat§This object represents a chat.|id§Unique identifier for this chat. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|type§Type of chat, can be either "private", "group", "supergroup" or "channel"|title§Optional. Title, for supergroups, channels and group chats|username§Optional. Username, for private chats, supergroups and channels if available|first_name§Optional. First name of the other party in a private chat|last_name§Optional. Last name of the other party in a private chat|all_members_are_administrators§Optional. True if a group has 'All Members Are Admins' enabled.|photo§Optional. Chat photo. Returned only in getChat.|description§Optional. Description, for supergroups and channel chats. Returned only in getChat.|invite_link§Optional. Chat invite link, for supergroups and channel chats. Returned only in getChat.|pinned_message§Optional. Pinned message, for supergroups and channel chats. Returned only in getChat.|sticker_set_name§Optional. For supergroups, name of group sticker set. Returned only in getChat.|can_set_sticker_set§Optional. True, if the bot can change the group sticker set. Returned only in getChat. -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 = pyrogram.Message; // Docs: Message§This object represents a message.|message_id§Unique message identifier inside this chat|from_user§Optional. Sender, empty for messages sent to channels|date§Date the message was sent in Unix time|chat§Conversation the message belongs to|forward_from§Optional. For forwarded messages, sender of the original message|forward_from_chat§Optional. For messages forwarded from channels, information about the original channel|forward_from_message_id§Optional. For messages forwarded from channels, identifier of the original message in the channel|forward_signature§Optional. For messages forwarded from channels, signature of the post author if present|forward_date§Optional. For forwarded messages, date the original message was sent in Unix time|reply_to_message§Optional. For replies, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply.|edit_date§Optional. Date the message was last edited in Unix time|media_group_id§Optional. The unique identifier of a media message group this message belongs to|author_signature§Optional. Signature of the post author for messages in channels|text§Optional. For text messages, the actual UTF-8 text of the message, 0-4096 characters.|entities§Optional. For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text|caption_entities§Optional. For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption|audio§Optional. Message is an audio file, information about the file|document§Optional. Message is a general file, information about the file|game§Optional. Message is a game, information about the game. More about games|photo§Optional. Message is a photo, available sizes of the photo|sticker§Optional. Message is a sticker, information about the sticker|video§Optional. Message is a video, information about the video|voice§Optional. Message is a voice message, information about the file|video_note§Optional. Message is a video note, information about the video message|caption§Optional. Caption for the audio, document, photo, video or voice, 0-200 characters|contact§Optional. Message is a shared contact, information about the contact|location§Optional. Message is a shared location, information about the location|venue§Optional. Message is a venue, information about the venue|new_chat_members§Optional. New members that were added to the group or supergroup and information about them (the bot itself may be one of these members)|left_chat_member§Optional. A member was removed from the group, information about them (this member may be the bot itself)|new_chat_title§Optional. A chat title was changed to this value|new_chat_photo§Optional. A chat photo was change to this value|delete_chat_photo§Optional. Service message: the chat photo was deleted|group_chat_created§Optional. Service message: the group has been created|supergroup_chat_created§Optional. Service message: the supergroup has been created. This field can't be received in a message coming through updates, because bot can't be a member of a supergroup when it is created. It can only be found in reply_to_message if someone replies to a very first message in a directly created supergroup.|channel_chat_created§Optional. Service message: the channel has been created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in reply_to_message if someone replies to a very first message in a channel.|migrate_to_chat_id§Optional. The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|migrate_from_chat_id§Optional. The supergroup has been migrated from a group with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.|pinned_message§Optional. Specified message was pinned. Note that the Message object in this field will not contain further reply_to_message fields even if it is itself a reply.|invoice§Optional. Message is an invoice for a payment, information about the invoice. More about payments|successful_payment§Optional. Message is a service message about a successful payment, information about the payment. More about payments|connected_website§Optional. The domain name of the website on which the user has logged in. More about Telegram Login -pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; // Docs: MessageEntity§This object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc.|type§Type of the entity. Can be mention (@username), hashtag, bot_command, url, email, bold (bold text), italic (italic text), code (monowidth string), pre (monowidth block), text_link (for clickable text URLs), text_mention (for users without usernames)|offset§Offset in UTF-16 code units to the start of the entity|length§Length of the entity in UTF-16 code units|url§Optional. For "text_link" only, url that will be opened after user taps on the text|user§Optional. For "text_mention" only, the mentioned user -pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; // Docs: PhotoSize§This object represents one size of a photo or a file / sticker thumbnail.|file_id§Unique identifier for this file|width§Photo width|height§Photo height|file_size§Optional. File size -pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; // Docs: Audio§This object represents an audio file to be treated as music by the Telegram clients.|file_id§Unique identifier for this file|duration§Duration of the audio in seconds as defined by sender|performer§Optional. Performer of the audio as defined by sender or by audio tags|title§Optional. Title of the audio as defined by sender or by audio tags|mime_type§Optional. MIME type of the file as defined by sender|file_size§Optional. File size -pyrogram.document#b0700007 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Document; // Docs: Document§This object represents a general file (as opposed to photos, voice messages and audio files).|file_id§Unique file identifier|thumb§Optional. Document thumbnail as defined by sender|file_name§Optional. Original filename as defined by sender|mime_type§Optional. MIME type of the file as defined by sender|file_size§Optional. File size -pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; // Docs: Video§This object represents a video file.|file_id§Unique identifier for this file|width§Video width as defined by sender|height§Video height as defined by sender|duration§Duration of the video in seconds as defined by sender|thumb§Optional. Video thumbnail|mime_type§Optional. Mime type of a file as defined by sender|file_size§Optional. File size -pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; // Docs: Voice§This object represents a voice note.|file_id§Unique identifier for this file|duration§Duration of the audio in seconds as defined by sender|mime_type§Optional. MIME type of the file as defined by sender|file_size§Optional. File size -pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; // Docs: VideoNote§This object represents a video message (available in Telegram apps as of v.4.0).|file_id§Unique identifier for this file|length§Video width and height as defined by sender|duration§Duration of the video in seconds as defined by sender|thumb§Optional. Video thumbnail|file_size§Optional. File size -pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact; // Docs: Contact§This object represents a phone contact.|phone_number§Contact's phone number|first_name§Contact's first name|last_name§Optional. Contact's last name|user_id§Optional. Contact's user identifier in Telegram -pyrogram.location#b0700012 longitude:double latitude:double = pyrogram.Location; // Docs: Location§This object represents a point on the map.|longitude§Longitude as defined by sender|latitude§Latitude as defined by sender -pyrogram.venue#b0700013 flags:# location:Location title:string address:string foursquare_id:flags.0?string = pyrogram.Venue; // Docs: Venue§This object represents a venue.|location§Venue location|title§Name of the venue|address§Address of the venue|foursquare_id§Optional. Foursquare identifier of the venue -pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector> = pyrogram.UserProfilePhotos; // Docs: UserProfilePhotos§This object represent a user's profile pictures.|total_count§Total number of profile pictures the target user has|photos§Requested profile pictures (in up to 4 sizes each) -pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto; // Docs: ChatPhoto§This object represents a chat photo.|small_file_id§Unique file identifier of small (160x160) chat photo. This file_id can be used only for photo download.|big_file_id§Unique file identifier of big (640x640) chat photo. This file_id can be used only for photo download. -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; // Docs: ChatMember§This object contains information about one member of a chat.|user§Information about the user|status§The member's status in the chat. Can be "creator", "administrator", "member", "restricted", "left" or "kicked"|until_date§Optional. Restricted and kicked only. Date when restrictions will be lifted for this user, unix time|can_be_edited§Optional. Administrators only. True, if the bot is allowed to edit administrator privileges of that user|can_change_info§Optional. Administrators only. True, if the administrator can change the chat title, photo and other settings|can_post_messages§Optional. Administrators only. True, if the administrator can post in the channel, channels only|can_edit_messages§Optional. Administrators only. True, if the administrator can edit messages of other users and can pin messages, channels only|can_delete_messages§Optional. Administrators only. True, if the administrator can delete messages of other users|can_invite_users§Optional. Administrators only. True, if the administrator can invite new users to the chat|can_restrict_members§Optional. Administrators only. True, if the administrator can restrict, ban or unban chat members|can_pin_messages§Optional. Administrators only. True, if the administrator can pin messages, supergroups only|can_promote_members§Optional. Administrators only. True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by the user)|can_send_messages§Optional. Restricted only. True, if the user can send text messages, contacts, locations and venues|can_send_media_messages§Optional. Restricted only. True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages|can_send_other_messages§Optional. Restricted only. True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages|can_add_web_page_previews§Optional. Restricted only. True, if user may add web page previews to his messages, implies can_send_media_messages -pyrogram.sticker#b0700017 flags:# file_id:string width:int height:int thumb:flags.0?PhotoSize emoji:flags.1?string set_name:flags.2?string mask_position:flags.3?MaskPosition file_size:flags.4?int = pyrogram.Sticker; // Docs: Sticker§This object represents a sticker.|file_id§Unique identifier for this file|width§Sticker width|height§Sticker height|thumb§Optional. Sticker thumbnail in the .webp or .jpg format|emoji§Optional. Emoji associated with the sticker|set_name§Optional. Name of the sticker set to which the sticker belongs|mask_position§Optional. For mask stickers, the position where the mask should be placed|file_size§Optional. File size +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 = 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 = pyrogram.Message; +pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; +pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; +pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; +pyrogram.document#b0700007 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Document; +pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; +pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; +pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; +pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact; +pyrogram.location#b0700012 longitude:double latitude:double = pyrogram.Location; +pyrogram.venue#b0700013 flags:# location:Location title:string address:string foursquare_id:flags.0?string = pyrogram.Venue; +pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector> = pyrogram.UserProfilePhotos; +pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto; +pyrogram.chatMember#b0700016 flags:# user:User status:string until_date:flags.0?int can_be_edited:flags.1?Bool can_change_info:flags.2?Bool can_post_messages:flags.3?Bool can_edit_messages:flags.4?Bool can_delete_messages:flags.5?Bool can_invite_users:flags.6?Bool can_restrict_members:flags.7?Bool can_pin_messages:flags.8?Bool can_promote_members:flags.9?Bool can_send_messages:flags.10?Bool can_send_media_messages:flags.11?Bool can_send_other_messages:flags.12?Bool can_add_web_page_previews:flags.13?Bool = pyrogram.ChatMember; +pyrogram.sticker#b0700017 flags:# file_id:string width:int height:int thumb:flags.0?PhotoSize emoji:flags.1?string set_name:flags.2?string mask_position:flags.3?MaskPosition file_size:flags.4?int = pyrogram.Sticker; From f3fcfb17df784579997125d34828634987463f37 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 20:44:11 +0200 Subject: [PATCH 174/285] Add fallback for any unsupported media --- pyrogram/client/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index cf81c68d..2358db3c 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -355,16 +355,18 @@ def parse_message( mime_type=doc.mime_type, file_size=doc.size ) + else: + media = None m = pyrogram.Message( message_id=message.id, date=message.date, chat=parse_chat(message, users, chats), from_user=parse_user(users.get(message.from_id, None)), - text=message.message or None if message.media is None else None, - caption=message.message or None if message.media is not None else None, - entities=entities or None if message.media is None else None, - caption_entities=entities or None if message.media is not None else None, + text=message.message or None if media is None else None, + caption=message.message or None if media is not None else None, + entities=entities or None if media is None else None, + caption_entities=entities or None if media is not None else None, author_signature=message.post_author, forward_from=forward_from, forward_from_chat=forward_from_chat, From 055d1c80eaeea829a71fbb5c3b5038ddccc369d2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 5 Apr 2018 20:59:41 +0200 Subject: [PATCH 175/285] Handle text_mentions --- pyrogram/client/utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index 2358db3c..8890ee1a 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -15,11 +15,11 @@ ENTITIES = { types.MessageEntityCode.ID: "code", types.MessageEntityPre.ID: "pre", types.MessageEntityTextUrl.ID: "text_link", - # TODO: text_mention + types.MessageEntityMentionName.ID: "text_mention" } -def parse_entities(entities: list) -> list: +def parse_entities(entities: list, users: dict) -> list: output_entities = [] for entity in entities: @@ -30,7 +30,13 @@ def parse_entities(entities: list) -> list: type=entity_type, offset=entity.offset, length=entity.length, - url=getattr(entity, "url", None) + url=getattr(entity, "url", None), + user=parse_user( + users.get( + getattr(entity, "user_id", None), + None + ) + ) )) return output_entities @@ -121,7 +127,7 @@ def parse_message( chats: dict, replies: int = 1 ) -> pyrogram.Message: - entities = parse_entities(message.entities) + entities = parse_entities(message.entities, users) forward_from = None forward_from_chat = None From 73246c26f990e9ca885d7415224c53554f878707 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 14:47:00 +0200 Subject: [PATCH 176/285] Rename utils to message_parser --- pyrogram/client/__init__.py | 2 +- pyrogram/client/client.py | 6 +++--- pyrogram/client/{utils.py => message_parser.py} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename pyrogram/client/{utils.py => message_parser.py} (100%) diff --git a/pyrogram/client/__init__.py b/pyrogram/client/__init__.py index 49214797..1faa8dc6 100644 --- a/pyrogram/client/__init__.py +++ b/pyrogram/client/__init__.py @@ -20,4 +20,4 @@ from .chat_action import ChatAction from .client import Client from .parse_mode import ParseMode from .emoji import Emoji -from . import utils +from . import message_parser diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index c14ab380..c85ff8d7 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -46,7 +46,7 @@ from pyrogram.api.errors import ( PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, VolumeLocNotFound, UserMigrate) -from pyrogram.client import utils +from pyrogram.client import message_parser from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId @@ -732,9 +732,9 @@ class Client: message = update.message if isinstance(message, types.Message): - m = utils.parse_message(self, message, users, chats) + m = message_parser.parse_message(self, message, users, chats) elif isinstance(message, types.MessageService): - m = utils.parse_message_service(self, message, users, chats) + m = message_parser.parse_message_service(self, message, users, chats) else: continue else: diff --git a/pyrogram/client/utils.py b/pyrogram/client/message_parser.py similarity index 100% rename from pyrogram/client/utils.py rename to pyrogram/client/message_parser.py From 6fd4c5c016348901c17769998a9644aefa3529f2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 14:47:47 +0200 Subject: [PATCH 177/285] Add TODO --- pyrogram/client/message_parser.py | 553 +----------------------------- 1 file changed, 1 insertion(+), 552 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 8890ee1a..39512414 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -1,552 +1 @@ -from base64 import b64encode, b64decode -from struct import pack - -import pyrogram -from pyrogram.api import types - -ENTITIES = { - types.MessageEntityMention.ID: "mention", - types.MessageEntityHashtag.ID: "hashtag", - types.MessageEntityBotCommand.ID: "bot_command", - types.MessageEntityUrl.ID: "url", - types.MessageEntityEmail.ID: "email", - types.MessageEntityBold.ID: "bold", - types.MessageEntityItalic.ID: "italic", - types.MessageEntityCode.ID: "code", - types.MessageEntityPre.ID: "pre", - types.MessageEntityTextUrl.ID: "text_link", - types.MessageEntityMentionName.ID: "text_mention" -} - - -def parse_entities(entities: list, users: dict) -> list: - output_entities = [] - - for entity in entities: - entity_type = ENTITIES.get(entity.ID, None) - - if entity_type: - output_entities.append(pyrogram.MessageEntity( - type=entity_type, - offset=entity.offset, - length=entity.length, - url=getattr(entity, "url", None), - user=parse_user( - users.get( - getattr(entity, "user_id", None), - None - ) - ) - )) - - return output_entities - - -def parse_user(user: types.User) -> pyrogram.User or None: - return pyrogram.User( - id=user.id, - is_bot=user.bot, - first_name=user.first_name, - last_name=user.last_name, - username=user.username, - language_code=user.lang_code - ) if user else None - - -def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: - if isinstance(message.to_id, types.PeerUser): - return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) - elif isinstance(message.to_id, types.PeerChat): - return parse_chat_chat(chats[message.to_id.chat_id]) - else: - return parse_channel_chat(chats[message.to_id.channel_id]) - - -def parse_user_chat(user: types.User) -> pyrogram.Chat: - return pyrogram.Chat( - id=user.id, - type="private", - username=user.username, - first_name=user.first_name, - last_name=user.last_name - ) - - -def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: - return pyrogram.Chat( - id=-chat.id, - type="group", - title=chat.title, - all_members_are_administrators=not chat.admins_enabled - ) - - -def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: - return pyrogram.Chat( - id=int("-100" + str(channel.id)), - type="supergroup" if channel.megagroup else "channel", - title=channel.title, - username=channel.username - ) - - -def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: - if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): - loc = thumb.location - - if isinstance(thumb, types.PhotoSize): - file_size = thumb.size - else: - file_size = len(thumb.bytes) - - if isinstance(loc, types.FileLocation): - return pyrogram.PhotoSize( - file_id=encode( - pack( - " pyrogram.Message: - entities = parse_entities(message.entities, users) - - forward_from = None - forward_from_chat = None - forward_from_message_id = None - forward_signature = None - forward_date = None - - forward_header = message.fwd_from # type: types.MessageFwdHeader - - if forward_header: - forward_date = forward_header.date - - if forward_header.from_id: - forward_from = parse_user(users[forward_header.from_id]) - else: - forward_from_chat = parse_channel_chat(chats[forward_header.channel_id]) - forward_from_message_id = forward_header.channel_post - forward_signature = forward_header.post_author - - photo = None - location = None - contact = None - venue = None - audio = None - voice = None - video = None - video_note = None - sticker = None - document = None - - media = message.media - - if media: - if isinstance(media, types.MessageMediaPhoto): - photo = media.photo - - if isinstance(photo, types.Photo): - sizes = photo.sizes - photo_sizes = [] - - for size in sizes: - if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): - loc = size.location - - if isinstance(size, types.PhotoSize): - file_size = size.size - else: - file_size = len(size.bytes) - - if isinstance(loc, types.FileLocation): - photo_size = pyrogram.PhotoSize( - file_id=encode( - pack( - " pyrogram.Message: - action = message.action - - new_chat_members = None - left_chat_member = None - new_chat_title = None - delete_chat_photo = None - migrate_to_chat_id = None - migrate_from_chat_id = None - group_chat_created = None - channel_chat_created = None - new_chat_photo = None - - if isinstance(action, types.MessageActionChatAddUser): - new_chat_members = [parse_user(users[i]) for i in action.users] - elif isinstance(action, types.MessageActionChatJoinedByLink): - new_chat_members = [parse_user(users[action.inviter_id])] - elif isinstance(action, types.MessageActionChatDeleteUser): - left_chat_member = parse_user(users[action.user_id]) - elif isinstance(action, types.MessageActionChatEditTitle): - new_chat_title = action.title - elif isinstance(action, types.MessageActionChatDeletePhoto): - delete_chat_photo = True - elif isinstance(action, types.MessageActionChatMigrateTo): - migrate_to_chat_id = action.channel_id - elif isinstance(action, types.MessageActionChannelMigrateFrom): - migrate_from_chat_id = action.chat_id - elif isinstance(action, types.MessageActionChatCreate): - group_chat_created = True - elif isinstance(action, types.MessageActionChannelCreate): - channel_chat_created = True - elif isinstance(action, types.MessageActionChatEditPhoto): - photo = action.photo - - if isinstance(photo, types.Photo): - sizes = photo.sizes - photo_sizes = [] - - for size in sizes: - if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): - loc = size.location - - if isinstance(size, types.PhotoSize): - file_size = size.size - else: - file_size = len(size.bytes) - - if isinstance(loc, types.FileLocation): - photo_size = pyrogram.PhotoSize( - file_id=encode( - pack( - " 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("=") +from base64 import b64encode, b64decode from struct import pack import pyrogram from pyrogram.api import types # TODO: Organize the code better? ENTITIES = { types.MessageEntityMention.ID: "mention", types.MessageEntityHashtag.ID: "hashtag", types.MessageEntityBotCommand.ID: "bot_command", types.MessageEntityUrl.ID: "url", types.MessageEntityEmail.ID: "email", types.MessageEntityBold.ID: "bold", types.MessageEntityItalic.ID: "italic", types.MessageEntityCode.ID: "code", types.MessageEntityPre.ID: "pre", types.MessageEntityTextUrl.ID: "text_link", types.MessageEntityMentionName.ID: "text_mention" } def parse_entities(entities: list, users: dict) -> list: output_entities = [] for entity in entities: entity_type = ENTITIES.get(entity.ID, None) if entity_type: output_entities.append(pyrogram.MessageEntity( type=entity_type, offset=entity.offset, length=entity.length, url=getattr(entity, "url", None), user=parse_user( users.get( getattr(entity, "user_id", None), None ) ) )) return output_entities def parse_user(user: types.User) -> pyrogram.User or None: return pyrogram.User( id=user.id, is_bot=user.bot, first_name=user.first_name, last_name=user.last_name, username=user.username, language_code=user.lang_code ) if user else None def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: if isinstance(message.to_id, types.PeerUser): return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) elif isinstance(message.to_id, types.PeerChat): return parse_chat_chat(chats[message.to_id.chat_id]) else: return parse_channel_chat(chats[message.to_id.channel_id]) def parse_user_chat(user: types.User) -> pyrogram.Chat: return pyrogram.Chat( id=user.id, type="private", username=user.username, first_name=user.first_name, last_name=user.last_name ) def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: return pyrogram.Chat( id=-chat.id, type="group", title=chat.title, all_members_are_administrators=not chat.admins_enabled ) def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: return pyrogram.Chat( id=int("-100" + str(channel.id)), type="supergroup" if channel.megagroup else "channel", title=channel.title, username=channel.username ) def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): loc = thumb.location if isinstance(thumb, types.PhotoSize): file_size = thumb.size else: file_size = len(thumb.bytes) if isinstance(loc, types.FileLocation): return pyrogram.PhotoSize( file_id=encode( pack( " pyrogram.Message: entities = parse_entities(message.entities, users) forward_from = None forward_from_chat = None forward_from_message_id = None forward_signature = None forward_date = None forward_header = message.fwd_from # type: types.MessageFwdHeader if forward_header: forward_date = forward_header.date if forward_header.from_id: forward_from = parse_user(users[forward_header.from_id]) else: forward_from_chat = parse_channel_chat(chats[forward_header.channel_id]) forward_from_message_id = forward_header.channel_post forward_signature = forward_header.post_author photo = None location = None contact = None venue = None audio = None voice = None video = None video_note = None sticker = None document = None media = message.media if media: if isinstance(media, types.MessageMediaPhoto): photo = media.photo if isinstance(photo, types.Photo): sizes = photo.sizes photo_sizes = [] for size in sizes: if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): loc = size.location if isinstance(size, types.PhotoSize): file_size = size.size else: file_size = len(size.bytes) if isinstance(loc, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( pack( " pyrogram.Message: action = message.action new_chat_members = None left_chat_member = None new_chat_title = None delete_chat_photo = None migrate_to_chat_id = None migrate_from_chat_id = None group_chat_created = None channel_chat_created = None new_chat_photo = None if isinstance(action, types.MessageActionChatAddUser): new_chat_members = [parse_user(users[i]) for i in action.users] elif isinstance(action, types.MessageActionChatJoinedByLink): new_chat_members = [parse_user(users[action.inviter_id])] elif isinstance(action, types.MessageActionChatDeleteUser): left_chat_member = parse_user(users[action.user_id]) elif isinstance(action, types.MessageActionChatEditTitle): new_chat_title = action.title elif isinstance(action, types.MessageActionChatDeletePhoto): delete_chat_photo = True elif isinstance(action, types.MessageActionChatMigrateTo): migrate_to_chat_id = action.channel_id elif isinstance(action, types.MessageActionChannelMigrateFrom): migrate_from_chat_id = action.chat_id elif isinstance(action, types.MessageActionChatCreate): group_chat_created = True elif isinstance(action, types.MessageActionChannelCreate): channel_chat_created = True elif isinstance(action, types.MessageActionChatEditPhoto): photo = action.photo if isinstance(photo, types.Photo): sizes = photo.sizes photo_sizes = [] for size in sizes: if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): loc = size.location if isinstance(size, types.PhotoSize): file_size = size.size else: file_size = len(size.bytes) if isinstance(loc, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( pack( " 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("=") \ No newline at end of file From 6bc52fd03b88598ce5f4051be20a61c6e60c17ad Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 17:44:12 +0200 Subject: [PATCH 178/285] Add base Handler class --- pyrogram/client/handler/handler.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pyrogram/client/handler/handler.py diff --git a/pyrogram/client/handler/handler.py b/pyrogram/client/handler/handler.py new file mode 100644 index 00000000..5bcde2f9 --- /dev/null +++ b/pyrogram/client/handler/handler.py @@ -0,0 +1,22 @@ +# 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): + self.callback = callback From 1e8cb843cb67e7e706abd37cc7e483ac3c013a1a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 17:44:50 +0200 Subject: [PATCH 179/285] Add MessageHandler --- pyrogram/client/handler/message_handler.py | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 pyrogram/client/handler/message_handler.py diff --git a/pyrogram/client/handler/message_handler.py b/pyrogram/client/handler/message_handler.py new file mode 100644 index 00000000..3b751fd7 --- /dev/null +++ b/pyrogram/client/handler/message_handler.py @@ -0,0 +1,24 @@ +# 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): + def __init__(self, callback: callable): + super().__init__(callback) From f5a906452c93a40b0acc806115c2107003fa0253 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 17:45:03 +0200 Subject: [PATCH 180/285] Add EditedMessageHandler --- .../client/handler/edited_message_handler.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 pyrogram/client/handler/edited_message_handler.py diff --git a/pyrogram/client/handler/edited_message_handler.py b/pyrogram/client/handler/edited_message_handler.py new file mode 100644 index 00000000..05fb0690 --- /dev/null +++ b/pyrogram/client/handler/edited_message_handler.py @@ -0,0 +1,24 @@ +# 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 EditedMessageHandler(Handler): + def __init__(self, callback: callable): + super().__init__(callback) From e638cc68c66090d840dec9deb4b5255b98806fb1 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 17:45:37 +0200 Subject: [PATCH 181/285] Add ChannelPostHandler and EditedChannelPostHandler --- .../client/handler/channel_post_handler.py | 24 +++++++++++++++++++ .../handler/edited_channel_post_handler.py | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 pyrogram/client/handler/channel_post_handler.py create mode 100644 pyrogram/client/handler/edited_channel_post_handler.py diff --git a/pyrogram/client/handler/channel_post_handler.py b/pyrogram/client/handler/channel_post_handler.py new file mode 100644 index 00000000..35310d46 --- /dev/null +++ b/pyrogram/client/handler/channel_post_handler.py @@ -0,0 +1,24 @@ +# 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 ChannelPostHandler(Handler): + def __init__(self, callback: callable): + super().__init__(callback) diff --git a/pyrogram/client/handler/edited_channel_post_handler.py b/pyrogram/client/handler/edited_channel_post_handler.py new file mode 100644 index 00000000..fb5abd85 --- /dev/null +++ b/pyrogram/client/handler/edited_channel_post_handler.py @@ -0,0 +1,24 @@ +# 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 EditedChannelPostHandler(Handler): + def __init__(self, callback: callable): + super().__init__(callback) From 9df7fc774fb4f31fb28ae9388191136e4d909c1f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 17:46:34 +0200 Subject: [PATCH 182/285] Add handlers to init file --- pyrogram/client/handler/__init__.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pyrogram/client/handler/__init__.py diff --git a/pyrogram/client/handler/__init__.py b/pyrogram/client/handler/__init__.py new file mode 100644 index 00000000..f9366fd0 --- /dev/null +++ b/pyrogram/client/handler/__init__.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 . + +from .channel_post_handler import ChannelPostHandler +from .edited_channel_post_handler import EditedChannelPostHandler +from .edited_message_handler import EditedMessageHandler +from .handler import Handler +from .message_handler import MessageHandler From 2dc57002d6026e622cf092f4eb981dc918d77cb3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 18:36:29 +0200 Subject: [PATCH 183/285] Add Dispatcher --- pyrogram/client/dispatcher/__init__.py | 19 + pyrogram/client/dispatcher/dispatcher.py | 129 +++++ pyrogram/client/dispatcher/message_parser.py | 572 +++++++++++++++++++ pyrogram/client/message_parser.py | 1 - 4 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 pyrogram/client/dispatcher/__init__.py create mode 100644 pyrogram/client/dispatcher/dispatcher.py create mode 100644 pyrogram/client/dispatcher/message_parser.py delete mode 100644 pyrogram/client/message_parser.py 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..035e2aff --- /dev/null +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -0,0 +1,129 @@ +# 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 ..handler import ( + Handler, MessageHandler, EditedMessageHandler, + ChannelPostHandler, EditedChannelPostHandler +) + +log = logging.getLogger(__name__) + + +class Dispatcher: + def __init__(self, client, workers): + self.client = client + self.workers = workers + self.updates = Queue() + self.handlers = OrderedDict() + + def start(self): + for i in range(self.workers): + Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() + + def stop(self): + for _ in range(self.workers): + self.updates.put(None) + + def add_handler(self, handler: Handler, group: int): + if group not in self.handlers: + self.handlers[group] = {} + self.handlers = OrderedDict(sorted(self.handlers.items())) + + if type(handler) not in self.handlers[group]: + self.handlers[group][type(handler)] = handler + else: + raise ValueError( + "'{0}' is already registered in Group #{1}. " + "You can register a different handler in this group or another '{0}' in a different group".format( + type(handler).__name__, + group + ) + ) + + def dispatch(self, update): + if update.message: + key = MessageHandler + elif update.edited_message: + key = EditedMessageHandler + elif update.channel_post: + key = ChannelPostHandler + elif update.edited_channel_post: + key = EditedChannelPostHandler + else: + return + + for group in self.handlers.values(): + handler = group.get(key, None) + + if handler is not None: + handler.callback(self.client, update) + + 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] + + valid_updates = (types.UpdateNewMessage, types.UpdateNewChannelMessage, + types.UpdateEditMessage, types.UpdateEditChannelMessage) + + if isinstance(update, valid_updates): + message = update.message + + if isinstance(message, types.Message): + m = message_parser.parse_message(self.client, message, users, chats) + elif isinstance(message, types.MessageService): + m = message_parser.parse_message_service(self.client, message, users, chats) + else: + continue + else: + continue + + edit = isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)) + + self.dispatch( + pyrogram.Update( + update_id=0, + message=(m if m.chat.type is not "channel" else None) if not edit else None, + edited_message=(m if m.chat.type is not "channel" else None) if edit else None, + channel_post=(m if m.chat.type is "channel" else None) if not edit else None, + edited_channel_post=(m if m.chat.type is "channel" else None) if edit else None + ) + ) + except Exception as e: + log.error(e, exc_info=True) + + log.debug("{} stopped".format(name)) diff --git a/pyrogram/client/dispatcher/message_parser.py b/pyrogram/client/dispatcher/message_parser.py new file mode 100644 index 00000000..a101b78e --- /dev/null +++ b/pyrogram/client/dispatcher/message_parser.py @@ -0,0 +1,572 @@ +# 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 b64encode, b64decode +from struct import pack + +import pyrogram +from pyrogram.api import types + +# TODO: Organize the code better? + +ENTITIES = { + types.MessageEntityMention.ID: "mention", + types.MessageEntityHashtag.ID: "hashtag", + types.MessageEntityBotCommand.ID: "bot_command", + types.MessageEntityUrl.ID: "url", + types.MessageEntityEmail.ID: "email", + types.MessageEntityBold.ID: "bold", + types.MessageEntityItalic.ID: "italic", + types.MessageEntityCode.ID: "code", + types.MessageEntityPre.ID: "pre", + types.MessageEntityTextUrl.ID: "text_link", + types.MessageEntityMentionName.ID: "text_mention" +} + + +def parse_entities(entities: list, users: dict) -> list: + output_entities = [] + + for entity in entities: + entity_type = ENTITIES.get(entity.ID, None) + + if entity_type: + output_entities.append(pyrogram.MessageEntity( + type=entity_type, + offset=entity.offset, + length=entity.length, + url=getattr(entity, "url", None), + user=parse_user( + users.get( + getattr(entity, "user_id", None), + None + ) + ) + )) + + return output_entities + + +def parse_user(user: types.User) -> pyrogram.User or None: + return pyrogram.User( + id=user.id, + is_bot=user.bot, + first_name=user.first_name, + last_name=user.last_name, + username=user.username, + language_code=user.lang_code + ) if user else None + + +def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: + if isinstance(message.to_id, types.PeerUser): + return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) + elif isinstance(message.to_id, types.PeerChat): + return parse_chat_chat(chats[message.to_id.chat_id]) + else: + return parse_channel_chat(chats[message.to_id.channel_id]) + + +def parse_user_chat(user: types.User) -> pyrogram.Chat: + return pyrogram.Chat( + id=user.id, + type="private", + username=user.username, + first_name=user.first_name, + last_name=user.last_name + ) + + +def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: + return pyrogram.Chat( + id=-chat.id, + type="group", + title=chat.title, + all_members_are_administrators=not chat.admins_enabled + ) + + +def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: + return pyrogram.Chat( + id=int("-100" + str(channel.id)), + type="supergroup" if channel.megagroup else "channel", + title=channel.title, + username=channel.username + ) + + +def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: + if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): + loc = thumb.location + + if isinstance(thumb, types.PhotoSize): + file_size = thumb.size + else: + file_size = len(thumb.bytes) + + if isinstance(loc, types.FileLocation): + return pyrogram.PhotoSize( + file_id=encode( + pack( + " pyrogram.Message: + entities = parse_entities(message.entities, users) + + forward_from = None + forward_from_chat = None + forward_from_message_id = None + forward_signature = None + forward_date = None + + forward_header = message.fwd_from # type: types.MessageFwdHeader + + if forward_header: + forward_date = forward_header.date + + if forward_header.from_id: + forward_from = parse_user(users[forward_header.from_id]) + else: + forward_from_chat = parse_channel_chat(chats[forward_header.channel_id]) + forward_from_message_id = forward_header.channel_post + forward_signature = forward_header.post_author + + photo = None + location = None + contact = None + venue = None + audio = None + voice = None + video = None + video_note = None + sticker = None + document = None + + media = message.media + + if media: + if isinstance(media, types.MessageMediaPhoto): + photo = media.photo + + if isinstance(photo, types.Photo): + sizes = photo.sizes + photo_sizes = [] + + for size in sizes: + if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): + loc = size.location + + if isinstance(size, types.PhotoSize): + file_size = size.size + else: + file_size = len(size.bytes) + + if isinstance(loc, types.FileLocation): + photo_size = pyrogram.PhotoSize( + file_id=encode( + pack( + " pyrogram.Message: + action = message.action + + new_chat_members = None + left_chat_member = None + new_chat_title = None + delete_chat_photo = None + migrate_to_chat_id = None + migrate_from_chat_id = None + group_chat_created = None + channel_chat_created = None + new_chat_photo = None + + if isinstance(action, types.MessageActionChatAddUser): + new_chat_members = [parse_user(users[i]) for i in action.users] + elif isinstance(action, types.MessageActionChatJoinedByLink): + new_chat_members = [parse_user(users[action.inviter_id])] + elif isinstance(action, types.MessageActionChatDeleteUser): + left_chat_member = parse_user(users[action.user_id]) + elif isinstance(action, types.MessageActionChatEditTitle): + new_chat_title = action.title + elif isinstance(action, types.MessageActionChatDeletePhoto): + delete_chat_photo = True + elif isinstance(action, types.MessageActionChatMigrateTo): + migrate_to_chat_id = action.channel_id + elif isinstance(action, types.MessageActionChannelMigrateFrom): + migrate_from_chat_id = action.chat_id + elif isinstance(action, types.MessageActionChatCreate): + group_chat_created = True + elif isinstance(action, types.MessageActionChannelCreate): + channel_chat_created = True + elif isinstance(action, types.MessageActionChatEditPhoto): + photo = action.photo + + if isinstance(photo, types.Photo): + sizes = photo.sizes + photo_sizes = [] + + for size in sizes: + if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): + loc = size.location + + if isinstance(size, types.PhotoSize): + file_size = size.size + else: + file_size = len(size.bytes) + + if isinstance(loc, types.FileLocation): + photo_size = pyrogram.PhotoSize( + file_id=encode( + pack( + " 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/client/message_parser.py b/pyrogram/client/message_parser.py deleted file mode 100644 index 39512414..00000000 --- a/pyrogram/client/message_parser.py +++ /dev/null @@ -1 +0,0 @@ -from base64 import b64encode, b64decode from struct import pack import pyrogram from pyrogram.api import types # TODO: Organize the code better? ENTITIES = { types.MessageEntityMention.ID: "mention", types.MessageEntityHashtag.ID: "hashtag", types.MessageEntityBotCommand.ID: "bot_command", types.MessageEntityUrl.ID: "url", types.MessageEntityEmail.ID: "email", types.MessageEntityBold.ID: "bold", types.MessageEntityItalic.ID: "italic", types.MessageEntityCode.ID: "code", types.MessageEntityPre.ID: "pre", types.MessageEntityTextUrl.ID: "text_link", types.MessageEntityMentionName.ID: "text_mention" } def parse_entities(entities: list, users: dict) -> list: output_entities = [] for entity in entities: entity_type = ENTITIES.get(entity.ID, None) if entity_type: output_entities.append(pyrogram.MessageEntity( type=entity_type, offset=entity.offset, length=entity.length, url=getattr(entity, "url", None), user=parse_user( users.get( getattr(entity, "user_id", None), None ) ) )) return output_entities def parse_user(user: types.User) -> pyrogram.User or None: return pyrogram.User( id=user.id, is_bot=user.bot, first_name=user.first_name, last_name=user.last_name, username=user.username, language_code=user.lang_code ) if user else None def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: if isinstance(message.to_id, types.PeerUser): return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) elif isinstance(message.to_id, types.PeerChat): return parse_chat_chat(chats[message.to_id.chat_id]) else: return parse_channel_chat(chats[message.to_id.channel_id]) def parse_user_chat(user: types.User) -> pyrogram.Chat: return pyrogram.Chat( id=user.id, type="private", username=user.username, first_name=user.first_name, last_name=user.last_name ) def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: return pyrogram.Chat( id=-chat.id, type="group", title=chat.title, all_members_are_administrators=not chat.admins_enabled ) def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: return pyrogram.Chat( id=int("-100" + str(channel.id)), type="supergroup" if channel.megagroup else "channel", title=channel.title, username=channel.username ) def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.PhotoSize or None: if isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize)): loc = thumb.location if isinstance(thumb, types.PhotoSize): file_size = thumb.size else: file_size = len(thumb.bytes) if isinstance(loc, types.FileLocation): return pyrogram.PhotoSize( file_id=encode( pack( " pyrogram.Message: entities = parse_entities(message.entities, users) forward_from = None forward_from_chat = None forward_from_message_id = None forward_signature = None forward_date = None forward_header = message.fwd_from # type: types.MessageFwdHeader if forward_header: forward_date = forward_header.date if forward_header.from_id: forward_from = parse_user(users[forward_header.from_id]) else: forward_from_chat = parse_channel_chat(chats[forward_header.channel_id]) forward_from_message_id = forward_header.channel_post forward_signature = forward_header.post_author photo = None location = None contact = None venue = None audio = None voice = None video = None video_note = None sticker = None document = None media = message.media if media: if isinstance(media, types.MessageMediaPhoto): photo = media.photo if isinstance(photo, types.Photo): sizes = photo.sizes photo_sizes = [] for size in sizes: if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): loc = size.location if isinstance(size, types.PhotoSize): file_size = size.size else: file_size = len(size.bytes) if isinstance(loc, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( pack( " pyrogram.Message: action = message.action new_chat_members = None left_chat_member = None new_chat_title = None delete_chat_photo = None migrate_to_chat_id = None migrate_from_chat_id = None group_chat_created = None channel_chat_created = None new_chat_photo = None if isinstance(action, types.MessageActionChatAddUser): new_chat_members = [parse_user(users[i]) for i in action.users] elif isinstance(action, types.MessageActionChatJoinedByLink): new_chat_members = [parse_user(users[action.inviter_id])] elif isinstance(action, types.MessageActionChatDeleteUser): left_chat_member = parse_user(users[action.user_id]) elif isinstance(action, types.MessageActionChatEditTitle): new_chat_title = action.title elif isinstance(action, types.MessageActionChatDeletePhoto): delete_chat_photo = True elif isinstance(action, types.MessageActionChatMigrateTo): migrate_to_chat_id = action.channel_id elif isinstance(action, types.MessageActionChannelMigrateFrom): migrate_from_chat_id = action.chat_id elif isinstance(action, types.MessageActionChatCreate): group_chat_created = True elif isinstance(action, types.MessageActionChannelCreate): channel_chat_created = True elif isinstance(action, types.MessageActionChatEditPhoto): photo = action.photo if isinstance(photo, types.Photo): sizes = photo.sizes photo_sizes = [] for size in sizes: if isinstance(size, (types.PhotoSize, types.PhotoCachedSize)): loc = size.location if isinstance(size, types.PhotoSize): file_size = size.size else: file_size = len(size.bytes) if isinstance(loc, types.FileLocation): photo_size = pyrogram.PhotoSize( file_id=encode( pack( " 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("=") \ No newline at end of file From 331eb624554db202699d5584a7603745bfe01eec Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 18:37:11 +0200 Subject: [PATCH 184/285] Clean up __init__ file --- pyrogram/client/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyrogram/client/__init__.py b/pyrogram/client/__init__.py index 1faa8dc6..b2935dad 100644 --- a/pyrogram/client/__init__.py +++ b/pyrogram/client/__init__.py @@ -18,6 +18,5 @@ from .chat_action import ChatAction from .client import Client -from .parse_mode import ParseMode from .emoji import Emoji -from . import message_parser +from .parse_mode import ParseMode From 7bd52c3718a78dc76efd8c77cb7ac681266b98db Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 18:37:54 +0200 Subject: [PATCH 185/285] Add handlers to __init__ file --- pyrogram/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 0d6595fa..e0dfe473 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -34,3 +34,7 @@ from .client.input_media_photo import InputMediaPhoto from .client.input_media_video import InputMediaVideo from .client.input_phone_contact import InputPhoneContact from .client import Emoji +from .client.handler import ( + MessageHandler, EditedMessageHandler, ChannelPostHandler, + EditedChannelPostHandler +) From e98b209526953d77daed9f2f0d3b3eecbcc1ba07 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 18:48:41 +0200 Subject: [PATCH 186/285] Accommodate the new Dispatcher --- pyrogram/client/client.py | 115 +++++--------------------------------- 1 file changed, 14 insertions(+), 101 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index c85ff8d7..74c2d01c 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -36,7 +36,6 @@ 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 ( @@ -46,10 +45,11 @@ from pyrogram.api.errors import ( PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, VolumeLocNotFound, UserMigrate) -from pyrogram.client import message_parser from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId +from .dispatcher import Dispatcher +from .handler import Handler from .input_media import InputMedia from .style import Markdown, HTML @@ -183,10 +183,13 @@ class Client: self.is_idle = None self.updates_queue = Queue() - self.update_queue = Queue() + self.download_queue = Queue() + + self.dispatcher = Dispatcher(self, workers) self.update_handler = None - self.download_queue = Queue() + def add_handler(self, handler: Handler, group: int = 0): + self.dispatcher.add_handler(handler, group) def start(self): """Use this method to start the Client after creating it. @@ -234,12 +237,11 @@ class Client: for i in range(self.UPDATES_WORKERS): Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start() - for i in range(self.workers): - Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() - for i in range(self.DOWNLOAD_WORKERS): Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start() + self.dispatcher.start() + mimetypes.init() def stop(self): @@ -255,12 +257,11 @@ class Client: for _ in range(self.UPDATES_WORKERS): self.updates_queue.put(None) - for _ in range(self.workers): - self.update_queue.put(None) - for _ in range(self.DOWNLOAD_WORKERS): self.download_queue.put(None) + self.dispatcher.stop() + def authorize_bot(self): try: r = self.send( @@ -684,7 +685,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( @@ -694,7 +695,7 @@ class Client: ) ) - self.update_queue.put(( + self.dispatcher.updates.put(( types.UpdateNewMessage( message=diff.new_messages[0], pts=updates.pts, @@ -704,54 +705,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: - users = {i.id: i for i in update[1]} - chats = {i.id: i for i in update[2]} - update = update[0] - - valid_updates = (types.UpdateNewMessage, types.UpdateNewChannelMessage, - types.UpdateEditMessage, types.UpdateEditChannelMessage) - - if isinstance(update, valid_updates): - message = update.message - - if isinstance(message, types.Message): - m = message_parser.parse_message(self, message, users, chats) - elif isinstance(message, types.MessageService): - m = message_parser.parse_message_service(self, message, users, chats) - else: - continue - else: - continue - - edit = isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)) - - u = pyrogram.Update( - update_id=0, - message=(m if m.chat.type is not "channel" else None) if not edit else None, - edited_message=(m if m.chat.type is not "channel" else None) if edit else None, - channel_post=(m if m.chat.type is "channel" else None) if not edit else None, - edited_channel_post=(m if m.chat.type is "channel" else None) if edit else None - ) - - if self.update_handler: - self.update_handler(self, u) + self.dispatcher.updates.put((updates.update, [], [])) except Exception as e: log.error(e, exc_info=True) @@ -778,47 +732,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. From ede627de52536aa961473bf32928bca13b86dca8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 20:35:38 +0200 Subject: [PATCH 187/285] Directly pass the message instead of the update --- pyrogram/client/dispatcher/dispatcher.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 035e2aff..1584740d 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -67,12 +67,16 @@ class Dispatcher: def dispatch(self, update): if update.message: key = MessageHandler + value = update.message elif update.edited_message: key = EditedMessageHandler + value = update.edited_message elif update.channel_post: key = ChannelPostHandler + value = update.channel_post elif update.edited_channel_post: key = EditedChannelPostHandler + value = update.edited_channel_post else: return @@ -80,7 +84,7 @@ class Dispatcher: handler = group.get(key, None) if handler is not None: - handler.callback(self.client, update) + handler.callback(self.client, value) def update_worker(self): name = threading.current_thread().name From 90a4e4c41131bae5025ccfe010da9228edd1bc28 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 6 Apr 2018 20:38:34 +0200 Subject: [PATCH 188/285] Allow registering handlers using decorators --- pyrogram/client/client.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 74c2d01c..cd7fe030 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 ( @@ -188,6 +189,25 @@ class Client: self.dispatcher = Dispatcher(self, workers) self.update_handler = None + def on(self, handler, group: int): + def decorator(f): + self.add_handler(handler(f), group) + return f + + return decorator + + def on_message(self, group: int = 0): + return self.on(pyrogram.MessageHandler, group) + + def on_edited_message(self, group: int = 0): + return self.on(pyrogram.EditedMessageHandler, group) + + def on_channel_post(self, group: int = 0): + return self.on(pyrogram.ChannelPostHandler, group) + + def on_edited_channel_post(self, group: int = 0): + return self.on(pyrogram.EditedChannelPostHandler, group) + def add_handler(self, handler: Handler, group: int = 0): self.dispatcher.add_handler(handler, group) From ee2d66b416c04e644201baf7a6229c9665a2bae7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 7 Apr 2018 23:34:28 +0200 Subject: [PATCH 189/285] Only keep MessageHandler --- pyrogram/__init__.py | 5 +--- pyrogram/client/client.py | 17 ++----------- pyrogram/client/dispatcher/dispatcher.py | 21 +++++++--------- pyrogram/client/handler/__init__.py | 3 --- .../client/handler/channel_post_handler.py | 24 ------------------- .../handler/edited_channel_post_handler.py | 24 ------------------- .../client/handler/edited_message_handler.py | 24 ------------------- 7 files changed, 11 insertions(+), 107 deletions(-) delete mode 100644 pyrogram/client/handler/channel_post_handler.py delete mode 100644 pyrogram/client/handler/edited_channel_post_handler.py delete mode 100644 pyrogram/client/handler/edited_message_handler.py diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index e0dfe473..e366ae20 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -34,7 +34,4 @@ from .client.input_media_photo import InputMediaPhoto from .client.input_media_video import InputMediaVideo from .client.input_phone_contact import InputPhoneContact from .client import Emoji -from .client.handler import ( - MessageHandler, EditedMessageHandler, ChannelPostHandler, - EditedChannelPostHandler -) +from .client.handler import MessageHandler diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index cd7fe030..0495fe56 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -189,25 +189,12 @@ class Client: self.dispatcher = Dispatcher(self, workers) self.update_handler = None - def on(self, handler, group: int): + def on_message(self, group: int = 0): def decorator(f): - self.add_handler(handler(f), group) - return f + self.add_handler(pyrogram.MessageHandler(f), group) return decorator - def on_message(self, group: int = 0): - return self.on(pyrogram.MessageHandler, group) - - def on_edited_message(self, group: int = 0): - return self.on(pyrogram.EditedMessageHandler, group) - - def on_channel_post(self, group: int = 0): - return self.on(pyrogram.ChannelPostHandler, group) - - def on_edited_channel_post(self, group: int = 0): - return self.on(pyrogram.EditedChannelPostHandler, group) - def add_handler(self, handler: Handler, group: int = 0): self.dispatcher.add_handler(handler, group) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 1584740d..58a17599 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -26,8 +26,7 @@ import pyrogram from pyrogram.api import types from . import message_parser from ..handler import ( - Handler, MessageHandler, EditedMessageHandler, - ChannelPostHandler, EditedChannelPostHandler + Handler, MessageHandler ) log = logging.getLogger(__name__) @@ -65,18 +64,14 @@ class Dispatcher: ) def dispatch(self, update): - if update.message: + message = (update.message + or update.channel_post + or update.edited_message + or update.edited_channel_post) + + if message: key = MessageHandler - value = update.message - elif update.edited_message: - key = EditedMessageHandler - value = update.edited_message - elif update.channel_post: - key = ChannelPostHandler - value = update.channel_post - elif update.edited_channel_post: - key = EditedChannelPostHandler - value = update.edited_channel_post + value = message else: return diff --git a/pyrogram/client/handler/__init__.py b/pyrogram/client/handler/__init__.py index f9366fd0..c86ca6ec 100644 --- a/pyrogram/client/handler/__init__.py +++ b/pyrogram/client/handler/__init__.py @@ -16,8 +16,5 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .channel_post_handler import ChannelPostHandler -from .edited_channel_post_handler import EditedChannelPostHandler -from .edited_message_handler import EditedMessageHandler from .handler import Handler from .message_handler import MessageHandler diff --git a/pyrogram/client/handler/channel_post_handler.py b/pyrogram/client/handler/channel_post_handler.py deleted file mode 100644 index 35310d46..00000000 --- a/pyrogram/client/handler/channel_post_handler.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 ChannelPostHandler(Handler): - def __init__(self, callback: callable): - super().__init__(callback) diff --git a/pyrogram/client/handler/edited_channel_post_handler.py b/pyrogram/client/handler/edited_channel_post_handler.py deleted file mode 100644 index fb5abd85..00000000 --- a/pyrogram/client/handler/edited_channel_post_handler.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 EditedChannelPostHandler(Handler): - def __init__(self, callback: callable): - super().__init__(callback) diff --git a/pyrogram/client/handler/edited_message_handler.py b/pyrogram/client/handler/edited_message_handler.py deleted file mode 100644 index 05fb0690..00000000 --- a/pyrogram/client/handler/edited_message_handler.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 EditedMessageHandler(Handler): - def __init__(self, callback: callable): - super().__init__(callback) From 1f05c4223a9b0bbb982e1884fad3e3b2a7d85edc Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 8 Apr 2018 11:58:17 +0200 Subject: [PATCH 190/285] Fix copypasta --- pyrogram/client/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 0495fe56..ca737f2f 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -192,6 +192,7 @@ class Client: def on_message(self, group: int = 0): def decorator(f): self.add_handler(pyrogram.MessageHandler(f), group) + return f return decorator From 8e8613bc0ffca9bdd7161fa2cac13685e5ab7733 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 8 Apr 2018 12:43:47 +0200 Subject: [PATCH 191/285] Refactor Dispatcher --- pyrogram/client/dispatcher/dispatcher.py | 60 +++++++++++++++++------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 58a17599..a5ae9049 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -33,6 +33,18 @@ 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 @@ -41,7 +53,10 @@ class Dispatcher: def start(self): for i in range(self.workers): - Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start() + Thread( + target=self.update_worker, + name="UpdateWorker#{}".format(i + 1) + ).start() def stop(self): for _ in range(self.workers): @@ -57,7 +72,8 @@ class Dispatcher: else: raise ValueError( "'{0}' is already registered in Group #{1}. " - "You can register a different handler in this group or another '{0}' in a different group".format( + "You can register a different handler in this group " + "or another '{0}' in a different group".format( type(handler).__name__, group ) @@ -96,30 +112,40 @@ class Dispatcher: chats = {i.id: i for i in update[2]} update = update[0] - valid_updates = (types.UpdateNewMessage, types.UpdateNewChannelMessage, - types.UpdateEditMessage, types.UpdateEditChannelMessage) - - if isinstance(update, valid_updates): - message = update.message - - if isinstance(message, types.Message): - m = message_parser.parse_message(self.client, message, users, chats) - elif isinstance(message, types.MessageService): - m = message_parser.parse_message_service(self.client, message, users, chats) + 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 - edit = isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)) + is_edited_message = isinstance(update, Dispatcher.EDIT_UPDATES) self.dispatch( pyrogram.Update( update_id=0, - message=(m if m.chat.type is not "channel" else None) if not edit else None, - edited_message=(m if m.chat.type is not "channel" else None) if edit else None, - channel_post=(m if m.chat.type is "channel" else None) if not edit else None, - edited_channel_post=(m if m.chat.type is "channel" else None) if edit else None + 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: From 1a7ab62ed9bf56cc94f4c1af63b9c791ec24299d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 8 Apr 2018 13:20:31 +0200 Subject: [PATCH 192/285] Add handler for raw mtproto updates --- pyrogram/__init__.py | 2 +- pyrogram/client/client.py | 10 ++++-- pyrogram/client/dispatcher/dispatcher.py | 31 ++++++++++++------- pyrogram/client/handler/__init__.py | 1 + pyrogram/client/handler/raw_update_handler.py | 24 ++++++++++++++ 5 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 pyrogram/client/handler/raw_update_handler.py diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index e366ae20..18813d55 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -34,4 +34,4 @@ from .client.input_media_photo import InputMediaPhoto from .client.input_media_video import InputMediaVideo from .client.input_phone_contact import InputPhoneContact from .client import Emoji -from .client.handler import MessageHandler +from .client.handler import MessageHandler, RawUpdateHandler diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index e67876d9..12030a9c 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -189,13 +189,19 @@ class Client: self.dispatcher = Dispatcher(self, workers) self.update_handler = None - def on_message(self, group: int = 0): + def on(self, handler, group): def decorator(f): - self.add_handler(pyrogram.MessageHandler(f), group) + self.add_handler(handler(f), group) return f return decorator + def on_message(self, group: int = 0): + return self.on(pyrogram.MessageHandler, group) + + def on_raw_update(self, group: int = 0): + return self.on(pyrogram.RawUpdateHandler, group) + def add_handler(self, handler: Handler, group: int = 0): self.dispatcher.add_handler(handler, group) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index a5ae9049..fb4e46a8 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -26,7 +26,7 @@ import pyrogram from pyrogram.api import types from . import message_parser from ..handler import ( - Handler, MessageHandler + Handler, MessageHandler, RawUpdateHandler ) log = logging.getLogger(__name__) @@ -79,23 +79,28 @@ class Dispatcher: ) ) - def dispatch(self, update): - message = (update.message - or update.channel_post - or update.edited_message - or update.edited_channel_post) - - if message: - key = MessageHandler - value = message + def dispatch(self, update, users: dict = None, chats: dict = None, is_raw: bool = False): + if is_raw: + key = RawUpdateHandler + value = update else: - return + message = (update.message + or update.channel_post + or update.edited_message + or update.edited_channel_post) + + if message: + key = MessageHandler + value = message + else: + return for group in self.handlers.values(): handler = group.get(key, None) if handler is not None: - handler.callback(self.client, value) + args = (self, value, users, chats) if is_raw else (self.client, value) + handler.callback(*args) def update_worker(self): name = threading.current_thread().name @@ -112,6 +117,8 @@ class Dispatcher: 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 diff --git a/pyrogram/client/handler/__init__.py b/pyrogram/client/handler/__init__.py index c86ca6ec..be0bfb56 100644 --- a/pyrogram/client/handler/__init__.py +++ b/pyrogram/client/handler/__init__.py @@ -18,3 +18,4 @@ from .handler import Handler from .message_handler import MessageHandler +from .raw_update_handler import RawUpdateHandler diff --git a/pyrogram/client/handler/raw_update_handler.py b/pyrogram/client/handler/raw_update_handler.py new file mode 100644 index 00000000..758606db --- /dev/null +++ b/pyrogram/client/handler/raw_update_handler.py @@ -0,0 +1,24 @@ +# 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 RawUpdateHandler(Handler): + def __init__(self, callback: callable): + super().__init__(callback) From 28ffff57e1cb9f9571c42e28290b66464ea0ab2d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 8 Apr 2018 13:23:26 +0200 Subject: [PATCH 193/285] Move file_id codec outside --- pyrogram/client/dispatcher/message_parser.py | 38 +------------------- pyrogram/client/utils.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 37 deletions(-) create mode 100644 pyrogram/client/utils.py diff --git a/pyrogram/client/dispatcher/message_parser.py b/pyrogram/client/dispatcher/message_parser.py index a101b78e..42e0a327 100644 --- a/pyrogram/client/dispatcher/message_parser.py +++ b/pyrogram/client/dispatcher/message_parser.py @@ -16,11 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from base64 import b64encode, b64decode from struct import pack import pyrogram from pyrogram.api import types +from ..utils import encode # TODO: Organize the code better? @@ -534,39 +534,3 @@ def parse_message_service( m.pinned_message = parse_message_service(client, message, users, chats) return m - - -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/client/utils.py b/pyrogram/client/utils.py new file mode 100644 index 00000000..e1819b43 --- /dev/null +++ b/pyrogram/client/utils.py @@ -0,0 +1,37 @@ +from base64 import b64decode, b64encode + + +def decode(s: str) -> bytes: + s = b64decode(s + "=" * (-len(s) % 4), "-_") + r = b"" + + assert s[-1] == 2 + + i = 0 + while i < len(s) - 1: + if s[i] != 0: + r += bytes([s[i]]) + else: + r += b"\x00" * s[i + 1] + i += 1 + + i += 1 + + return r + + +def encode(s: bytes) -> str: + r = b"" + n = 0 + + for i in s + bytes([2]): + if i == 0: + n += 1 + else: + if n: + r += b"\x00" + bytes([n]) + n = 0 + + r += bytes([i]) + + return b64encode(r, b"-_").decode().rstrip("=") From 578047dbece9c5d10243da91c70516e95acf9e1c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 8 Apr 2018 15:36:22 +0200 Subject: [PATCH 194/285] Move message_parser outside --- pyrogram/client/dispatcher/dispatcher.py | 2 +- pyrogram/client/{dispatcher => }/message_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pyrogram/client/{dispatcher => }/message_parser.py (99%) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index fb4e46a8..e1ffe293 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -24,7 +24,7 @@ from threading import Thread import pyrogram from pyrogram.api import types -from . import message_parser +from .. import message_parser from ..handler import ( Handler, MessageHandler, RawUpdateHandler ) diff --git a/pyrogram/client/dispatcher/message_parser.py b/pyrogram/client/message_parser.py similarity index 99% rename from pyrogram/client/dispatcher/message_parser.py rename to pyrogram/client/message_parser.py index 42e0a327..f5ff8aa5 100644 --- a/pyrogram/client/dispatcher/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -20,7 +20,7 @@ from struct import pack import pyrogram from pyrogram.api import types -from ..utils import encode +from .utils import encode # TODO: Organize the code better? From 245720278e92643e4912aebafd052a129389edf7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 8 Apr 2018 16:50:18 +0200 Subject: [PATCH 195/285] Enhance send_photo by accepting file_ids and URLs This is the first step of a total revamp of the current Pyrogram API --- pyrogram/client/client.py | 71 ++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 34b4273f..74dfdafd 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -45,14 +45,16 @@ from pyrogram.api.errors import ( PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded, PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing, ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned, - VolumeLocNotFound, UserMigrate) + VolumeLocNotFound, UserMigrate, FileIdInvalid) from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId +from . import message_parser from .dispatcher import Dispatcher from .handler import Handler from .input_media import InputMedia from .style import Markdown, HTML +from .utils import decode log = logging.getLogger(__name__) @@ -133,6 +135,18 @@ class Client: UPDATES_WORKERS = 1 DOWNLOAD_WORKERS = 1 + MEDIA_TYPE_ID = { + 0: "Thumbnail", + 2: "Photo", + 3: "Voice", + 4: "Video", + 5: "Document", + 8: "Sticker", + 9: "Audio", + 10: "GIF", + 13: "VideoNote" + } + def __init__(self, session_name: str, api_id: int or str = None, @@ -1075,7 +1089,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. @@ -1109,23 +1125,55 @@ class Client: The size of the file. Returns: - On success, the sent Message is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` """ + file = None style = self.html if parse_mode.lower() == "html" else self.markdown - file = self.save_file(photo, progress=progress) + + if os.path.exists(photo): + file = self.save_file(photo, progress=progress) + media = types.InputMediaUploadedPhoto( + file=file, + ttl_seconds=ttl_seconds + ) + elif photo.startswith("http"): + media = types.InputMediaPhotoExternal( + url=photo, + ttl_seconds=ttl_seconds + ) + else: + try: + decoded = decode(photo) + fmt = " 24 else " Date: Mon, 9 Apr 2018 22:02:44 +0200 Subject: [PATCH 196/285] Enhance send_audio by accepting file_ids and URLs --- pyrogram/client/client.py | 69 ++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 74dfdafd..3576128a 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1214,7 +1214,9 @@ class Client: audio (``str``): Audio file to send. - Pass a file path as string to send an audio file that exists on your local machine. + Pass a file_id as string to send an audio file that exists on the Telegram servers, + pass an HTTP URL as a string for Telegram to get an audio file from the Internet, or + pass a file path as string to upload a new audio file that exists on your local machine. caption (``str``, optional): Audio caption, 0-200 characters. @@ -1252,31 +1254,61 @@ 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(audio, progress=progress) + + if os.path.exists(audio): + file = self.save_file(audio, progress=progress) + media = types.InputMediaUploadedDocument( + mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"), + file=file, + attributes=[ + types.DocumentAttributeAudio( + duration=duration, + performer=performer, + title=title + ), + types.DocumentAttributeFilename(os.path.basename(audio)) + ] + ) + elif audio.startswith("http"): + media = types.InputMediaDocumentExternal( + url=audio + ) + else: + try: + decoded = decode(audio) + fmt = " 24 else " Date: Mon, 9 Apr 2018 23:30:50 +0200 Subject: [PATCH 197/285] Add base Filter class --- pyrogram/client/filters/filter.py | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 pyrogram/client/filters/filter.py 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) From fb4e98b0b59807291d1054b6ee44f189163182ca Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 9 Apr 2018 23:35:51 +0200 Subject: [PATCH 198/285] Evaluate filters before dispatching messages --- pyrogram/client/dispatcher/dispatcher.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index e1ffe293..957e806b 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -99,7 +99,14 @@ class Dispatcher: handler = group.get(key, None) if handler is not None: - args = (self, value, users, chats) if is_raw else (self.client, value) + if is_raw: + args = (self, value, users, chats) + else: + if not handler.check(value): + continue + + args = (self.client, value) + handler.callback(*args) def update_worker(self): From 142ce0757675cd85d688777429c3cb5e3f4780c7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 00:24:03 +0200 Subject: [PATCH 199/285] Make handlers accept filters --- pyrogram/client/client.py | 12 ++++++------ pyrogram/client/handler/handler.py | 3 ++- pyrogram/client/handler/message_handler.py | 11 +++++++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 3576128a..179b11e2 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -203,15 +203,15 @@ class Client: self.dispatcher = Dispatcher(self, workers) self.update_handler = None - def on(self, handler, group): - def decorator(f): - self.add_handler(handler(f), group) - return f + def on(self, handler, group, filters=None): + def decorator(func): + self.add_handler(handler(func, filters), group) + return func return decorator - def on_message(self, group: int = 0): - return self.on(pyrogram.MessageHandler, group) + def on_message(self, filters=None, group: int = 0): + return self.on(pyrogram.MessageHandler, group, filters) def on_raw_update(self, group: int = 0): return self.on(pyrogram.RawUpdateHandler, group) diff --git a/pyrogram/client/handler/handler.py b/pyrogram/client/handler/handler.py index 5bcde2f9..0e46a205 100644 --- a/pyrogram/client/handler/handler.py +++ b/pyrogram/client/handler/handler.py @@ -18,5 +18,6 @@ class Handler: - def __init__(self, callback: callable): + def __init__(self, callback: callable, filters=None): self.callback = callback + self.filters = filters diff --git a/pyrogram/client/handler/message_handler.py b/pyrogram/client/handler/message_handler.py index 3b751fd7..fdbba245 100644 --- a/pyrogram/client/handler/message_handler.py +++ b/pyrogram/client/handler/message_handler.py @@ -20,5 +20,12 @@ from .handler import Handler class MessageHandler(Handler): - def __init__(self, callback: callable): - super().__init__(callback) + def __init__(self, callback: callable, filters=None): + super().__init__(callback, filters) + + def check(self, message): + return ( + self.filters(message) + if self.filters + else True + ) From b3506a7afabc7ddd59b2bf7979f81a4399711a1a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 00:25:04 +0200 Subject: [PATCH 200/285] Add a text filter (more to come) --- pyrogram/client/filters/__init__.py | 19 +++++++++++++++++++ pyrogram/client/filters/filters.py | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 pyrogram/client/filters/__init__.py create mode 100644 pyrogram/client/filters/filters.py 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/filters.py b/pyrogram/client/filters/filters.py new file mode 100644 index 00000000..50d841d5 --- /dev/null +++ b/pyrogram/client/filters/filters.py @@ -0,0 +1,27 @@ +# 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 .filter import Filter + + +def build(name: str, func: callable) -> type: + return type(name, (Filter,), {"__call__": func})() + + +class Filters: + text = build("Text", lambda self, m: bool(m.text)) From b0a528cadfbb74ba830a9ea3a9fbfbf6d1dce71c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 00:25:41 +0200 Subject: [PATCH 201/285] Make Filters importable via the main package --- pyrogram/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 18813d55..fbb73eb4 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -35,3 +35,4 @@ from .client.input_media_video import InputMediaVideo from .client.input_phone_contact import InputPhoneContact from .client import Emoji from .client.handler import MessageHandler, RawUpdateHandler +from .client.filters import Filters From 9ce13518ecf1e2d151bce59d61f08d1d7536a03e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 00:25:51 +0200 Subject: [PATCH 202/285] Add missing notice --- pyrogram/client/utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pyrogram/client/utils.py b/pyrogram/client/utils.py index e1819b43..dc258993 100644 --- a/pyrogram/client/utils.py +++ b/pyrogram/client/utils.py @@ -1,3 +1,21 @@ +# 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 f2424d3b1e701abfa1727ca4103abea73f849748 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 13:14:10 +0200 Subject: [PATCH 203/285] Add some more filters --- pyrogram/client/filters/filters.py | 37 +++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 50d841d5..ff043a15 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -16,12 +16,43 @@ # 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) -> type: - return type(name, (Filter,), {"__call__": func})() +def build(name: str, func: callable, **kwargs) -> type: + d = {"__call__": func} + d.update(kwargs) + + return type(name, (Filter,), d)() class Filters: - text = build("Text", lambda self, m: bool(m.text)) + text = build("Text", lambda _, m: bool(m.text and not m.text.startswith("/"))) + command = build("Command", lambda _, m: bool(m.text and m.text.startswith("/"))) + reply = build("Reply", lambda _, m: bool(m.reply_to_message)) + forwarded = build("Forwarded", lambda _, m: bool(m.forward_date)) + caption = build("Caption", lambda _, m: bool(m.caption)) + + audio = build("Audio", lambda _, m: bool(m.audio)) + document = build("Document", lambda _, m: bool(m.document)) + photo = build("Photo", lambda _, m: bool(m.photo)) + sticker = build("Sticker", lambda _, m: bool(m.sticker)) + video = build("Video", lambda _, m: bool(m.video)) + voice = build("Voice", lambda _, m: bool(m.voice)) + video_note = build("Voice", lambda _, m: bool(m.video_note)) + contact = build("Contact", lambda _, m: bool(m.contact)) + location = build("Location", lambda _, m: bool(m.location)) + venue = build("Venue", lambda _, m: bool(m.venue)) + + private = build("Private", lambda _, m: bool(m.chat.type == "private")) + group = build("Group", lambda _, m: bool(m.chat.type in ("group", "supergroup"))) + channel = build("Channel", lambda _, m: bool(m.chat.type == "channel")) + + @staticmethod + def regex(pattern, flags: int = 0): + return build( + "Regex", lambda _, m: bool(_.p.search(m.text or "")), + p=re.compile(pattern, flags) + ) From c33c7c76fdcb53d584ebc8316d8d8650d00191ac Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 14:52:31 +0200 Subject: [PATCH 204/285] Rework dispatcher --- pyrogram/client/dispatcher/dispatcher.py | 60 +++++++++--------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 957e806b..62086acd 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -26,7 +26,7 @@ import pyrogram from pyrogram.api import types from .. import message_parser from ..handler import ( - Handler, MessageHandler, RawUpdateHandler + Handler, RawUpdateHandler ) log = logging.getLogger(__name__) @@ -49,7 +49,7 @@ class Dispatcher: self.client = client self.workers = workers self.updates = Queue() - self.handlers = OrderedDict() + self.groups = OrderedDict() def start(self): for i in range(self.workers): @@ -63,51 +63,33 @@ class Dispatcher: self.updates.put(None) def add_handler(self, handler: Handler, group: int): - if group not in self.handlers: - self.handlers[group] = {} - self.handlers = OrderedDict(sorted(self.handlers.items())) + if group not in self.groups: + self.groups[group] = [] + self.groups = OrderedDict(sorted(self.groups.items())) - if type(handler) not in self.handlers[group]: - self.handlers[group][type(handler)] = handler - else: - raise ValueError( - "'{0}' is already registered in Group #{1}. " - "You can register a different handler in this group " - "or another '{0}' in a different group".format( - type(handler).__name__, - group - ) - ) + self.groups[group].append(handler) def dispatch(self, update, users: dict = None, chats: dict = None, is_raw: bool = False): - if is_raw: - key = RawUpdateHandler - value = update - else: - message = (update.message - or update.channel_post - or update.edited_message - or update.edited_channel_post) - - if message: - key = MessageHandler - value = message - else: - return - - for group in self.handlers.values(): - handler = group.get(key, None) - - if handler is not None: + for group in self.groups.values(): + for handler in group: if is_raw: - args = (self, value, users, chats) - else: - if not handler.check(value): + if not isinstance(handler, RawUpdateHandler): continue - args = (self.client, value) + args = (self.client, update, users, chats) + else: + message = (update.message + or update.channel_post + or update.edited_message + or update.edited_channel_post) + + if not handler.check(message): + continue + + args = (self.client, message) handler.callback(*args) + break def update_worker(self): name = threading.current_thread().name From 7537a276201d93841d698bc4b878a74f4bf20973 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 14:54:06 +0200 Subject: [PATCH 205/285] Accept command strings as parameter --- pyrogram/client/filters/filters.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index ff043a15..be6a935d 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -30,7 +30,23 @@ def build(name: str, func: callable, **kwargs) -> type: class Filters: text = build("Text", lambda _, m: bool(m.text and not m.text.startswith("/"))) - command = build("Command", lambda _, m: bool(m.text and m.text.startswith("/"))) + + @staticmethod + def command(command: str or list = list()): + 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 command + ) + ) + reply = build("Reply", lambda _, m: bool(m.reply_to_message)) forwarded = build("Forwarded", lambda _, m: bool(m.forward_date)) caption = build("Caption", lambda _, m: bool(m.caption)) From 9165c7f591d4cfb8ba7e315e919a2f5bc64c3add Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 14:54:39 +0200 Subject: [PATCH 206/285] Rework decorators impl --- pyrogram/client/client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 179b11e2..6545e9a2 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -203,18 +203,19 @@ class Client: self.dispatcher = Dispatcher(self, workers) self.update_handler = None - def on(self, handler, group, filters=None): + def on_message(self, filters=None, group: int = 0): def decorator(func): - self.add_handler(handler(func, filters), group) + self.add_handler(pyrogram.MessageHandler(func, filters), group) return func return decorator - def on_message(self, filters=None, group: int = 0): - return self.on(pyrogram.MessageHandler, group, filters) - def on_raw_update(self, group: int = 0): - return self.on(pyrogram.RawUpdateHandler, group) + def decorator(func): + self.add_handler(pyrogram.RawUpdateHandler(func), group) + return func + + return decorator def add_handler(self, handler: Handler, group: int = 0): self.dispatcher.add_handler(handler, group) From 3b028698f7986a803e8d8945f80d2e12a08893a6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 15:04:31 +0200 Subject: [PATCH 207/285] Make command parameter non-optional --- pyrogram/client/filters/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index be6a935d..f57f961f 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -32,7 +32,7 @@ class Filters: text = build("Text", lambda _, m: bool(m.text and not m.text.startswith("/"))) @staticmethod - def command(command: str or list = list()): + def command(command: str or list): return build( "Command", lambda _, m: bool( From a6b6b0dfd6d11df3c0693bc21a6dfa28332bc27d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 16:04:22 +0200 Subject: [PATCH 208/285] Add edited filter --- pyrogram/client/filters/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index f57f961f..36046185 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -50,6 +50,7 @@ class Filters: reply = build("Reply", lambda _, m: bool(m.reply_to_message)) forwarded = build("Forwarded", lambda _, m: bool(m.forward_date)) caption = build("Caption", lambda _, m: bool(m.caption)) + edited = build("Edited", lambda _, m: bool(m.edit_date)) audio = build("Audio", lambda _, m: bool(m.audio)) document = build("Document", lambda _, m: bool(m.document)) From 793ecc2ab5640fb350064e6731668381aaef31d7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 16:24:39 +0200 Subject: [PATCH 209/285] Add user filter --- pyrogram/client/filters/filters.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 36046185..1a70f130 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -73,3 +73,17 @@ class Filters: "Regex", lambda _, m: bool(_.p.search(m.text or "")), p=re.compile(pattern, flags) ) + + @staticmethod + def user(user: int or str or list()): + return build( + "User", + lambda _, m: bool(m.from_user + and (m.from_user.id in _.u + or 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} + ) + ) From f553e521cec4e468442f98f012bdc354d5d3c805 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 16:26:04 +0200 Subject: [PATCH 210/285] Use sets instead of lists or tuples For faster checks --- pyrogram/client/filters/filters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 1a70f130..1eb8d645 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -41,9 +41,9 @@ class Filters: and (m.text[1:].split()[0] in _.c) ), c=( - [command] + {command} if not isinstance(command, list) - else command + else {c for c in command} ) ) @@ -64,7 +64,7 @@ class Filters: venue = build("Venue", lambda _, m: bool(m.venue)) private = build("Private", lambda _, m: bool(m.chat.type == "private")) - group = build("Group", lambda _, m: bool(m.chat.type in ("group", "supergroup"))) + group = build("Group", lambda _, m: bool(m.chat.type in {"group", "supergroup"})) channel = build("Channel", lambda _, m: bool(m.chat.type == "channel")) @staticmethod From dfa1e5128168384492724aaa90d4bce528a8a023 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 16:27:04 +0200 Subject: [PATCH 211/285] Fix type hinting --- pyrogram/client/filters/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 1eb8d645..5a92b2b1 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -75,7 +75,7 @@ class Filters: ) @staticmethod - def user(user: int or str or list()): + def user(user: int or str or list): return build( "User", lambda _, m: bool(m.from_user From cbf9104aa31fddfbf94673451558e40760022f7c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 18:33:19 +0200 Subject: [PATCH 212/285] Add chat filter --- pyrogram/client/filters/filters.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 5a92b2b1..eafd8a83 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -80,10 +80,26 @@ class Filters: "User", lambda _, m: bool(m.from_user and (m.from_user.id in _.u - or m.from_user.username.lower() 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): + 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} + ) + ) From 059c1d7a27b9c99fec95c7f961e48a2c6bbd66c8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 19:23:40 +0200 Subject: [PATCH 213/285] Add service message filters --- pyrogram/client/filters/filters.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index eafd8a83..6058b0ce 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -103,3 +103,33 @@ class Filters: else {i.lower().strip("@") if type(i) is str else i for i in chat} ) ) + + class _Service(Filter): + new_chat_members = build("NewChatMembers", lambda _, m: bool(m.new_chat_members)) + left_chat_member = build("LeftChatMember", lambda _, m: bool(m.left_chat_member)) + new_chat_title = build("NewChatTitle", lambda _, m: bool(m.new_chat_title)) + new_chat_photo = build("NewChatPhoto", lambda _, m: bool(m.new_chat_photo)) + delete_chat_photo = build("DeleteChatPhoto", lambda _, m: bool(m.delete_chat_photo)) + group_chat_created = build("GroupChatCreated", lambda _, m: bool(m.group_chat_created)) + supergroup_chat_created = build("SupergroupChatCreated", lambda _, m: bool(m.supergroup_chat_created)) + channel_chat_created = build("ChannelChatCreated", lambda _, m: bool(m.channel_chat_created)) + migrate_to_chat_id = build("MigrateToChatId", lambda _, m: bool(m.migrate_to_chat_id)) + migrate_from_chat_id = build("MigrateFromChatId", lambda _, m: bool(m.migrate_from_chat_id)) + pinned_message = build("PinnedMessage", lambda _, m: bool(m.pinned_message)) + + def __call__(self, message): + return bool( + self.new_chat_members(message) + or self.left_chat_member(message) + or self.new_chat_title(message) + or self.new_chat_photo(message) + or self.delete_chat_photo(message) + or self.group_chat_created(message) + or self.supergroup_chat_created(message) + or self.channel_chat_created(message) + or self.migrate_to_chat_id(message) + or self.migrate_from_chat_id(message) + or self.pinned_message(message) + ) + + service = _Service() From 847b8dd5d4f25282910d28200d6e8be4a9ad1540 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 19:30:55 +0200 Subject: [PATCH 214/285] Add views field in messages --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index e28b7c27..3faef182 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -5,7 +5,7 @@ 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 = 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 = pyrogram.Message; +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 = pyrogram.Message; pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index f5ff8aa5..9ce55bdd 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -410,7 +410,8 @@ def parse_message( video=video, video_note=video_note, sticker=sticker, - document=document + document=document, + views=message.views ) if message.reply_to_msg_id and replies: From c4f2906009636a670dc688cd1666dc5b3b11178c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 19:37:52 +0200 Subject: [PATCH 215/285] Add via_bot field in messages --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 3faef182..912359eb 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -5,7 +5,7 @@ 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 = 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 = pyrogram.Message; +pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int chat:Chat forward_from:flags.1?User forward_from_chat:flags.2?Chat forward_from_message_id:flags.3?int forward_signature:flags.4?string forward_date:flags.5?int reply_to_message:flags.6?Message edit_date:flags.7?int media_group_id:flags.8?string author_signature:flags.9?string text:flags.10?string entities:flags.11?Vector caption_entities:flags.12?Vector audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector sticker:flags.17?Sticker video:flags.18?Video voice:flags.19?Voice video_note:flags.20?VideoNote caption:flags.21?string contact:flags.22?Contact location:flags.23?Location venue:flags.24?Venue new_chat_members:flags.25?Vector left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector delete_chat_photo:flags.29?true group_chat_created:flags.30?true supergroup_chat_created:flags.31?true channel_chat_created:flags.32?true migrate_to_chat_id:flags.33?int migrate_from_chat_id:flags.34?int pinned_message:flags.35?Message invoice:flags.36?Invoice successful_payment:flags.37?SuccessfulPayment connected_website:flags.38?string views:flags.39?int via_bot:flags.40?User = pyrogram.Message; pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 9ce55bdd..8057d844 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -411,7 +411,8 @@ def parse_message( video_note=video_note, sticker=sticker, document=document, - views=message.views + views=message.views, + via_bot=parse_user(users.get(message.via_bot_id, None)) ) if message.reply_to_msg_id and replies: From 84b1e697bb950e1b4f8243c99a4fae536f4f1ddb Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 10 Apr 2018 19:48:44 +0200 Subject: [PATCH 216/285] Add phone_number field in users --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 912359eb..a2716ade 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -3,7 +3,7 @@ ---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 = pyrogram.User; +pyrogram.user#b0700001 flags:# id:int is_bot:Bool first_name:string last_name:flags.0?string username:flags.1?string language_code:flags.2?string phone_number:flags.3?string = pyrogram.User; pyrogram.chat#b0700002 flags:# id:int type:string title:flags.0?string username:flags.1?string first_name:flags.2?string last_name:flags.3?string all_members_are_administrators:flags.4?Bool photo:flags.5?ChatPhoto description:flags.6?string invite_link:flags.7?string pinned_message:flags.8?Message sticker_set_name:flags.9?string can_set_sticker_set:flags.10?Bool = pyrogram.Chat; pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int chat:Chat forward_from:flags.1?User forward_from_chat:flags.2?Chat forward_from_message_id:flags.3?int forward_signature:flags.4?string forward_date:flags.5?int reply_to_message:flags.6?Message edit_date:flags.7?int media_group_id:flags.8?string author_signature:flags.9?string text:flags.10?string entities:flags.11?Vector caption_entities:flags.12?Vector audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector sticker:flags.17?Sticker video:flags.18?Video voice:flags.19?Voice video_note:flags.20?VideoNote caption:flags.21?string contact:flags.22?Contact location:flags.23?Location venue:flags.24?Venue new_chat_members:flags.25?Vector left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector delete_chat_photo:flags.29?true group_chat_created:flags.30?true supergroup_chat_created:flags.31?true channel_chat_created:flags.32?true migrate_to_chat_id:flags.33?int migrate_from_chat_id:flags.34?int pinned_message:flags.35?Message invoice:flags.36?Invoice successful_payment:flags.37?SuccessfulPayment connected_website:flags.38?string views:flags.39?int via_bot:flags.40?User = pyrogram.Message; pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 8057d844..427b98b1 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -69,7 +69,8 @@ def parse_user(user: types.User) -> pyrogram.User or None: first_name=user.first_name, last_name=user.last_name, username=user.username, - language_code=user.lang_code + language_code=user.lang_code, + phone_number=user.phone ) if user else None From 472ed8e355e797181c299a72a9de9429a732e7b2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 03:16:48 +0200 Subject: [PATCH 217/285] Document the new features --- compiler/api/compiler.py | 2 +- docs/source/pyrogram/Filters.rst | 6 + docs/source/pyrogram/InputMedia.rst | 6 - docs/source/pyrogram/InputMediaPhoto.rst | 6 + docs/source/pyrogram/InputMediaVideo.rst | 6 + docs/source/pyrogram/MessageHandler.rst | 6 + docs/source/pyrogram/RawUpdateHandler.rst | 6 + docs/source/pyrogram/index.rst | 29 ++- pyrogram/__init__.py | 2 +- pyrogram/client/client.py | 38 +++- pyrogram/client/dispatcher/dispatcher.py | 6 +- pyrogram/client/filters/filters.py | 177 +++++++++++++----- pyrogram/client/handler/message_handler.py | 31 --- pyrogram/client/handler/raw_update_handler.py | 24 --- .../client/{handler => handlers}/__init__.py | 4 +- .../client/{handler => handlers}/handler.py | 0 pyrogram/client/handlers/handlers.py | 93 +++++++++ 17 files changed, 322 insertions(+), 120 deletions(-) create mode 100644 docs/source/pyrogram/Filters.rst delete mode 100644 docs/source/pyrogram/InputMedia.rst create mode 100644 docs/source/pyrogram/InputMediaPhoto.rst create mode 100644 docs/source/pyrogram/InputMediaVideo.rst create mode 100644 docs/source/pyrogram/MessageHandler.rst create mode 100644 docs/source/pyrogram/RawUpdateHandler.rst delete mode 100644 pyrogram/client/handler/message_handler.py delete mode 100644 pyrogram/client/handler/raw_update_handler.py rename pyrogram/client/{handler => handlers}/__init__.py (86%) rename pyrogram/client/{handler => handlers}/handler.py (100%) create mode 100644 pyrogram/client/handlers/handlers.py diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 3d5af589..250de451 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -297,7 +297,7 @@ def start(): "{}: {}{}".format( arg_name, "``optional`` ".format(flag_number) if is_optional else "", - get_docstring_arg_type(arg_type) + get_docstring_arg_type(arg_type, is_pyrogram_type=c.namespace == "pyrogram") ) ) diff --git a/docs/source/pyrogram/Filters.rst b/docs/source/pyrogram/Filters.rst new file mode 100644 index 00000000..083bd64a --- /dev/null +++ b/docs/source/pyrogram/Filters.rst @@ -0,0 +1,6 @@ +Filters +======= + +.. autoclass:: pyrogram.Filters + :members: + :undoc-members: diff --git a/docs/source/pyrogram/InputMedia.rst b/docs/source/pyrogram/InputMedia.rst deleted file mode 100644 index c637bdc0..00000000 --- a/docs/source/pyrogram/InputMedia.rst +++ /dev/null @@ -1,6 +0,0 @@ -InputMedia -========== - -.. autoclass:: pyrogram.InputMedia - :members: - :undoc-members: diff --git a/docs/source/pyrogram/InputMediaPhoto.rst b/docs/source/pyrogram/InputMediaPhoto.rst new file mode 100644 index 00000000..abc3f456 --- /dev/null +++ b/docs/source/pyrogram/InputMediaPhoto.rst @@ -0,0 +1,6 @@ +InputMediaPhoto +=============== + +.. autoclass:: pyrogram.InputMediaPhoto + :members: + :undoc-members: diff --git a/docs/source/pyrogram/InputMediaVideo.rst b/docs/source/pyrogram/InputMediaVideo.rst new file mode 100644 index 00000000..de9c480b --- /dev/null +++ b/docs/source/pyrogram/InputMediaVideo.rst @@ -0,0 +1,6 @@ +InputMediaVideo +=============== + +.. autoclass:: pyrogram.InputMediaVideo + :members: + :undoc-members: diff --git a/docs/source/pyrogram/MessageHandler.rst b/docs/source/pyrogram/MessageHandler.rst new file mode 100644 index 00000000..de908bd3 --- /dev/null +++ b/docs/source/pyrogram/MessageHandler.rst @@ -0,0 +1,6 @@ +MessageHandler +============== + +.. autoclass:: pyrogram.MessageHandler + :members: + :undoc-members: diff --git a/docs/source/pyrogram/RawUpdateHandler.rst b/docs/source/pyrogram/RawUpdateHandler.rst new file mode 100644 index 00000000..3d74a34b --- /dev/null +++ b/docs/source/pyrogram/RawUpdateHandler.rst @@ -0,0 +1,6 @@ +\RawUpdateHandler +================ + +.. autoclass:: pyrogram.RawUpdateHandler + :members: + :undoc-members: diff --git a/docs/source/pyrogram/index.rst b/docs/source/pyrogram/index.rst index 160766eb..7701fd60 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -9,9 +9,36 @@ the same parameters as well, thus offering a familiar look to Bot developers. .. toctree:: Client + MessageHandler + RawUpdateHandler + Filters ChatAction ParseMode - InputMedia 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 + ../types/pyrogram/Sticker + + .. _Telegram Bot API: https://core.telegram.org/bots/api#available-methods diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index fbb73eb4..d5e26b43 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -34,5 +34,5 @@ from .client.input_media_photo import InputMediaPhoto from .client.input_media_video import InputMediaVideo from .client.input_phone_contact import InputPhoneContact from .client import Emoji -from .client.handler import MessageHandler, RawUpdateHandler +from .client.handlers import MessageHandler, RawUpdateHandler from .client.filters import Filters diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 6545e9a2..7e8eb791 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -51,7 +51,6 @@ from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId from . import message_parser from .dispatcher import Dispatcher -from .handler import Handler from .input_media import InputMedia from .style import Markdown, HTML from .utils import decode @@ -204,6 +203,19 @@ class Client: self.update_handler = None 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 @@ -211,13 +223,35 @@ class Client: 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: Handler, group: int = 0): + def add_handler(self, handler, group: int = 0): + """Use this method to register an event handler. + + You can register multiple handlers, but at most one handler within a group + will be used for a single event. To handle the same event more than once, register + your handler using a different group id (lower group id == higher priority). + + Args: + handler (:obj:`Handler `): + The handler to be registered. + + group (``int``, optional): + The group identifier, defaults to 0. + """ self.dispatcher.add_handler(handler, group) def start(self): diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 62086acd..2710c4c0 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -25,9 +25,7 @@ from threading import Thread import pyrogram from pyrogram.api import types from .. import message_parser -from ..handler import ( - Handler, RawUpdateHandler -) +from ..handlers import RawUpdateHandler log = logging.getLogger(__name__) @@ -62,7 +60,7 @@ class Dispatcher: for _ in range(self.workers): self.updates.put(None) - def add_handler(self, handler: Handler, group: int): + def add_handler(self, handler, group: int): if group not in self.groups: self.groups[group] = [] self.groups = OrderedDict(sorted(self.groups.items())) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 6058b0ce..ac2ecf67 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -29,10 +29,71 @@ def build(name: str, func: callable, **kwargs) -> type: class Filters: + """This class provides access to all the Filters available in Pyrogram. + It is intended to be used when adding an handler.""" + text = build("Text", lambda _, m: bool(m.text and not m.text.startswith("/"))) + """Filter text messages.""" + + reply = build("Reply", lambda _, m: bool(m.reply_to_message)) + """Filter messages that are replies to other messages.""" + + forwarded = build("Forwarded", lambda _, m: bool(m.forward_date)) + """Filter messages that are forwarded.""" + + caption = build("Caption", lambda _, m: bool(m.caption)) + """Filter media messages that contain captions.""" + + edited = build("Edited", lambda _, m: bool(m.edit_date)) + """Filter edited messages.""" + + audio = build("Audio", lambda _, m: bool(m.audio)) + """Filter messages that contain an :obj:`Audio `.""" + + document = build("Document", lambda _, m: bool(m.document)) + """Filter messages that contain a :obj:`Document `.""" + + photo = build("Photo", lambda _, m: bool(m.photo)) + """Filter messages that contain a :obj:`Photo `.""" + + sticker = build("Sticker", lambda _, m: bool(m.sticker)) + """Filter messages that contain a :obj:`Sticker `.""" + + video = build("Video", lambda _, m: bool(m.video)) + """Filter messages that contain a :obj:`Video `.""" + + voice = build("Voice", lambda _, m: bool(m.voice)) + """Filter messages that contain a :obj:`Voice ` note.""" + + video_note = build("Voice", lambda _, m: bool(m.video_note)) + """Filter messages that contain a :obj:`VideoNote `.""" + + contact = build("Contact", lambda _, m: bool(m.contact)) + """Filter messages that contain a :obj:`Contact `.""" + + location = build("Location", lambda _, m: bool(m.location)) + """Filter messages that contain a :obj:`Location `.""" + + venue = build("Venue", lambda _, m: bool(m.venue)) + """Filter messages that contain a :obj:`Venue `.""" + + private = build("Private", lambda _, m: bool(m.chat.type == "private")) + """Filter messages sent in private chats.""" + + group = build("Group", lambda _, m: bool(m.chat.type in {"group", "supergroup"})) + """Filter messages sent in group or supergroup chats.""" + + channel = build("Channel", lambda _, m: bool(m.chat.type == "channel")) + """Filter messages sent in channels.""" @staticmethod def command(command: str or list): + """Filter commands, i.e.: text messages starting with '/'. + + Args: + command (``str`` | ``list``): + The command or list of commands as strings the filter should look for. + """ return build( "Command", lambda _, m: bool( @@ -47,28 +108,17 @@ class Filters: ) ) - reply = build("Reply", lambda _, m: bool(m.reply_to_message)) - forwarded = build("Forwarded", lambda _, m: bool(m.forward_date)) - caption = build("Caption", lambda _, m: bool(m.caption)) - edited = build("Edited", lambda _, m: bool(m.edit_date)) - - audio = build("Audio", lambda _, m: bool(m.audio)) - document = build("Document", lambda _, m: bool(m.document)) - photo = build("Photo", lambda _, m: bool(m.photo)) - sticker = build("Sticker", lambda _, m: bool(m.sticker)) - video = build("Video", lambda _, m: bool(m.video)) - voice = build("Voice", lambda _, m: bool(m.voice)) - video_note = build("Voice", lambda _, m: bool(m.video_note)) - contact = build("Contact", lambda _, m: bool(m.contact)) - location = build("Location", lambda _, m: bool(m.location)) - venue = build("Venue", lambda _, m: bool(m.venue)) - - private = build("Private", lambda _, m: bool(m.chat.type == "private")) - group = build("Group", lambda _, m: bool(m.chat.type in {"group", "supergroup"})) - channel = build("Channel", lambda _, m: bool(m.chat.type == "channel")) - @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) @@ -76,6 +126,12 @@ class Filters: @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 @@ -91,6 +147,12 @@ class Filters: @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 @@ -104,32 +166,53 @@ class Filters: ) ) - class _Service(Filter): - new_chat_members = build("NewChatMembers", lambda _, m: bool(m.new_chat_members)) - left_chat_member = build("LeftChatMember", lambda _, m: bool(m.left_chat_member)) - new_chat_title = build("NewChatTitle", lambda _, m: bool(m.new_chat_title)) - new_chat_photo = build("NewChatPhoto", lambda _, m: bool(m.new_chat_photo)) - delete_chat_photo = build("DeleteChatPhoto", lambda _, m: bool(m.delete_chat_photo)) - group_chat_created = build("GroupChatCreated", lambda _, m: bool(m.group_chat_created)) - supergroup_chat_created = build("SupergroupChatCreated", lambda _, m: bool(m.supergroup_chat_created)) - channel_chat_created = build("ChannelChatCreated", lambda _, m: bool(m.channel_chat_created)) - migrate_to_chat_id = build("MigrateToChatId", lambda _, m: bool(m.migrate_to_chat_id)) - migrate_from_chat_id = build("MigrateFromChatId", lambda _, m: bool(m.migrate_from_chat_id)) - pinned_message = build("PinnedMessage", lambda _, m: bool(m.pinned_message)) + new_chat_members = build("NewChatMembers", lambda _, m: bool(m.new_chat_members)) + """Filter service messages for new chat members.""" - def __call__(self, message): - return bool( - self.new_chat_members(message) - or self.left_chat_member(message) - or self.new_chat_title(message) - or self.new_chat_photo(message) - or self.delete_chat_photo(message) - or self.group_chat_created(message) - or self.supergroup_chat_created(message) - or self.channel_chat_created(message) - or self.migrate_to_chat_id(message) - or self.migrate_from_chat_id(message) - or self.pinned_message(message) - ) + left_chat_member = build("LeftChatMember", lambda _, m: bool(m.left_chat_member)) + """Filter service messages for members that left the chat.""" - service = _Service() + new_chat_title = build("NewChatTitle", lambda _, m: bool(m.new_chat_title)) + """Filter service messages for new chat titles.""" + + new_chat_photo = build("NewChatPhoto", lambda _, m: bool(m.new_chat_photo)) + """Filter service messages for new chat photos.""" + + delete_chat_photo = build("DeleteChatPhoto", lambda _, m: bool(m.delete_chat_photo)) + """Filter service messages for deleted photos.""" + + group_chat_created = build("GroupChatCreated", lambda _, m: bool(m.group_chat_created)) + """Filter service messages for group chat creations.""" + + supergroup_chat_created = build("SupergroupChatCreated", lambda _, m: bool(m.supergroup_chat_created)) + """Filter service messages for supergroup chat creations.""" + + channel_chat_created = build("ChannelChatCreated", lambda _, m: bool(m.channel_chat_created)) + """Filter service messages for channel chat creations.""" + + migrate_to_chat_id = build("MigrateToChatId", lambda _, m: bool(m.migrate_to_chat_id)) + """Filter service messages that contain migrate_to_chat_id.""" + + migrate_from_chat_id = build("MigrateFromChatId", lambda _, m: bool(m.migrate_from_chat_id)) + """Filter service messages that contain migrate_from_chat_id.""" + + pinned_message = build("PinnedMessage", lambda _, m: bool(m.pinned_message)) + """Filter service messages for pinned messages.""" + + service = build( + "Service", + lambda _, m: bool( + _.new_chat_members(m) + or _.left_chat_member(m) + or _.new_chat_title(m) + or _.new_chat_photo(m) + or _.delete_chat_photo(m) + or _.group_chat_created(m) + or _.supergroup_chat_created(m) + or _.channel_chat_created(m) + or _.migrate_to_chat_id(m) + or _.migrate_from_chat_id(m) + or _.pinned_m(m) + ) + ) + """Filter all service messages""" diff --git a/pyrogram/client/handler/message_handler.py b/pyrogram/client/handler/message_handler.py deleted file mode 100644 index fdbba245..00000000 --- a/pyrogram/client/handler/message_handler.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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): - def __init__(self, callback: callable, filters=None): - super().__init__(callback, filters) - - def check(self, message): - return ( - self.filters(message) - if self.filters - else True - ) diff --git a/pyrogram/client/handler/raw_update_handler.py b/pyrogram/client/handler/raw_update_handler.py deleted file mode 100644 index 758606db..00000000 --- a/pyrogram/client/handler/raw_update_handler.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 RawUpdateHandler(Handler): - def __init__(self, callback: callable): - super().__init__(callback) diff --git a/pyrogram/client/handler/__init__.py b/pyrogram/client/handlers/__init__.py similarity index 86% rename from pyrogram/client/handler/__init__.py rename to pyrogram/client/handlers/__init__.py index be0bfb56..d9c48359 100644 --- a/pyrogram/client/handler/__init__.py +++ b/pyrogram/client/handlers/__init__.py @@ -16,6 +16,4 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from .handler import Handler -from .message_handler import MessageHandler -from .raw_update_handler import RawUpdateHandler +from .handlers import MessageHandler, RawUpdateHandler diff --git a/pyrogram/client/handler/handler.py b/pyrogram/client/handlers/handler.py similarity index 100% rename from pyrogram/client/handler/handler.py rename to pyrogram/client/handlers/handler.py diff --git a/pyrogram/client/handlers/handlers.py b/pyrogram/client/handlers/handlers.py new file mode 100644 index 00000000..ca43282f --- /dev/null +++ b/pyrogram/client/handlers/handlers.py @@ -0,0 +1,93 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2018 Dan Tès +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from .handler import Handler + + +class MessageHandler(Handler): + """The Message handler class. It is used to handle text, media and service messages coming from + any chat (private, group, channel). + + Args: + callback (``callable``): + Pass a function that will be called when a new Message arrives. It takes *(client, message)* + as positional arguments (look at the section below for a detailed description). + + filters (:obj:`Filters `): + Pass one or more filters to allow only a subset of messages to be passed + in your callback function. + + Other parameters: + client (:obj:`Client `): + The Client itself, useful when you want to call other API methods inside the message handler. + + message (:obj:`Message `): + The received message. + """ + + def __init__(self, callback: callable, filters=None): + super().__init__(callback, filters) + + def check(self, message): + return ( + self.filters(message) + if self.filters + else True + ) + + +class RawUpdateHandler(Handler): + """The Raw Update handler class. It is used to handle raw updates. + + Args: + callback (``callable``): + A function that will be called when a new update is received from the server. It takes + *(client, update, users, chats)* as positional arguments (look at the section below for + a detailed description). + + Other Parameters: + client (:class:`Client `): + The Client itself, useful when you want to call other API methods inside the update handler. + + update (``Update``): + The received update, which can be one of the many single Updates listed in the *updates* + field you see in the :obj:`Update ` type. + + users (``dict``): + Dictionary of all :obj:`User ` mentioned in the update. + You can access extra info about the user (such as *first_name*, *last_name*, etc...) by using + the IDs you find in the *update* argument (e.g.: *users[1768841572]*). + + chats (``dict``): + Dictionary of all :obj:`Chat ` and + :obj:`Channel ` mentioned in the update. + You can access extra info about the chat (such as *title*, *participants_count*, etc...) + by using the IDs you find in the *update* argument (e.g.: *chats[1701277281]*). + + Note: + The following Empty or Forbidden types may exist inside the *users* and *chats* dictionaries. + They mean you have been blocked by the user or banned from the group/channel. + + - :obj:`UserEmpty ` + - :obj:`ChatEmpty ` + - :obj:`ChatForbidden ` + - :obj:`ChannelForbidden ` + """ + + def __init__(self, callback: callable): + super().__init__(callback) From 1f683ba00e80ce1c56ad597382ef9737140ce5ee Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 03:40:40 +0200 Subject: [PATCH 218/285] Update compiler --- compiler/api/compiler.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 250de451..6d77c16b 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -283,23 +283,24 @@ def start(): flag_number = is_optional.group(1) if is_optional else -1 arg_type = arg_type.split("?")[-1] - if docs: - docstring_args.append( - "{} ({}{}):\n {}\n".format( - arg_name, - get_docstring_arg_type(arg_type, is_pyrogram_type=True), - ", optional" if "Optional" in docs[i] else "", - re.sub("Optional\. ", "", docs[i].split("§")[1].rstrip(".") + ".") - ) - ) - else: - docstring_args.append( - "{}: {}{}".format( - arg_name, - "``optional`` ".format(flag_number) if is_optional else "", - get_docstring_arg_type(arg_type, is_pyrogram_type=c.namespace == "pyrogram") - ) + # if 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( + arg_name, + " (optional)".format(flag_number) if is_optional else "", + get_docstring_arg_type(arg_type, is_pyrogram_type=c.namespace == "pyrogram") ) + ) if docstring_args: docstring_args = "Args:\n " + "\n ".join(docstring_args) From 3f56bf0af2588685430d047cba07d00bba33a6ef Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 03:46:37 +0200 Subject: [PATCH 219/285] Update docs --- docs/source/pyrogram/Client.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index 97e3c376..b626e745 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -14,7 +14,9 @@ Client start stop idle - set_update_handler + on_message + on_raw_update + add_handler send resolve_peer get_me From b5f1d3a2a541cb16efc9a4cb19744645897e4015 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 03:53:10 +0200 Subject: [PATCH 220/285] Update docstrings --- pyrogram/client/client.py | 2 +- pyrogram/client/handlers/handlers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 7e8eb791..f0e049f7 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -246,7 +246,7 @@ class Client: your handler using a different group id (lower group id == higher priority). Args: - handler (:obj:`Handler `): + handler (``Handler``): The handler to be registered. group (``int``, optional): diff --git a/pyrogram/client/handlers/handlers.py b/pyrogram/client/handlers/handlers.py index ca43282f..33fff85d 100644 --- a/pyrogram/client/handlers/handlers.py +++ b/pyrogram/client/handlers/handlers.py @@ -36,7 +36,7 @@ class MessageHandler(Handler): client (:obj:`Client `): The Client itself, useful when you want to call other API methods inside the message handler. - message (:obj:`Message `): + message (:obj:`Message `): The received message. """ From 3f163901930d16c48cb1fef8bed84f4c2b6bee4b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 15:16:29 +0200 Subject: [PATCH 221/285] Update docstrings --- pyrogram/client/input_phone_contact.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/input_phone_contact.py b/pyrogram/client/input_phone_contact.py index 9268ca0a..5d5f9f96 100644 --- a/pyrogram/client/input_phone_contact.py +++ b/pyrogram/client/input_phone_contact.py @@ -22,7 +22,7 @@ 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:`pyrogram.Client.add_contacts` Args: phone (:obj:`str`): @@ -35,6 +35,9 @@ class InputPhoneContact: 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(), From 144c229fec4c19d5bf7a0de6488011809c0ad3a5 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 15:34:58 +0200 Subject: [PATCH 222/285] Update compiler --- compiler/api/compiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index 12a97c8c..15b1ec76 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -58,7 +58,7 @@ def get_docstring_arg_type(t: str, is_list: bool = False, is_pyrogram_type: bool 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], is_list=True) + return "List of " + get_docstring_arg_type(t.split("<", 1)[1][:-1], True, is_pyrogram_type) else: if is_pyrogram_type: t = "pyrogram." + t @@ -274,7 +274,7 @@ def start(): ) if c.args else "pass" docstring_args = [] - docs = c.docs.split("|")[1:] if c.docs else None + # docs = c.docs.split("|")[1:] if c.docs else None for i, arg in enumerate(sorted_args): arg_name, arg_type = arg From 98937dbc3b1568d4384421be4c5dd48917646b2c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 11 Apr 2018 23:18:17 +0200 Subject: [PATCH 223/285] Update Docs --- docs/source/pyrogram/index.rst | 7 +- docs/source/resources/TextFormatting.rst | 4 +- docs/source/resources/UpdateHandling.rst | 125 ++++++++++++++++++----- docs/source/start/BasicUsage.rst | 19 ++-- docs/source/start/ProjectSetup.rst | 48 +++++---- docs/source/start/QuickInstallation.rst | 6 ++ pyrogram/client/chat_action.py | 2 +- pyrogram/client/filters/filters.py | 60 +++++------ pyrogram/client/input_phone_contact.py | 8 +- pyrogram/client/parse_mode.py | 4 +- 10 files changed, 190 insertions(+), 93 deletions(-) diff --git a/docs/source/pyrogram/index.rst b/docs/source/pyrogram/index.rst index d1084a29..6b2bc5dd 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -1,11 +1,10 @@ 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. -:class:`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 `Telegram Bot API`_ methods, thus offering a familiar look to Bot developers. .. toctree:: Client diff --git a/docs/source/resources/TextFormatting.rst b/docs/source/resources/TextFormatting.rst index ffec21b9..b822dd5f 100644 --- a/docs/source/resources/TextFormatting.rst +++ b/docs/source/resources/TextFormatting.rst @@ -9,7 +9,7 @@ Markdown Style -------------- 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: +:obj:`send_message() `. Use the following syntax in your message: .. code:: @@ -31,7 +31,7 @@ HTML Style ---------- 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: +:obj:`send_message() `. The following tags are currently supported: .. code:: diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index c13202c3..4ce86656 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -1,41 +1,118 @@ 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 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 :meth:`set_update_handler ` -by passing the name of your defined callback function as argument *before* you start the Client. +Registering an Handler +---------------------- -Here's a complete example on how to set it up: +We shall examine the :obj:`MessageHandler `, which will be in charge for handling +:obj:`Message ` objects. + +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 import Client + app = Client("my_account") - def update_handler(client, update, users, chats): - print(update) - def main(): - client = Client(session_name="example") - client.set_update_handler(update_handler) + @app.on_message() + def my_handler(client, message): + print(message) - client.start() - client.idle() - if __name__ == "__main__": - main() + app.start() + app.idle() -The last line of the main function, :meth:`client.idle() `, is not strictly necessary but highly -recommended when using the update handler; it will block your script execution until you press ``CTRL+C`` and -automatically call the :meth:`stop ` method which stops the Client and gently close the underlying -connection. +Alternatively, if you prefer not to use decorators, there is an alternative way for registering Handlers. +This is useful, for example, if you want to keep your callback functions in a separate file. -Examples --------- +.. code-block:: python -- `Simple Echo `_ -- `Advanced Echo `_ -- `Advanced Echo 2 `_ + 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, you can use +:class:`Filters `. The next example will show you how to handler only messages +containing an :obj:`Audio ` object: + +.. 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)) + +Advanced 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 (``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** was sent in a **channel** or in a **private** chat. + + .. code-block:: python + + @app.on_message(Filters.sticker & (Filters.channel | Filters.private)) + def my_handler(client, message): + print(message) + +Some filters can also accept parameters, like :obj:`command ` or +:obj:`regex `: + +- 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) \ No newline at end of file diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index 3e04dee0..ea2f7005 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -9,9 +9,9 @@ Basic Usage Simple API Access ----------------- -The easiest way to interact with the API is via the :class:`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 easiest way to interact with the Telegram API is via the :class:`Client ` class, +which exposes bot-like_ methods. The purpose of this Client class is to make it even simpler to work with the +API by abstracting the raw functions listed in the scheme. The result is a much cleaner interface that allows you to: @@ -25,10 +25,13 @@ The result is a much cleaner interface that allows you to: .. code-block:: python - client.send_message( - chat_id="me", - text="Hi there! I'm using Pyrogram" - ) + client.send_message("me", "Hi there! I'm using Pyrogram") + +- Upload a photo (with caption): + + .. code-block:: python + + client.send_photo("me", "/home/dan/perla.jpg", "Cute!") .. seealso:: For a complete list of the available methods have a look at the :class:`Client ` class. @@ -39,7 +42,7 @@ 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 +package and call any Telegram API method you wish using the :meth:`send() ` method provided by the Client class. Here some examples: diff --git a/docs/source/start/ProjectSetup.rst b/docs/source/start/ProjectSetup.rst index 323e6a0c..ca6f388e 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/ProjectSetup.rst @@ -8,7 +8,7 @@ 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: +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. @@ -31,8 +31,8 @@ There are two ways to configure a Pyrogram application project, and you can choo api_id = 12345 api_hash = 0123456789abcdef0123456789abcdef -- Alternatively, you can pass your API key to Pyrogram by simply using the *api_key* parameter of the Client class. - This way you can have full control on how to store and load your credentials: +- 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 @@ -40,25 +40,26 @@ There are two ways to configure a Pyrogram application project, and you can choo client = Client( session_name="example", - api_key=(12345, "0123456789abcdef0123456789abcdef") + api_id=12345 + api_hash="0123456789abcdef0123456789abcdef" ) -.. note:: The examples below will assume you have created a *config.ini* file, thus they won't show the *api_key* - parameter usage in the :class:`Client ` class. +.. 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. -Authorization -------------- +User Authorization +------------------ -Telegram requires that users be authorized in order to use the API. +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 ```` of your choice -and call the :meth:`start ` method: +(e.g.: "my_account") and call the :meth:`start ` method: .. code-block:: python from pyrogram import Client - client = Client(session_name="example") + client = Client("my_account") client.start() This starts an interactive shell asking you to input your **phone number** (including your `Country Code`_) @@ -70,15 +71,26 @@ and the **phone code** you will receive: 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. +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. -.. note:: +Bot Authorization +----------------- - 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. +Being written entirely from the ground up, Pyrogram is also able to authorize Bots. +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_: -.. _`Country Code`: https://en.wikipedia.org/wiki/List_of_country_calling_codes \ No newline at end of file +.. code-block:: python + + from pyrogram import Client + + client = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") + client.start() + + +.. _`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 index f3f568e7..4969fe24 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -7,6 +7,12 @@ The most straightforward and recommended way to install or upgrade Pyrogram is b $ pip3 install --upgrade pyrogram +or, with TgCrypto_ (recommended): + +.. code-block:: bash + + $ pip3 install --upgrade pyrogram[tgcrypto] + Bleeding Edge ------------- 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/filters/filters.py b/pyrogram/client/filters/filters.py index ac2ecf67..9f6966ec 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -29,8 +29,8 @@ def build(name: str, func: callable, **kwargs) -> type: class Filters: - """This class provides access to all the Filters available in Pyrogram. - It is intended to be used when adding an handler.""" + """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.""" @@ -48,34 +48,34 @@ class Filters: """Filter edited messages.""" audio = build("Audio", lambda _, m: bool(m.audio)) - """Filter messages that contain an :obj:`Audio `.""" + """Filter messages that contain :obj:`Audio ` objects.""" document = build("Document", lambda _, m: bool(m.document)) - """Filter messages that contain a :obj:`Document `.""" + """Filter messages that contain :obj:`Document ` objects.""" photo = build("Photo", lambda _, m: bool(m.photo)) - """Filter messages that contain a :obj:`Photo `.""" + """Filter messages that contain :obj:`Photo ` objects.""" sticker = build("Sticker", lambda _, m: bool(m.sticker)) - """Filter messages that contain a :obj:`Sticker `.""" + """Filter messages that contain :obj:`Sticker ` objects.""" video = build("Video", lambda _, m: bool(m.video)) - """Filter messages that contain a :obj:`Video `.""" + """Filter messages that contain :obj:`Video ` objects.""" voice = build("Voice", lambda _, m: bool(m.voice)) - """Filter messages that contain a :obj:`Voice ` note.""" + """Filter messages that contain :obj:`Voice ` note objects.""" video_note = build("Voice", lambda _, m: bool(m.video_note)) - """Filter messages that contain a :obj:`VideoNote `.""" + """Filter messages that contain :obj:`VideoNote ` objects.""" contact = build("Contact", lambda _, m: bool(m.contact)) - """Filter messages that contain a :obj:`Contact `.""" + """Filter messages that contain :obj:`Contact ` objects.""" location = build("Location", lambda _, m: bool(m.location)) - """Filter messages that contain a :obj:`Location `.""" + """Filter messages that contain :obj:`Location ` objects.""" venue = build("Venue", lambda _, m: bool(m.venue)) - """Filter messages that contain a :obj:`Venue `.""" + """Filter messages that contain :obj:`Venue ` objects.""" private = build("Private", lambda _, m: bool(m.chat.type == "private")) """Filter messages sent in private chats.""" @@ -166,6 +166,24 @@ class Filters: ) ) + service = build( + "Service", + lambda _, m: bool( + _.new_chat_members(m) + or _.left_chat_member(m) + or _.new_chat_title(m) + or _.new_chat_photo(m) + or _.delete_chat_photo(m) + or _.group_chat_created(m) + or _.supergroup_chat_created(m) + or _.channel_chat_created(m) + or _.migrate_to_chat_id(m) + or _.migrate_from_chat_id(m) + or _.pinned_m(m) + ) + ) + """Filter all service messages""" + new_chat_members = build("NewChatMembers", lambda _, m: bool(m.new_chat_members)) """Filter service messages for new chat members.""" @@ -198,21 +216,3 @@ class Filters: pinned_message = build("PinnedMessage", lambda _, m: bool(m.pinned_message)) """Filter service messages for pinned messages.""" - - service = build( - "Service", - lambda _, m: bool( - _.new_chat_members(m) - or _.left_chat_member(m) - or _.new_chat_title(m) - or _.new_chat_photo(m) - or _.delete_chat_photo(m) - or _.group_chat_created(m) - or _.supergroup_chat_created(m) - or _.channel_chat_created(m) - or _.migrate_to_chat_id(m) - or _.migrate_from_chat_id(m) - or _.pinned_m(m) - ) - ) - """Filter all service messages""" diff --git a/pyrogram/client/input_phone_contact.py b/pyrogram/client/input_phone_contact.py index 5d5f9f96..1ca8bf4c 100644 --- a/pyrogram/client/input_phone_contact.py +++ b/pyrogram/client/input_phone_contact.py @@ -22,16 +22,16 @@ 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 :meth:`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 """ diff --git a/pyrogram/client/parse_mode.py b/pyrogram/client/parse_mode.py index 668bb9c8..817bccb0 100644 --- a/pyrogram/client/parse_mode.py +++ b/pyrogram/client/parse_mode.py @@ -18,8 +18,8 @@ class ParseMode: - """This class provides a convenient access to parse modes. - It is intended to be used with any method that accepts the optional argument **parse_mode** + """This class provides a convenient access to Parse Modes. + Parse Modes are intended to be used with any method that accepts the optional argument **parse_mode**. """ HTML = "html" From 1ccda820c1d486235fb25e8cc8253d8828859d3a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 12 Apr 2018 13:43:16 +0200 Subject: [PATCH 224/285] Update docs --- docs/source/resources/BotsInteraction.rst | 8 ++-- docs/source/resources/UpdateHandling.rst | 52 +++++++++++++++++------ docs/source/start/BasicUsage.rst | 2 +- docs/source/start/ProjectSetup.rst | 40 +++++++++-------- 4 files changed, 67 insertions(+), 35 deletions(-) diff --git a/docs/source/resources/BotsInteraction.rst b/docs/source/resources/BotsInteraction.rst index 48551b4e..5bdba777 100644 --- a/docs/source/resources/BotsInteraction.rst +++ b/docs/source/resources/BotsInteraction.rst @@ -7,7 +7,7 @@ Inline Bots ----------- - If a bot accepts inline queries, you can call it by using - :obj:`get_inline_bot_results ` to get the list of its inline results + :meth:`get_inline_bot_results() ` to get the list of its inline results for a query: .. code-block:: python @@ -20,11 +20,11 @@ Inline Bots :align: center :figwidth: 60% - ``get_inline_bot_results`` is the equivalent action of writing ``@vid Fuzz Universe`` and getting the + ``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 - :obj:`send_inline_bot_result ` to send a chosen result to any chat: + :meth:`send_inline_bot_result() ` to send a chosen result to any chat: .. code-block:: python @@ -36,5 +36,5 @@ Inline Bots :align: center :figwidth: 60% - ``send_inline_bot_result`` is the equivalent action of choosing a result from the list and sending it + ``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/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 4ce86656..0e9b67d4 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -1,8 +1,9 @@ Update Handling =============== -Updates 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. +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. Registering an Handler ---------------------- @@ -29,16 +30,18 @@ of a message as soon as it arrives. app.start() app.idle() -Alternatively, if you prefer not to use decorators, there is an alternative way for registering Handlers. +If you prefer not to use decorators, there is an alternative way for registering Handlers. This is useful, for example, if you want to keep your callback functions in a separate file. .. 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)) @@ -46,10 +49,11 @@ This is useful, for example, if you want to keep your callback functions in a se app.start() app.idle() + Using Filters ------------- -For a finer grained control over what kind of messages will be allowed or not, you can use +For a finer grained control over what kind of messages will be allowed or not in your callback functions, you can use :class:`Filters `. The next example will show you how to handler only messages containing an :obj:`Audio ` object: @@ -57,6 +61,7 @@ containing an :obj:`Audio ` object: from pyrogram import Filters + @app.on_message(Filters.audio) def my_handler(client, message): print(message) @@ -67,18 +72,20 @@ or, without decorators: from pyrogram import Filters, Messagehandler + def my_handler(client, message): print(message) + app.add_handler(MessageHandler(my_handler, Filters.audio)) -Advanced Filters ----------------- +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 (``and``, ``or`` operators respectively). +- Use ``&`` and ``|`` to merge two filters (behave like ``and``, ``or`` operators respectively). Here are some examples: @@ -90,7 +97,7 @@ Here are some examples: def my_handler(client, message): print(message) -- Message is a **sticker** **and** was sent in a **channel** or in a **private** chat. +- Message is a **sticker** **and** is coming from a **channel** or a **private** chat. .. code-block:: python @@ -98,10 +105,13 @@ Here are some examples: def my_handler(client, message): print(message) -Some filters can also accept parameters, like :obj:`command ` or -:obj:`regex `: +Advanced Filters +---------------- -- Message is either a /start or /help **command**. +Some filters, like :obj:`command() ` or :obj:`regex() ` +can also accept arguments: + +- Message is either a */start* or */help* **command**. .. code-block:: python @@ -115,4 +125,22 @@ Some filters can also accept parameters, like :obj:`command ` class, which exposes bot-like_ methods. The purpose of this Client class is to make it even simpler to work with the -API by abstracting the raw functions listed in the scheme. +API by abstracting the raw functions listed in the schema. The result is a much cleaner interface that allows you to: diff --git a/docs/source/start/ProjectSetup.rst b/docs/source/start/ProjectSetup.rst index ca6f388e..c61e2791 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/ProjectSetup.rst @@ -21,28 +21,28 @@ 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: +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 +.. code-block:: ini - [pyrogram] - api_id = 12345 - api_hash = 0123456789abcdef0123456789abcdef + [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: +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 +.. code-block:: python - from pyrogram import Client + from pyrogram import Client - client = Client( - session_name="example", - api_id=12345 - api_hash="0123456789abcdef0123456789abcdef" - ) + client = Client( + session_name="example", + 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. @@ -53,7 +53,7 @@ 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 ```` of your choice -(e.g.: "my_account") and call the :meth:`start ` method: +(e.g.: "my_account") and call the :meth:`start() ` method: .. code-block:: python @@ -81,7 +81,9 @@ Bot Authorization ----------------- Being written entirely from the ground up, Pyrogram is also able to authorize Bots. -This means that you can use Pyrogram to execute API calls with a Bot identity. +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 @@ -91,6 +93,8 @@ Instead of phone numbers, Bots are authorized via their tokens which are created client = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") client.start() +That's all, no further action is needed. The session file created will be named after the Bot user_id, which is +``123456.session`` in 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 From 346d3da1758700b5a8269005791c8b60a9e3906e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 12 Apr 2018 14:11:01 +0200 Subject: [PATCH 225/285] Add more info in case of an error --- pyrogram/client/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index f0e049f7..3232e0b0 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -830,7 +830,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 is not None: self.proxy = Proxy( From 255d33cefc5628085f58c32a9a4012521458d79f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 12 Apr 2018 14:13:58 +0200 Subject: [PATCH 226/285] Update snippet --- docs/source/index.rst | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index fc3ede99..38403c58 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,6 +35,22 @@ Welcome to Pyrogram

+.. 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() + About ----- @@ -45,16 +61,6 @@ button at the end of each page. But first, here's a brief overview of what is th **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. -.. code-block:: python - - from pyrogram import Client - - client = Client("example") - client.start() - - client.send_message("me", "Hi there! I'm using Pyrogram") - - client.stop() Features -------- From 87586fba36c0fa857d932a8036026ea2ab2bff9b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 13 Apr 2018 10:01:08 +0200 Subject: [PATCH 227/285] Update docs --- docs/source/start/QuickInstallation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index 4969fe24..8203550e 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -1,7 +1,7 @@ Quick Installation ================== -The most straightforward and recommended way to install or upgrade Pyrogram is by using **pip**: +The easiest way to install and upgrade Pyrogram is by using **pip**: .. code-block:: bash @@ -25,7 +25,7 @@ If you want the latest development version of the library, you can install it wi Verifying --------- -To verify that Pyrogram is correctly installed, open a Python shell and try to import it. +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 From ebc34e71d3cfb0a16f2f99986ff13a560b5f2f95 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 13 Apr 2018 16:30:19 +0200 Subject: [PATCH 228/285] Optimize imports --- pyrogram/client/client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 9e28d66b..c6664daf 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -50,12 +50,11 @@ from pyrogram.crypto import AES from pyrogram.session import Auth, Session from pyrogram.session.internals import MsgId from . import message_parser -from .dispatcher import Dispatcher from . import utils +from .dispatcher import Dispatcher from .input_media import InputMedia from .style import Markdown, HTML from .syncer import Syncer -from .utils import decode log = logging.getLogger(__name__) @@ -1182,7 +1181,7 @@ class Client: ) else: try: - decoded = decode(photo) + decoded = utils.decode(photo) fmt = " 24 else " 24 else " Date: Fri, 13 Apr 2018 19:09:00 +0200 Subject: [PATCH 229/285] Join threads before closing the connection --- pyrogram/client/client.py | 32 ++++++++++++++++++++---- pyrogram/client/dispatcher/dispatcher.py | 16 +++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 0950bd7b..51f63896 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -191,7 +191,9 @@ class Client: self.is_idle = None self.updates_queue = Queue() + self.updates_workers_list = [] self.download_queue = Queue() + self.download_workers_list = [] self.dispatcher = Dispatcher(self, workers) self.update_handler = None @@ -301,10 +303,24 @@ 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) + ) + ) + + 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() @@ -318,17 +334,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 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): diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index 2710c4c0..ac61b04f 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -46,20 +46,28 @@ class Dispatcher: 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): - Thread( - target=self.update_worker, - name="UpdateWorker#{}".format(i + 1) - ).start() + 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] = [] From e8193435a94914f6112d251290a34873dfb3c15f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 14:02:48 +0200 Subject: [PATCH 230/285] Fix service filter and add media filter --- pyrogram/client/filters/filters.py | 105 +++++++++++++++++------------ 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index 9f6966ec..3386c352 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -86,6 +86,39 @@ class Filters: 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 '/'. @@ -169,50 +202,34 @@ class Filters: service = build( "Service", lambda _, m: bool( - _.new_chat_members(m) - or _.left_chat_member(m) - or _.new_chat_title(m) - or _.new_chat_photo(m) - or _.delete_chat_photo(m) - or _.group_chat_created(m) - or _.supergroup_chat_created(m) - or _.channel_chat_created(m) - or _.migrate_to_chat_id(m) - or _.migrate_from_chat_id(m) - or _.pinned_m(m) + 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""" + """Filter all service messages.""" - 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.""" + 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.""" From c84c64a5e1aa7150a3d55a223bcfd1ecc092c068 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 16:10:46 +0200 Subject: [PATCH 231/285] Allow accessing Object fields using square brackets --- docs/source/index.rst | 31 ++++---------- docs/source/pyrogram/index.rst | 2 +- docs/source/resources/AutoAuthorization.rst | 12 +++--- docs/source/resources/BotsInteraction.rst | 4 +- docs/source/resources/SOCKS5Proxy.rst | 4 +- docs/source/resources/TextFormatting.rst | 6 +-- docs/source/resources/UpdateHandling.rst | 8 ++-- docs/source/start/BasicUsage.rst | 32 +++++++------- docs/source/start/ProjectSetup.rst | 46 ++++++++++----------- 9 files changed, 65 insertions(+), 80 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 38403c58..b9b422b0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,35 +51,22 @@ Welcome to Pyrogram app.start() app.idle() -About ------ - 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: **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. +Awesomeness +----------- -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**: All the low-level details of communication with Telegram servers are automatically handled. +- ⚡️ **Fast**: Critical parts are boosted up by TgCrypto_, a high-performance Crypto Library written in pure C. +- 🔄 **Updated** to the latest Telegram MTProto API version, currently Layer 76. +- 📖 **Documented**: Pyrogram public API methods are documented and resemble the Telegram Bot API. +- 🔋 **Full API**, allows to execute any advanced action an official client is able to do, and more. To get started, press the Next button. diff --git a/docs/source/pyrogram/index.rst b/docs/source/pyrogram/index.rst index 6b2bc5dd..9a86c11d 100644 --- a/docs/source/pyrogram/index.rst +++ b/docs/source/pyrogram/index.rst @@ -4,7 +4,7 @@ Pyrogram In this section you can find a detailed description of the Pyrogram package and its high-level API. :class:`Client ` is the main class. It exposes easy-to-use methods that are named -after the `Telegram Bot API`_ methods, thus offering a familiar look to Bot developers. +after the well established `Telegram Bot API`_ methods, thus offering a familiar look to Bot developers. .. toctree:: Client diff --git a/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst index 1e2d72f0..46f0809d 100644 --- a/docs/source/resources/AutoAuthorization.rst +++ b/docs/source/resources/AutoAuthorization.rst @@ -26,15 +26,15 @@ ask you to input the phone code manually. return code # Must be string, e.g., "12345" - client = Client( + app = Client( session_name="example", phone_number="39**********", phone_code=phone_code_callback, password="password" # (if you have one) ) - client.start() - print(client.get_me()) + app.start() + print(app.get_me()) Sign Up ------- @@ -52,7 +52,7 @@ Telegram account in case the phone number you passed is not registered yet. return code # Must be string, e.g., "12345" - client = Client( + app = Client( session_name="example", phone_number="39**********", phone_code=phone_code_callback, @@ -60,5 +60,5 @@ Telegram account in case the phone number you passed is not registered yet. last_name="" # Can be an empty string ) - client.start() - print(client.get_me()) \ No newline at end of file + 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 index 5bdba777..cbbe23c1 100644 --- a/docs/source/resources/BotsInteraction.rst +++ b/docs/source/resources/BotsInteraction.rst @@ -13,7 +13,7 @@ Inline Bots .. code-block:: python # 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") .. figure:: https://i.imgur.com/IAqLs54.png :width: 90% @@ -29,7 +29,7 @@ Inline Bots .. code-block:: python # 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) .. figure:: https://i.imgur.com/wwxr7B7.png :width: 90% diff --git a/docs/source/resources/SOCKS5Proxy.rst b/docs/source/resources/SOCKS5Proxy.rst index f51448ec..761899e6 100644 --- a/docs/source/resources/SOCKS5Proxy.rst +++ b/docs/source/resources/SOCKS5Proxy.rst @@ -32,7 +32,7 @@ Usage from pyrogram import Client - client = Client( + app = Client( session_name="example", proxy=dict( hostname="11.22.33.44", @@ -42,7 +42,7 @@ Usage ) ) - client.start() + app.start() ... diff --git a/docs/source/resources/TextFormatting.rst b/docs/source/resources/TextFormatting.rst index b822dd5f..124d02da 100644 --- a/docs/source/resources/TextFormatting.rst +++ b/docs/source/resources/TextFormatting.rst @@ -56,7 +56,7 @@ Examples .. code-block:: python - client.send_message( + app.send_message( chat_id="me", text=( "**bold**, " @@ -71,7 +71,7 @@ Examples .. code-block:: python - client.send_message( + app.send_message( chat_id="me", text=( # Code block language is optional @@ -88,7 +88,7 @@ Examples from pyrogram import ParseMode - client.send_message( + app.send_message( chat_id="me", text=( "bold, bold, " diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index 0e9b67d4..e2484af9 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -54,7 +54,7 @@ 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 `. The next example will show you how to handler only messages +:class:`Filters `. The next example will show you how to handle only messages containing an :obj:`Audio ` object: .. code-block:: python @@ -97,7 +97,7 @@ Here are some examples: def my_handler(client, message): print(message) -- Message is a **sticker** **and** is coming from a **channel** or a **private** chat. +- Message is a **sticker** **and** is coming from a **channel or** a **private** chat. .. code-block:: python @@ -119,7 +119,7 @@ can also accept arguments: def my_handler(client, message): print(message) -- Message is a **text** message matching the given regex pattern. +- Message is a **text** message matching the given **regex** pattern. .. code-block:: python @@ -127,7 +127,7 @@ can also accept arguments: def my_handler(client, message): print(message) -More handlers using different filters can be created as well: +More handlers using different filters can also live together: .. code-block:: python diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index 8a7572c1..2f376675 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -9,31 +9,29 @@ Basic Usage Simple API Access ----------------- -The easiest way to interact with the Telegram API is via the :class:`Client ` class, -which exposes bot-like_ methods. The purpose of this Client class is to make it even simpler to work with the -API by abstracting the raw functions listed in the schema. - -The result is a much cleaner interface that allows you to: +The easiest way to interact with the Telegram API is via the :class:`Client ` class, which +exposes bot-like_ methods: - Get information about the authorized user: .. code-block:: python - print(client.get_me()) + print(app.get_me()) - Send a message to yourself (Saved Messages): .. code-block:: python - client.send_message("me", "Hi there! I'm using Pyrogram") + app.send_message("me", "Hi there! I'm using Pyrogram") -- Upload a photo (with caption): +- Upload a new photo (with caption): .. code-block:: python - client.send_photo("me", "/home/dan/perla.jpg", "Cute!") + app.send_photo("me", "/home/dan/perla.jpg", "Cute!") -.. seealso:: For a complete list of the available methods have a look at the :class:`Client ` class. +.. 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: @@ -55,7 +53,7 @@ Here some examples: ... - client.send( + app.send( functions.account.UpdateProfile( first_name="Dan", last_name="Tès", about="Bio written from Pyrogram" @@ -70,7 +68,7 @@ Here some examples: ... - client.send( + app.send( functions.account.SetPrivacy( key=types.InputPrivacyKeyStatusTimestamp(), rules=[types.InputPrivacyValueAllowContacts()] @@ -85,13 +83,13 @@ Here some examples: ... - client.send( + app.send( functions.channels.InviteToChannel( - channel=client.resolve_peer(123456789), # ID or Username + channel=app.resolve_peer(123456789), # ID or Username users=[ # The users you want to invite - client.resolve_peer(23456789), # By ID - client.resolve_peer("username"), # By username - client.resolve_peer("393281234567"), # By phone number + app.resolve_peer(23456789), # By ID + app.resolve_peer("username"), # By username + app.resolve_peer("393281234567"), # By phone number ] ) ) diff --git a/docs/source/start/ProjectSetup.rst b/docs/source/start/ProjectSetup.rst index c61e2791..3ac16336 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/ProjectSetup.rst @@ -21,28 +21,28 @@ 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: +- 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 + .. code-block:: ini - [pyrogram] - api_id = 12345 - api_hash = 0123456789abcdef0123456789abcdef + [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: +- 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 + .. code-block:: python - from pyrogram import Client + from pyrogram import Client - client = Client( - session_name="example", - api_id=12345 - api_hash="0123456789abcdef0123456789abcdef" - ) + 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. @@ -52,15 +52,15 @@ 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 ```` of your choice +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 - client = Client("my_account") - client.start() + 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: @@ -90,11 +90,11 @@ Instead of phone numbers, Bots are authorized via their tokens which are created from pyrogram import Client - client = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") - client.start() + app = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") + app.start() -That's all, no further action is needed. The session file created will be named after the Bot user_id, which is -``123456.session`` in the example above. +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 From 2e604572956d92bfebb4996d048834b9d0fc5446 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 16:12:52 +0200 Subject: [PATCH 232/285] Remove data folder --- examples/data/pyrogram.png | Bin 41329 -> 0 bytes examples/hello_world.py | 3 --- 2 files changed, 3 deletions(-) delete mode 100644 examples/data/pyrogram.png diff --git a/examples/data/pyrogram.png b/examples/data/pyrogram.png deleted file mode 100644 index 57bfefe86b814585d8260a62585417f1b7af9849..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41329 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 Date: Sat, 14 Apr 2018 16:13:24 +0200 Subject: [PATCH 233/285] Update hello_world.py --- examples/hello_world.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/hello_world.py b/examples/hello_world.py index 386cd38b..2f98508c 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,16 +1,16 @@ from pyrogram import Client # 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**") +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() From 0dd5ebef804f4f499ddb2aea7ea4353e49585cec Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 16:59:34 +0200 Subject: [PATCH 234/285] Fix raw update handler throwing errors --- pyrogram/client/dispatcher/dispatcher.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/dispatcher/dispatcher.py b/pyrogram/client/dispatcher/dispatcher.py index ac61b04f..72bcbfc9 100644 --- a/pyrogram/client/dispatcher/dispatcher.py +++ b/pyrogram/client/dispatcher/dispatcher.py @@ -25,7 +25,7 @@ from threading import Thread import pyrogram from pyrogram.api import types from .. import message_parser -from ..handlers import RawUpdateHandler +from ..handlers import RawUpdateHandler, MessageHandler log = logging.getLogger(__name__) @@ -84,6 +84,9 @@ class Dispatcher: args = (self.client, update, users, chats) else: + if not isinstance(handler, MessageHandler): + continue + message = (update.message or update.channel_post or update.edited_message From 4965e0b4f8b9ab6fb8133872254e97e697142acb Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 17:37:37 +0200 Subject: [PATCH 235/285] Remove warning --- pyrogram/session/auth.py | 1 - 1 file changed, 1 deletion(-) 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: From b5a78ed1d4b219ed514e0b1256c7ad1dc355b96e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 18:46:54 +0200 Subject: [PATCH 236/285] Update examples --- examples/README.md | 11 +++--- examples/advanced_echo.py | 64 --------------------------------- examples/advanced_echo2.py | 55 ---------------------------- examples/echo_bot.py | 17 +++++++++ examples/get_history.py | 10 +++--- examples/get_participants.py | 10 +++--- examples/get_participants2.py | 10 +++--- examples/hello_world.py | 2 ++ examples/inline_bots.py | 10 +++--- examples/raw_update_handler.py | 14 ++++++++ examples/simple_echo.py | 34 ------------------ examples/updates.py | 25 ------------- examples/welcome_bot.py | 66 ++++++++++++---------------------- 13 files changed, 80 insertions(+), 248 deletions(-) delete mode 100644 examples/advanced_echo.py delete mode 100644 examples/advanced_echo2.py create mode 100644 examples/echo_bot.py create mode 100644 examples/raw_update_handler.py delete mode 100644 examples/simple_echo.py delete mode 100644 examples/updates.py 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/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 2f98508c..cd338ed5 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,5 +1,7 @@ from pyrogram import Client +"""This example demonstrates a simple API methods usage""" + # Create a new Client app = Client("my_account") 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() From 84461290033bfb72111c05c31cf96f6470d1ab95 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 19:48:15 +0200 Subject: [PATCH 237/285] Enhance send_document by accepting file_ids and URLs --- pyrogram/client/client.py | 59 +++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 51f63896..90cdac10 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1394,7 +1394,9 @@ class Client: document (``str``): File to send. - Pass a file path as string to send a file that exists on your local machine. + Pass a file_id as string to send a file that exists on the Telegram servers, + pass an HTTP URL as a string for Telegram to get a file from the Internet, or + pass a file path as string to upload a new file that exists on your local machine. caption (``str``, optional): Document caption, 0-200 characters. @@ -1423,26 +1425,56 @@ 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(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 " Date: Sat, 14 Apr 2018 19:56:11 +0200 Subject: [PATCH 238/285] Enhance send_sticker by accepting file_ids and URLs --- pyrogram/client/client.py | 59 +++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 90cdac10..b23e3cda 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1508,7 +1508,9 @@ class Client: sticker (``str``): Sticker to send. - Pass a file path as string to send a sticker that exists on your local machine. + Pass a file_id as string to send a sticker that exists on the Telegram servers, + pass an HTTP URL as a string for Telegram to get a .webp sticker file from the Internet, or + pass a file path as string to upload a new sticker that exists on your local machine. disable_notification (``bool``, optional): Sends the message silently. @@ -1529,25 +1531,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 = 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 " Date: Sat, 14 Apr 2018 20:41:23 +0200 Subject: [PATCH 239/285] Enhance send_video by accepting file_ids and URLs --- pyrogram/client/client.py | 75 +++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index b23e3cda..1f9365ee 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1620,7 +1620,9 @@ class Client: video (``str``): Video to send. - Pass a file path as string to send a video that exists on your local machine. + Pass a file_id as string to send a video that exists on the Telegram servers, + pass an HTTP URL as a string for Telegram to get a video from the Internet, or + pass a file path as string to upload a new video that exists on your local machine. caption (``str``, optional): Video caption, 0-200 characters. @@ -1666,34 +1668,64 @@ 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(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 " Date: Sat, 14 Apr 2018 20:42:32 +0200 Subject: [PATCH 240/285] Enhance send_voice by accepting file_ids and URLs --- pyrogram/client/client.py | 65 ++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 1f9365ee..34820205 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1762,7 +1762,9 @@ class Client: voice (``str``): Audio file to send. - Pass a file path as string to send an audio file that exists on your local machine. + Pass a file_id as string to send an audio that exists on the Telegram servers, + pass an HTTP URL as a string for Telegram to get an audio from the Internet, or + pass a file path as string to upload a new audio that exists on your local machine. caption (``str``, optional): Voice message caption, 0-200 characters. @@ -1794,29 +1796,59 @@ 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(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 " Date: Sat, 14 Apr 2018 20:49:16 +0200 Subject: [PATCH 241/285] Enhance send_video_note by accepting file_ids --- pyrogram/client/client.py | 69 +++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 34820205..4547429d 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1884,7 +1884,9 @@ class Client: video_note (``str``): Video note to send. - Pass a file path as string to send a video note that exists on your local machine. + Pass a file_id as string to send a video note that exists on the Telegram servers, or + pass a file path as string to upload a new video note that exists on your local machine. + Sending video notes by a URL is currently unsupported. duration (``int``, optional): Duration of sent video in seconds. @@ -1911,40 +1913,71 @@ 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 = 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 " Date: Sat, 14 Apr 2018 21:48:15 +0200 Subject: [PATCH 242/285] Enhance send_media_group by accepting file_ids --- pyrogram/client/client.py | 125 ++++++++++++++++++--------- pyrogram/client/input_media_photo.py | 10 ++- pyrogram/client/input_media_video.py | 6 +- 3 files changed, 93 insertions(+), 48 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 4547429d..23cb0946 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -52,7 +52,8 @@ from pyrogram.session.internals import MsgId from . import message_parser from . import utils from .dispatcher import Dispatcher -from .input_media import InputMedia +from .input_media_photo import InputMediaPhoto +from .input_media_video import InputMediaVideo from .style import Markdown, HTML from .syncer import Syncer @@ -1980,6 +1981,8 @@ class Client: return message_parser.parse_message(self, i.message, users, chats) # TODO: Add progress parameter + # TODO: Return new Message object + # TODO: Figure out how to send albums using URLs def send_media_group(self, chat_id: int or str, media: list, @@ -1996,7 +1999,7 @@ class Client: For a private channel/supergroup you can use its *t.me/joinchat/* link. media (``list``): - A list containing either :obj:`pyrogram.InputMedia.Photo` or :obj:`pyrogram.InputMedia.Video` objects + A list containing either :obj:`pyrogram.InputMediaPhoto` or :obj:`pyrogram.InputMediaVideo` objects describing photos and videos to be sent, must include 2–10 items. disable_notification (``bool``, optional): @@ -2009,66 +2012,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 "` or :obj:`HTML ` if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption. Defaults to Markdown. diff --git a/pyrogram/client/input_media_video.py b/pyrogram/client/input_media_video.py index c14767e5..a25a65df 100644 --- a/pyrogram/client/input_media_video.py +++ b/pyrogram/client/input_media_video.py @@ -23,8 +23,10 @@ class InputMediaVideo: Args: media (:obj:`str`): - Video file to send. - Pass a file path as string to send a video that exists on your local machine. + 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 From 94c9e7c52bb648ac96987221ec30d2acadd93397 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 21:55:23 +0200 Subject: [PATCH 243/285] Add some TODOs --- pyrogram/client/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 23cb0946..5f4f91ae 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2326,6 +2326,7 @@ class Client: ) ) + # TODO: Improvements for the new API def get_user_profile_photos(self, user_id: int or str, offset: int = 0, @@ -2487,7 +2488,7 @@ class Client: ) ) - # TODO: Remove redundant code + # TODO: Improvements for the new API def save_file(self, path: str, file_id: int = None, @@ -2562,6 +2563,7 @@ class Client: finally: session.stop() + # TODO: Improvements for the new API def get_file(self, dc_id: int, id: int = None, From e4691151380d93ade2127985fa986b8455400e78 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sat, 14 Apr 2018 22:35:38 +0200 Subject: [PATCH 244/285] Update index --- docs/source/index.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index b9b422b0..80faf684 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -55,6 +55,9 @@ Welcome to Pyrogram's Documentation! Here you can find resources for learning ho 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: +Overview +-------- + **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. @@ -62,10 +65,10 @@ Awesomeness ----------- - 📦 **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. -- 🚀 **High-level**: All the low-level details of communication with Telegram servers are automatically handled. -- ⚡️ **Fast**: Critical parts are boosted up by TgCrypto_, a high-performance Crypto Library written in pure C. +- 🚀 **High-level**: The low-level details of MTProto are automatically handled by the library. +- ⚡️ **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. - 🔄 **Updated** to the latest Telegram MTProto API version, currently Layer 76. -- 📖 **Documented**: Pyrogram public API methods are documented and resemble the Telegram Bot API. +- 📖 **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. - 🔋 **Full API**, allows to execute any advanced action an official client is able to do, and more. To get started, press the Next button. From 0a3c1827aa74725edb577503805c2f07a1e369b8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 20:56:06 +0200 Subject: [PATCH 245/285] Update docs --- docs/source/conf.py | 8 +-- docs/source/index.rst | 30 +++++----- docs/source/resources/UpdateHandling.rst | 73 ++++++++++++------------ docs/source/start/BasicUsage.rst | 4 +- docs/source/start/ProjectSetup.rst | 2 +- docs/source/start/QuickInstallation.rst | 14 ++--- 6 files changed, 66 insertions(+), 65 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index cf8ef732..cecf047f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,7 @@ import sys sys.path.insert(0, os.path.abspath('../..')) # Import after sys.path.insert() to avoid issues -# from pyrogram import __version__ +from pyrogram import __version__ # -- General configuration ------------------------------------------------ @@ -68,9 +68,9 @@ author = 'Dan Tès' # built documents. # # The short X.Y version. -# version = "version " + __version__ +version = "version " + __version__ # The full version, including alpha/beta/rc tags. -# release = version +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -111,7 +111,7 @@ html_theme_options = { 'collapse_navigation': False, 'sticky_navigation': False, 'logo_only': True, - 'display_version': False + 'display_version': True } # The name of an image file (relative to this directory) to place at the top diff --git a/docs/source/index.rst b/docs/source/index.rst index 80faf684..fced42bb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -53,23 +53,23 @@ Welcome to Pyrogram 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: +button at the end of each page. But first, here's a brief overview of what is this all about. -Overview --------- +About +----- -**Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for building +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. -Awesomeness ------------ +Features +-------- -- 📦 **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 automatically handled by the library. -- ⚡️ **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. -- 🔄 **Updated** to the latest Telegram MTProto API version, currently Layer 76. -- 📖 **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. -- 🔋 **Full API**, allows to execute any advanced 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. To get started, press the Next button. @@ -85,13 +85,13 @@ To get started, press the Next button. :hidden: :caption: Resources - resources/TextFormatting resources/UpdateHandling - resources/ErrorHandling resources/SOCKS5Proxy - resources/AutoAuthorization resources/TgCrypto + resources/AutoAuthorization + resources/TextFormatting resources/BotsInteraction + resources/ErrorHandling .. toctree:: :hidden: diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index e2484af9..a6c73187 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -11,73 +11,74 @@ Registering an Handler We shall examine the :obj:`MessageHandler `, which will be in charge for handling :obj:`Message ` objects. -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. +- 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 + .. code-block:: python - from pyrogram import Client + from pyrogram import Client - app = Client("my_account") + app = Client("my_account") - @app.on_message() - def my_handler(client, message): - print(message) + @app.on_message() + def my_handler(client, message): + print(message) - app.start() - app.idle() + app.start() + app.idle() -If you prefer not to use decorators, there is an alternative way for registering Handlers. -This is useful, for example, if you want to keep your callback functions in a separate file. +- 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 a separate file. -.. code-block:: python + .. code-block:: python - from pyrogram import Client, MessageHandler + from pyrogram import Client, MessageHandler - def my_handler(client, message): - print(message) + def my_handler(client, message): + print(message) - app = Client("my_account") + app = Client("my_account") - app.add_handler(MessageHandler(my_handler)) - - app.start() - app.idle() + 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 `. The next example will show you how to handle only messages -containing an :obj:`Audio ` object: +:class:`Filters `. -.. code-block:: python +- This example will show you how to handle only messages containing an :obj:`Audio ` + object: - from pyrogram import Filters + .. code-block:: python + + from pyrogram import Filters - @app.on_message(Filters.audio) - def my_handler(client, message): - print(message) + @app.on_message(Filters.audio) + def my_handler(client, message): + print(message) -or, without decorators: +- or, without decorators: -.. code-block:: python + .. code-block:: python - from pyrogram import Filters, Messagehandler + from pyrogram import Filters, Messagehandler - def my_handler(client, message): - print(message) + def my_handler(client, message): + print(message) - app.add_handler(MessageHandler(my_handler, Filters.audio)) + app.add_handler(MessageHandler(my_handler, Filters.audio)) Combining Filters ----------------- @@ -142,5 +143,5 @@ More handlers using different filters can also live together: @app.on_message(Filters.chat("PyrogramChat")) - def my_handler(client, message): + def from_pyrogramchat(client, message): print("New message in @PyrogramChat") diff --git a/docs/source/start/BasicUsage.rst b/docs/source/start/BasicUsage.rst index 2f376675..94dfd00c 100644 --- a/docs/source/start/BasicUsage.rst +++ b/docs/source/start/BasicUsage.rst @@ -10,7 +10,7 @@ Simple API Access ----------------- The easiest way to interact with the Telegram API is via the :class:`Client ` class, which -exposes bot-like_ methods: +exposes `Bot API-like`_ methods: - Get information about the authorized user: @@ -94,4 +94,4 @@ Here some examples: ) ) -.. _bot-like: https://core.telegram.org/bots/api#available-methods \ No newline at end of file +.. _`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 index 3ac16336..4e397413 100644 --- a/docs/source/start/ProjectSetup.rst +++ b/docs/source/start/ProjectSetup.rst @@ -12,7 +12,7 @@ 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** +#. 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. diff --git a/docs/source/start/QuickInstallation.rst b/docs/source/start/QuickInstallation.rst index 8203550e..7044bfee 100644 --- a/docs/source/start/QuickInstallation.rst +++ b/docs/source/start/QuickInstallation.rst @@ -1,17 +1,17 @@ Quick Installation ================== -The easiest way to install and upgrade Pyrogram is by using **pip**: +- The easiest way to install and upgrade Pyrogram is by using **pip**: -.. code-block:: bash + .. code-block:: bash - $ pip3 install --upgrade pyrogram + $ pip3 install --upgrade pyrogram -or, with TgCrypto_ (recommended): +- or, with TgCrypto_ (recommended): -.. code-block:: bash + .. code-block:: bash - $ pip3 install --upgrade pyrogram[tgcrypto] + $ pip3 install --upgrade pyrogram[tgcrypto] Bleeding Edge ------------- @@ -32,6 +32,6 @@ If no error shows up you are good to go. >>> import pyrogram >>> pyrogram.__version__ - '0.6.5' + '0.7.0' .. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto \ No newline at end of file From ab1d780a61aa419dffe71fd5c073caeb511fbb1a Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 22:23:58 +0200 Subject: [PATCH 246/285] Add ChatPhoto to Chat objects --- pyrogram/client/message_parser.py | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 427b98b1..a5fb6cbb 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -83,13 +83,43 @@ def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Cha return parse_channel_chat(chats[message.to_id.channel_id]) +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.Chat: return pyrogram.Chat( id=user.id, type="private", username=user.username, first_name=user.first_name, - last_name=user.last_name + last_name=user.last_name, + photo=parse_chat_photo(user.photo) ) @@ -98,7 +128,8 @@ def parse_chat_chat(chat: types.Chat) -> pyrogram.Chat: id=-chat.id, type="group", title=chat.title, - all_members_are_administrators=not chat.admins_enabled + all_members_are_administrators=not chat.admins_enabled, + photo=parse_chat_photo(chat.photo) ) @@ -107,7 +138,8 @@ def parse_channel_chat(channel: types.Channel) -> pyrogram.Chat: id=int("-100" + str(channel.id)), type="supergroup" if channel.megagroup else "channel", title=channel.title, - username=channel.username + username=channel.username, + photo=parse_chat_photo(channel.photo) ) From d948831b64281f14bca65f1c2263da59d29d1535 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 22:27:35 +0200 Subject: [PATCH 247/285] Add photo field to User type --- compiler/api/source/pyrogram.tl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index a2716ade..720753e6 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -3,7 +3,7 @@ ---types--- pyrogram.update#b0700000 flags:# update_id:int message:flags.0?Message edited_message:flags.1?Message channel_post:flags.2?Message edited_channel_post:flags.3?Message inline_query:flags.4?InlineQuery chosen_inline_result:flags.5?ChosenInlineResult callback_query:flags.6?CallbackQuery shipping_query:flags.7?ShippingQuery pre_checkout_query:flags.8?PreCheckoutQuery = pyrogram.Update; -pyrogram.user#b0700001 flags:# id:int is_bot:Bool first_name:string last_name:flags.0?string username:flags.1?string language_code:flags.2?string phone_number:flags.3?string = pyrogram.User; +pyrogram.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; From d9fdd89c49b2e01dc4829e9d2837c37de9a2cf26 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 22:28:08 +0200 Subject: [PATCH 248/285] Add ChatPhoto to User objects --- pyrogram/client/message_parser.py | 43 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index a5fb6cbb..367accd6 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -62,27 +62,6 @@ def parse_entities(entities: list, users: dict) -> list: return output_entities -def parse_user(user: types.User) -> pyrogram.User or None: - return pyrogram.User( - id=user.id, - is_bot=user.bot, - first_name=user.first_name, - last_name=user.last_name, - username=user.username, - language_code=user.lang_code, - phone_number=user.phone - ) if user else None - - -def parse_chat(message: types.Message, users: dict, chats: dict) -> pyrogram.Chat: - if isinstance(message.to_id, types.PeerUser): - return parse_user_chat(users[message.to_id.user_id if message.out else message.from_id]) - elif isinstance(message.to_id, types.PeerChat): - return parse_chat_chat(chats[message.to_id.chat_id]) - else: - return parse_channel_chat(chats[message.to_id.channel_id]) - - def parse_chat_photo(photo): if not isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): return None @@ -112,6 +91,28 @@ def parse_chat_photo(photo): ) +def parse_user(user: types.User) -> pyrogram.User or None: + return pyrogram.User( + id=user.id, + is_bot=user.bot, + first_name=user.first_name, + last_name=user.last_name, + username=user.username, + language_code=user.lang_code, + phone_number=user.phone, + 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, From d5f368431076d06de93882fc193f493e4d4cd60b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 23:30:40 +0200 Subject: [PATCH 249/285] Add date field for PhotoSize --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 720753e6..e8da6789 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -7,7 +7,7 @@ pyrogram.user#b0700001 flags:# id:int is_bot:Bool first_name:string last_name:fl pyrogram.chat#b0700002 flags:# id:int type:string title:flags.0?string username:flags.1?string first_name:flags.2?string last_name:flags.3?string all_members_are_administrators:flags.4?Bool photo:flags.5?ChatPhoto description:flags.6?string invite_link:flags.7?string pinned_message:flags.8?Message sticker_set_name:flags.9?string can_set_sticker_set:flags.10?Bool = pyrogram.Chat; pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int chat:Chat forward_from:flags.1?User forward_from_chat:flags.2?Chat forward_from_message_id:flags.3?int forward_signature:flags.4?string forward_date:flags.5?int reply_to_message:flags.6?Message edit_date:flags.7?int media_group_id:flags.8?string author_signature:flags.9?string text:flags.10?string entities:flags.11?Vector caption_entities:flags.12?Vector audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector sticker:flags.17?Sticker video:flags.18?Video voice:flags.19?Voice video_note:flags.20?VideoNote caption:flags.21?string contact:flags.22?Contact location:flags.23?Location venue:flags.24?Venue new_chat_members:flags.25?Vector left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector delete_chat_photo:flags.29?true group_chat_created:flags.30?true supergroup_chat_created:flags.31?true channel_chat_created:flags.32?true migrate_to_chat_id:flags.33?int migrate_from_chat_id:flags.34?int pinned_message:flags.35?Message invoice:flags.36?Invoice successful_payment:flags.37?SuccessfulPayment connected_website:flags.38?string views:flags.39?int via_bot:flags.40?User = pyrogram.Message; pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; -pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int = pyrogram.PhotoSize; +pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int date:flags.1?int = pyrogram.PhotoSize; pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; pyrogram.document#b0700007 flags:# file_id:string thumb:flags.0?PhotoSize file_name:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Document; pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 367accd6..204d3b77 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -247,7 +247,8 @@ def parse_message( ), width=size.w, height=size.h, - file_size=file_size + file_size=file_size, + date=photo.date ) photo_sizes.append(photo_size) @@ -532,7 +533,8 @@ def parse_message_service( ), width=size.w, height=size.h, - file_size=file_size + file_size=file_size, + date=photo.date ) photo_sizes.append(photo_size) From e08e2850ee095781d45e76575619bc86f817e7c8 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 23:51:10 +0200 Subject: [PATCH 250/285] Add thumb, date and file_name to Audio type --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index e8da6789..dd5c290a 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -8,7 +8,7 @@ pyrogram.chat#b0700002 flags:# id:int type:string title:flags.0?string username: pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int chat:Chat forward_from:flags.1?User forward_from_chat:flags.2?Chat forward_from_message_id:flags.3?int forward_signature:flags.4?string forward_date:flags.5?int reply_to_message:flags.6?Message edit_date:flags.7?int media_group_id:flags.8?string author_signature:flags.9?string text:flags.10?string entities:flags.11?Vector caption_entities:flags.12?Vector audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector sticker:flags.17?Sticker video:flags.18?Video voice:flags.19?Voice video_note:flags.20?VideoNote caption:flags.21?string contact:flags.22?Contact location:flags.23?Location venue:flags.24?Venue new_chat_members:flags.25?Vector left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector delete_chat_photo:flags.29?true group_chat_created:flags.30?true supergroup_chat_created:flags.31?true channel_chat_created:flags.32?true migrate_to_chat_id:flags.33?int migrate_from_chat_id:flags.34?int pinned_message:flags.35?Message invoice:flags.36?Invoice successful_payment:flags.37?SuccessfulPayment connected_website:flags.38?string views:flags.39?int via_bot:flags.40?User = pyrogram.Message; pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int date:flags.1?int = pyrogram.PhotoSize; -pyrogram.audio#b0700006 flags:# file_id:string duration:int performer:flags.0?string title:flags.1?string mime_type:flags.2?string file_size:flags.3?int = pyrogram.Audio; +pyrogram.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 = pyrogram.Document; pyrogram.video#b0700008 flags:# file_id:string width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 204d3b77..82031d6b 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -285,6 +285,12 @@ def parse_message( if isinstance(doc, types.Document): attributes = {type(i): i for i in doc.attributes} + file_name = getattr( + attributes.get( + types.DocumentAttributeFilename, None + ), "file_name", None + ) + if types.DocumentAttributeAudio in attributes: audio_attributes = attributes[types.DocumentAttributeAudio] @@ -318,7 +324,10 @@ def parse_message( performer=audio_attributes.performer, title=audio_attributes.title, mime_type=doc.mime_type, - file_size=doc.size + file_size=doc.size, + thumb=parse_thumb(doc.thumb), + file_name=file_name, + date=doc.date ) elif types.DocumentAttributeAnimated in attributes: document = pyrogram.Document( @@ -332,11 +341,7 @@ def parse_message( ) ), thumb=parse_thumb(doc.thumb), - file_name=getattr( - attributes.get( - types.DocumentAttributeFilename, None - ), "file_name", None - ), + file_name=file_name, mime_type=doc.mime_type, file_size=doc.size ) @@ -408,11 +413,7 @@ def parse_message( ) ), thumb=parse_thumb(doc.thumb), - file_name=getattr( - attributes.get( - types.DocumentAttributeFilename, None - ), "file_name", None - ), + file_name=file_name, mime_type=doc.mime_type, file_size=doc.size ) From 44dda8550facf08e5c1fb533621f2aa0a8e315c3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Sun, 15 Apr 2018 23:54:46 +0200 Subject: [PATCH 251/285] Add date field to Document type --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index dd5c290a..dccaa7f0 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -9,7 +9,7 @@ pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int date:flags.1?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 = pyrogram.Document; +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 width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; pyrogram.voice#b0700009 flags:# file_id:string duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 82031d6b..9cfdcd16 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -343,7 +343,8 @@ def parse_message( thumb=parse_thumb(doc.thumb), file_name=file_name, mime_type=doc.mime_type, - file_size=doc.size + file_size=doc.size, + date=doc.date ) elif types.DocumentAttributeVideo in attributes: video_attributes = attributes[types.DocumentAttributeVideo] @@ -415,7 +416,8 @@ def parse_message( thumb=parse_thumb(doc.thumb), file_name=file_name, mime_type=doc.mime_type, - file_size=doc.size + file_size=doc.size, + date=doc.date ) else: media = None From 7184710948e46a0865e61f57f18363b927754258 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 00:01:27 +0200 Subject: [PATCH 252/285] Add file_name and date fields to Video objects --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index dccaa7f0..f6c8d965 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -10,7 +10,7 @@ pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:fl pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int date:flags.1?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 width:int height:int duration:int thumb:flags.0?PhotoSize mime_type:flags.1?string file_size:flags.2?int = pyrogram.Video; +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 duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; pyrogram.videoNote#b0700010 flags:# file_id:string length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 9cfdcd16..2616e8dd 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -381,7 +381,9 @@ def parse_message( duration=video_attributes.duration, thumb=parse_thumb(doc.thumb), mime_type=doc.mime_type, - file_size=doc.size + file_size=doc.size, + file_name=file_name, + date=doc.date ) elif types.DocumentAttributeSticker in attributes: image_size_attributes = attributes[types.DocumentAttributeImageSize] From f35d922c97fd384f99478efcb5f767af8f75c0f9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 00:24:43 +0200 Subject: [PATCH 253/285] Add thumb, file_name and date to Voice type --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index f6c8d965..76b17d65 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -11,7 +11,7 @@ pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_siz 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 duration:int mime_type:flags.0?string file_size:flags.1?int = pyrogram.Voice; +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 length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; pyrogram.contact#b0700011 flags:# phone_number:string first_name:string last_name:flags.0?string user_id:flags.1?int = pyrogram.Contact; pyrogram.location#b0700012 longitude:double latitude:double = pyrogram.Location; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 2616e8dd..befd04a5 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -307,7 +307,10 @@ def parse_message( ), duration=audio_attributes.duration, mime_type=doc.mime_type, - file_size=doc.size + file_size=doc.size, + thumb=parse_thumb(doc.thumb), + file_name=file_name, + date=doc.date ) else: audio = pyrogram.Audio( From ddfce4b7ea9b159b454c7730f43fa57b4fa702b7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 00:30:49 +0200 Subject: [PATCH 254/285] Add file_name, mime_type and date to VideoNote objects --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 76b17d65..1e5ea31b 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -12,7 +12,7 @@ pyrogram.audio#b0700006 flags:# file_id:string thumb:flags.0?PhotoSize file_name 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 length:int duration:int thumb:flags.0?PhotoSize file_size:flags.1?int = pyrogram.VideoNote; +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; diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index befd04a5..aa82c1b7 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -366,7 +366,10 @@ def parse_message( length=video_attributes.w, duration=video_attributes.duration, thumb=parse_thumb(doc.thumb), - file_size=doc.size + file_size=doc.size, + file_name=file_name, + mime_type=doc.mime_type, + date=doc.date ) else: video = pyrogram.Video( From 6275a4003fef2cab30b99876095a2c434fd21697 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 00:59:41 +0200 Subject: [PATCH 255/285] Add mime_type, file_name and date to Sticker objects --- compiler/api/source/pyrogram.tl | 2 +- pyrogram/client/message_parser.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 1e5ea31b..82815beb 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -19,4 +19,4 @@ pyrogram.venue#b0700013 flags:# location:Location title:string address:string fo pyrogram.userProfilePhotos#b0700014 total_count:int photos:Vector> = pyrogram.UserProfilePhotos; pyrogram.chatPhoto#b0700015 small_file_id:string big_file_id:string = pyrogram.ChatPhoto; pyrogram.chatMember#b0700016 flags:# user:User status:string until_date:flags.0?int can_be_edited:flags.1?Bool can_change_info:flags.2?Bool can_post_messages:flags.3?Bool can_edit_messages:flags.4?Bool can_delete_messages:flags.5?Bool can_invite_users:flags.6?Bool can_restrict_members:flags.7?Bool can_pin_messages:flags.8?Bool can_promote_members:flags.9?Bool can_send_messages:flags.10?Bool can_send_media_messages:flags.11?Bool can_send_other_messages:flags.12?Bool can_add_web_page_previews:flags.13?Bool = pyrogram.ChatMember; -pyrogram.sticker#b0700017 flags:# file_id:string width:int height:int thumb:flags.0?PhotoSize emoji:flags.1?string set_name:flags.2?string mask_position:flags.3?MaskPosition file_size:flags.4?int = pyrogram.Sticker; +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/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index aa82c1b7..c04d599f 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -409,6 +409,9 @@ def parse_message( thumb=parse_thumb(doc.thumb), # TODO: Emoji, set_name and mask_position file_size=doc.size, + mime_type=doc.mime_type, + file_name=file_name, + date=doc.date ) else: document = pyrogram.Document( From fdac67de69cf503aa849943aa4a5dfdac98d54f0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 01:07:02 +0200 Subject: [PATCH 256/285] Rework download_media to accept the new Message type --- pyrogram/client/client.py | 184 ++++++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 75 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 5f4f91ae..330940d7 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -129,15 +129,15 @@ class Client: OFFLINE_SLEEP = 300 MEDIA_TYPE_ID = { - 0: "Thumbnail", - 2: "Photo", - 3: "Voice", - 4: "Video", - 5: "Document", - 8: "Sticker", - 9: "Audio", - 10: "GIF", - 13: "VideoNote" + 0: "thumbnail", + 2: "photo", + 3: "voice", + 4: "video", + 5: "document", + 8: "sticker", + 9: "audio", + 10: "gif", + 13: "video_note" } def __init__(self, @@ -618,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))) @@ -3002,14 +3015,14 @@ 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 `): + message (:obj:`Message `): The Message containing the media. file_name (``str``, optional): @@ -3039,24 +3052,45 @@ 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 + elif isinstance(message, ( + pyrogram.PhotoSize, + pyrogram.Audio, + pyrogram.Document, + pyrogram.Video, + pyrogram.Voice, + pyrogram.VideoNote, + pyrogram.Sticker + )): + media = message + else: + return - if block: - done.wait() + done = Event() + path = [None] - return path[0] + self.download_queue.put((media, file_name, done, progress, path)) + + if block: + done.wait() + + return path[0] def download_photo(self, photo: types.Photo or types.UserProfilePhoto or types.ChatPhoto, From e48ad9a7dd365e5b85a1f3bcc77b630541c5095e Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:32:13 +0200 Subject: [PATCH 257/285] Reorganize parameters --- compiler/api/source/pyrogram.tl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/api/source/pyrogram.tl b/compiler/api/source/pyrogram.tl index 82815beb..4a6fb182 100644 --- a/compiler/api/source/pyrogram.tl +++ b/compiler/api/source/pyrogram.tl @@ -7,7 +7,7 @@ pyrogram.user#b0700001 flags:# id:int is_bot:Bool first_name:string last_name:fl pyrogram.chat#b0700002 flags:# id:int type:string title:flags.0?string username:flags.1?string first_name:flags.2?string last_name:flags.3?string all_members_are_administrators:flags.4?Bool photo:flags.5?ChatPhoto description:flags.6?string invite_link:flags.7?string pinned_message:flags.8?Message sticker_set_name:flags.9?string can_set_sticker_set:flags.10?Bool = pyrogram.Chat; pyrogram.message#b0700003 flags:# message_id:int from_user:flags.0?User date:int chat:Chat forward_from:flags.1?User forward_from_chat:flags.2?Chat forward_from_message_id:flags.3?int forward_signature:flags.4?string forward_date:flags.5?int reply_to_message:flags.6?Message edit_date:flags.7?int media_group_id:flags.8?string author_signature:flags.9?string text:flags.10?string entities:flags.11?Vector caption_entities:flags.12?Vector audio:flags.13?Audio document:flags.14?Document game:flags.15?Game photo:flags.16?Vector sticker:flags.17?Sticker video:flags.18?Video voice:flags.19?Voice video_note:flags.20?VideoNote caption:flags.21?string contact:flags.22?Contact location:flags.23?Location venue:flags.24?Venue new_chat_members:flags.25?Vector left_chat_member:flags.26?User new_chat_title:flags.27?string new_chat_photo:flags.28?Vector delete_chat_photo:flags.29?true group_chat_created:flags.30?true supergroup_chat_created:flags.31?true channel_chat_created:flags.32?true migrate_to_chat_id:flags.33?int migrate_from_chat_id:flags.34?int pinned_message:flags.35?Message invoice:flags.36?Invoice successful_payment:flags.37?SuccessfulPayment connected_website:flags.38?string views:flags.39?int via_bot:flags.40?User = pyrogram.Message; pyrogram.messageEntity#b0700004 flags:# type:string offset:int length:int url:flags.0?string user:flags.1?User = pyrogram.MessageEntity; -pyrogram.photoSize#b0700005 flags:# file_id:string width:int height:int file_size:flags.0?int date:flags.1?int = pyrogram.PhotoSize; +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; From d9b3d671266a51f95a899ede2e19bd42f330795f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:44:05 +0200 Subject: [PATCH 258/285] Update download_media docstrings --- pyrogram/client/client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 330940d7..cb3c30d6 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -3022,8 +3022,9 @@ class Client: """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. @@ -3038,6 +3039,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``): @@ -3079,6 +3081,11 @@ class Client: pyrogram.Sticker )): media = message + elif isinstance(message, str): + media = pyrogram.Document( + file_id=message, + file_size=0 + ) else: return From b4b53735050191642fc0f025c69af2339c1a38c3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:49:00 +0200 Subject: [PATCH 259/285] Deprecate download_photo --- docs/source/pyrogram/Client.rst | 1 - pyrogram/client/client.py | 43 --------------------------------- 2 files changed, 44 deletions(-) diff --git a/docs/source/pyrogram/Client.rst b/docs/source/pyrogram/Client.rst index b626e745..806950a8 100644 --- a/docs/source/pyrogram/Client.rst +++ b/docs/source/pyrogram/Client.rst @@ -35,7 +35,6 @@ Client send_chat_action send_sticker download_media - download_photo get_user_profile_photos edit_message_text edit_message_caption diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index cb3c30d6..2e43ac9f 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -3099,49 +3099,6 @@ class Client: 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 - )] - ) - - return self.download_media(photo, file_name, block) - def add_contacts(self, contacts: list): """Use this method to add contacts to your Telegram address book. From cd9f44183e51d6971da81a0367c8c63c53feb4ee Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:51:32 +0200 Subject: [PATCH 260/285] Make send_location return the new type --- pyrogram/client/client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 2e43ac9f..2eec0eb1 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2162,12 +2162,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.InputMediaGeoPoint( @@ -2183,6 +2183,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, From 4bbdadaa1714d72d26b96ed0d1111cec8e5559d3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:53:26 +0200 Subject: [PATCH 261/285] Don't pass an empty string, use None instead --- pyrogram/client/message_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index c04d599f..85266bff 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -277,7 +277,7 @@ def parse_message( ), title=media.title, address=media.address, - foursquare_id=media.venue_id + foursquare_id=media.venue_id or None ) elif isinstance(media, types.MessageMediaDocument): doc = media.document From 58c9f321b429bd4e2abcaaf5b8431073e84b53c6 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:53:38 +0200 Subject: [PATCH 262/285] Make send_venue return the new type --- pyrogram/client/client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 2eec0eb1..79a0a7d0 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2231,12 +2231,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( @@ -2257,6 +2257,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_contact(self, chat_id: int or str, phone_number: str, From 797e8ba4e98ed58bf26b8ea957e3fbe6f11c53c9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:56:43 +0200 Subject: [PATCH 263/285] Don't pass empty last_name or user_id, pass None instead --- pyrogram/client/message_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 85266bff..4ae4180e 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -266,8 +266,8 @@ def parse_message( contact = pyrogram.Contact( phone_number=media.phone_number, first_name=media.first_name, - last_name=media.last_name, - user_id=media.user_id + last_name=media.last_name or None, + user_id=media.user_id or None ) elif isinstance(media, types.MessageMediaVenue): venue = pyrogram.Venue( From 20bd4fb605370b8ced2929f713fa5bd59d405d10 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 11:56:52 +0200 Subject: [PATCH 264/285] Make send_contact return the new type --- pyrogram/client/client.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 79a0a7d0..a9517458 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2268,7 +2268,7 @@ class Client: 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. @@ -2297,12 +2297,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( @@ -2317,6 +2317,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, From fbc8cafe4d512025d66ef99a70ffea02e34e4c3c Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:01:42 +0200 Subject: [PATCH 265/285] Make edit_message return the new type --- pyrogram/client/client.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index a9517458..1add1790 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2345,6 +2345,9 @@ class Client: progress (``int``, optional): Progress of the upload process. + Returns: + On success, True is returned. + Raises: :class:`Error ` """ @@ -2420,12 +2423,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, @@ -2434,6 +2440,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, From 06685cfe15985d9e709a6ab978e94117dbdb9ec3 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:03:31 +0200 Subject: [PATCH 266/285] Make edit_message_caption return the new type --- pyrogram/client/client.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 1add1790..f9bcbfa3 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2472,12 +2472,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, @@ -2485,6 +2488,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, From d0ddb63830f7af59c8ca0e1d19a96c2ce6024e95 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:19:04 +0200 Subject: [PATCH 267/285] Use raw functions when getting messages --- pyrogram/client/message_parser.py | 40 ++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 4ae4180e..4433e6c0 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -19,7 +19,7 @@ from struct import pack import pyrogram -from pyrogram.api import types +from pyrogram.api import types, functions from .utils import encode # TODO: Organize the code better? @@ -173,6 +173,40 @@ def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.Pho ) +def get_message_reply(client, chat_id: int or str, message_id: int): + peer = client.resolve_peer(chat_id) + message_id = [types.InputMessageReplyTo(message_id)] + + if isinstance(peer, types.InputPeerChannel): + rpc = functions.channels.GetMessages( + channel=peer, + id=message_id + ) + else: + rpc = functions.messages.GetMessages( + id=message_id + ) + + return client.send(rpc) + + +def get_message_pinned(client, chat_id: int or str): + peer = client.resolve_peer(chat_id) + message_id = [types.InputMessagePinned()] + + if isinstance(peer, types.InputPeerChannel): + rpc = functions.channels.GetMessages( + channel=peer, + id=message_id + ) + else: + rpc = functions.messages.GetMessages( + id=message_id + ) + + return client.send(rpc) + + # TODO: Reorganize code, maybe split parts as well def parse_message( client, @@ -465,7 +499,7 @@ def parse_message( ) if message.reply_to_msg_id and replies: - reply_to_message = client.get_messages(m.chat.id, [message.reply_to_msg_id]) + reply_to_message = get_message_reply(client, m.chat.id, message.id) message = reply_to_message.messages[0] users = {i.id: i for i in reply_to_message.users} @@ -573,7 +607,7 @@ def parse_message_service( ) if isinstance(action, types.MessageActionPinMessage): - pin_message = client.get_messages(m.chat.id, [message.reply_to_msg_id]) + pin_message = get_message_pinned(client, m.chat.id) message = pin_message.messages[0] users = {i.id: i for i in pin_message.users} From f47f903f7f85945c04bcb077332477d9e93b7d7f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:42:31 +0200 Subject: [PATCH 268/285] Make get_messages return the correct type --- pyrogram/client/client.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index f9bcbfa3..82605595 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -3332,4 +3332,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 From e210b22aaac41d9cb968d62dc41db1bd0944639f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:43:02 +0200 Subject: [PATCH 269/285] Fix wrong users when joining by link --- pyrogram/client/message_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 4433e6c0..5669ef0c 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -534,7 +534,7 @@ def parse_message_service( if isinstance(action, types.MessageActionChatAddUser): new_chat_members = [parse_user(users[i]) for i in action.users] elif isinstance(action, types.MessageActionChatJoinedByLink): - new_chat_members = [parse_user(users[action.inviter_id])] + 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): From a547eb6fa15b69cb3084eee6e8f693ca3407f7b0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:52:32 +0200 Subject: [PATCH 270/285] Update get_messages docstrings --- pyrogram/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 82605595..937ff3d8 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -3314,7 +3314,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 ` From fcc595c5867579cdf3f9b35e95a9590b96a6071f Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 13:28:19 +0200 Subject: [PATCH 271/285] Update version --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index d5e26b43..a6e93332 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès Date: Mon, 16 Apr 2018 13:28:53 +0200 Subject: [PATCH 272/285] Set correct link for the Message type --- pyrogram/client/client.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 937ff3d8..9ed6261f 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -1189,7 +1189,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -1318,7 +1318,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -1439,7 +1439,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -1545,7 +1545,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -1682,7 +1682,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -1810,7 +1810,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -1927,7 +1927,7 @@ class Client: The size of the file. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -2162,7 +2162,7 @@ class Client: If the message is a reply, ID of the original message Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -2231,7 +2231,7 @@ class Client: If the message is a reply, ID of the original message Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -2297,7 +2297,7 @@ class Client: If the message is a reply, ID of the original message. Returns: - On success, the sent :obj:`Message ` is returned. + On success, the sent :obj:`Message ` is returned. Raises: :class:`Error ` @@ -2424,7 +2424,7 @@ class Client: Disables link previews for links in this message. Returns: - On success, the edited :obj:`Message ` is returned. + On success, the edited :obj:`Message ` is returned. Raises: :class:`Error ` @@ -2473,7 +2473,7 @@ class Client: Defaults to Markdown. Returns: - On success, the edited :obj:`Message ` is returned. + On success, the edited :obj:`Message ` is returned. Raises: :class:`Error ` @@ -3314,7 +3314,7 @@ class Client: A list of Message identifiers in the chat specified in *chat_id*. Returns: - On success, a list of the requested :obj:`Messages ` is returned. + On success, a list of the requested :obj:`Messages ` is returned. Raises: :class:`Error ` From 0295954d66cf623832861dfbb7735837f0fe3e47 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 19:48:50 +0200 Subject: [PATCH 273/285] Update docs --- docs/source/resources/UpdateHandling.rst | 53 +++++++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/source/resources/UpdateHandling.rst b/docs/source/resources/UpdateHandling.rst index a6c73187..2b2c05d9 100644 --- a/docs/source/resources/UpdateHandling.rst +++ b/docs/source/resources/UpdateHandling.rst @@ -3,7 +3,10 @@ Update Handling 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. +from, one for each kind of update: + +- `MessageHandler <../pyrogram/MessageHandler.html>`_ +- `RawUpdateHandler <../pyrogram/RawUpdateHandler.html>`_ Registering an Handler ---------------------- @@ -31,7 +34,7 @@ We shall examine the :obj:`MessageHandler `, which will 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 a separate file. + This is useful, for example, when you want to keep your callback functions in separate files. .. code-block:: python @@ -55,8 +58,8 @@ 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 handle only messages containing an :obj:`Audio ` - object: +- This example will show you how to **only** handle messages containing an + :obj:`Audio ` object and filter out any other message: .. code-block:: python @@ -71,7 +74,7 @@ For a finer grained control over what kind of messages will be allowed or not in .. code-block:: python - from pyrogram import Filters, Messagehandler + from pyrogram import Filters, MessageHandler def my_handler(client, message): @@ -128,7 +131,7 @@ can also accept arguments: def my_handler(client, message): print(message) -More handlers using different filters can also live together: +More handlers using different filters can also live together. .. code-block:: python @@ -145,3 +148,41 @@ More handlers using different filters can also live together: @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 From e2c101d28e73b7180b95b49778b73899b92d9981 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 19:56:24 +0200 Subject: [PATCH 274/285] Update README.rst --- README.rst | 159 ++++++++++++++--------------------------------------- 1 file changed, 40 insertions(+), 119 deletions(-) diff --git a/README.rst b/README.rst index ae6f084f..c6e18e6e 100644 --- a/README.rst +++ b/README.rst @@ -1,158 +1,79 @@ |header| -Table of Contents -================= +Pyrogram +======== -- `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 and feel free to join our Community_! +- For other requests you can send me 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 From 3947453bc7ef4e54562f856f262dc4fd5962b65d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 20:01:59 +0200 Subject: [PATCH 275/285] Fix RawUpdateHandler page title --- docs/source/pyrogram/RawUpdateHandler.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/pyrogram/RawUpdateHandler.rst b/docs/source/pyrogram/RawUpdateHandler.rst index 3d74a34b..a6d21ef3 100644 --- a/docs/source/pyrogram/RawUpdateHandler.rst +++ b/docs/source/pyrogram/RawUpdateHandler.rst @@ -1,4 +1,4 @@ -\RawUpdateHandler +RawUpdateHandler ================ .. autoclass:: pyrogram.RawUpdateHandler From ebe806e2b7736d25ee3a9402e4295d1eb5977881 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Mon, 16 Apr 2018 20:32:34 +0200 Subject: [PATCH 276/285] Add missing notice --- docs/source/sitemap.py | 18 ++++++++++++++++++ pyrogram/client/syncer.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/source/sitemap.py b/docs/source/sitemap.py index 338a4b8c..539bac0d 100644 --- a/docs/source/sitemap.py +++ b/docs/source/sitemap.py @@ -1,3 +1,21 @@ +# 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 diff --git a/pyrogram/client/syncer.py b/pyrogram/client/syncer.py index 4589103b..e92f4084 100644 --- a/pyrogram/client/syncer.py +++ b/pyrogram/client/syncer.py @@ -1,3 +1,21 @@ +# 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 base64 import json import logging From d78dfb4f9eeb10fd173291ae502c8052e47f6a16 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 17 Apr 2018 00:26:30 +0200 Subject: [PATCH 277/285] Don't use raw functions when getting messages --- pyrogram/client/message_parser.py | 61 +++---------------------------- 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index 5669ef0c..da12fae7 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -19,7 +19,7 @@ from struct import pack import pyrogram -from pyrogram.api import types, functions +from pyrogram.api import types from .utils import encode # TODO: Organize the code better? @@ -173,40 +173,6 @@ def parse_thumb(thumb: types.PhotoSize or types.PhotoCachedSize) -> pyrogram.Pho ) -def get_message_reply(client, chat_id: int or str, message_id: int): - peer = client.resolve_peer(chat_id) - message_id = [types.InputMessageReplyTo(message_id)] - - if isinstance(peer, types.InputPeerChannel): - rpc = functions.channels.GetMessages( - channel=peer, - id=message_id - ) - else: - rpc = functions.messages.GetMessages( - id=message_id - ) - - return client.send(rpc) - - -def get_message_pinned(client, chat_id: int or str): - peer = client.resolve_peer(chat_id) - message_id = [types.InputMessagePinned()] - - if isinstance(peer, types.InputPeerChannel): - rpc = functions.channels.GetMessages( - channel=peer, - id=message_id - ) - else: - rpc = functions.messages.GetMessages( - id=message_id - ) - - return client.send(rpc) - - # TODO: Reorganize code, maybe split parts as well def parse_message( client, @@ -499,16 +465,8 @@ def parse_message( ) if message.reply_to_msg_id and replies: - reply_to_message = get_message_reply(client, m.chat.id, message.id) - - message = reply_to_message.messages[0] - users = {i.id: i for i in reply_to_message.users} - chats = {i.id: i for i in reply_to_message.chats} - - if isinstance(message, types.Message): - m.reply_to_message = parse_message(client, message, users, chats, replies - 1) - elif isinstance(message, types.MessageService): - m.reply_to_message = parse_message_service(client, message, users, chats) + m.reply_to_message = client.get_messages(m.chat.id, [message.reply_to_msg_id]) + m.reply_to_message = m.reply_to_message[0] if m.reply_to_message else None return m @@ -607,16 +565,7 @@ def parse_message_service( ) if isinstance(action, types.MessageActionPinMessage): - pin_message = get_message_pinned(client, m.chat.id) - - message = pin_message.messages[0] - users = {i.id: i for i in pin_message.users} - chats = {i.id: i for i in pin_message.chats} - - if isinstance(message, types.Message): - m.pinned_message = parse_message(client, message, users, chats) - elif isinstance(message, types.MessageService): - # TODO: We can't pin a service message, can we? - m.pinned_message = parse_message_service(client, message, users, chats) + m.pinned_message = client.get_messages(m.chat.id, [message.reply_to_msg_id]) + m.pinned_message = m.pinned_message[0] if m.pinned_message else None return m From 9dff99cf3640f8b37436b93c727c7002f7a315a2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Tue, 17 Apr 2018 15:17:40 +0200 Subject: [PATCH 278/285] Add set_name and emoji to Stickers --- pyrogram/client/message_parser.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pyrogram/client/message_parser.py b/pyrogram/client/message_parser.py index da12fae7..51eba3ae 100644 --- a/pyrogram/client/message_parser.py +++ b/pyrogram/client/message_parser.py @@ -19,7 +19,7 @@ from struct import pack import pyrogram -from pyrogram.api import types +from pyrogram.api import types, functions from .utils import encode # TODO: Organize the code better? @@ -393,6 +393,14 @@ def parse_message( ) elif types.DocumentAttributeSticker in attributes: image_size_attributes = attributes[types.DocumentAttributeImageSize] + sticker_attribute = attributes[types.DocumentAttributeSticker] + + if isinstance(sticker_attribute.stickerset, types.InputStickerSetID): + set_name = client.send( + functions.messages.GetStickerSet(sticker_attribute.stickerset) + ).set.short_name + else: + set_name = None sticker = pyrogram.Sticker( file_id=encode( @@ -407,7 +415,9 @@ def parse_message( width=image_size_attributes.w, height=image_size_attributes.h, thumb=parse_thumb(doc.thumb), - # TODO: Emoji, set_name and mask_position + # TODO: mask_position + set_name=set_name, + emoji=sticker_attribute.alt, file_size=doc.size, mime_type=doc.mime_type, file_name=file_name, From 0bba5daea484981f4cb803c1009d695381f6281b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 11:15:09 +0200 Subject: [PATCH 279/285] Update docstrings --- pyrogram/client/client.py | 6 ++++-- pyrogram/client/handlers/handlers.py | 8 +++++--- pyrogram/client/input_media_photo.py | 2 +- pyrogram/client/input_media_video.py | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 9ed6261f..a1033629 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -2012,7 +2012,8 @@ class Client: For a private channel/supergroup you can use its *t.me/joinchat/* link. media (``list``): - A list containing either :obj:`pyrogram.InputMediaPhoto` or :obj:`pyrogram.InputMediaVideo` objects + A list containing either :obj:`InputMediaPhoto ` or + :obj:`InputMediaVideo ` objects describing photos and videos to be sent, must include 2–10 items. disable_notification (``bool``, optional): @@ -3128,7 +3129,8 @@ class Client: elif isinstance(message, str): media = pyrogram.Document( file_id=message, - file_size=0 + file_size=0, + mime_type="" ) else: return diff --git a/pyrogram/client/handlers/handlers.py b/pyrogram/client/handlers/handlers.py index 33fff85d..e909e218 100644 --- a/pyrogram/client/handlers/handlers.py +++ b/pyrogram/client/handlers/handlers.py @@ -20,8 +20,9 @@ from .handler import Handler class MessageHandler(Handler): - """The Message handler class. It is used to handle text, media and service messages coming from - any chat (private, group, channel). + """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``): @@ -52,7 +53,8 @@ class MessageHandler(Handler): class RawUpdateHandler(Handler): - """The Raw Update handler class. It is used to handle raw updates. + """The Raw Update handler class. Used to handle raw updates. It is intended to be used with + :meth:`add_handler() ` Args: callback (``callable``): diff --git a/pyrogram/client/input_media_photo.py b/pyrogram/client/input_media_photo.py index 597dfa75..3f0e4488 100644 --- a/pyrogram/client/input_media_photo.py +++ b/pyrogram/client/input_media_photo.py @@ -19,7 +19,7 @@ class InputMediaPhoto: """This object represents a photo to be sent inside an album. - It is intended to be used with :obj:`send_media_group `. + It is intended to be used with :obj:`send_media_group() `. Args: media (:obj:`str`): diff --git a/pyrogram/client/input_media_video.py b/pyrogram/client/input_media_video.py index a25a65df..7ad5ce50 100644 --- a/pyrogram/client/input_media_video.py +++ b/pyrogram/client/input_media_video.py @@ -19,7 +19,7 @@ class InputMediaVideo: """This object represents a video to be sent inside an album. - It is intended to be used with :obj:`send_media_group `. + It is intended to be used with :obj:`send_media_group() `. Args: media (:obj:`str`): From e1d617b680161ac8437b255abc8b4aa80b7699c9 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 11:17:05 +0200 Subject: [PATCH 280/285] Move some errors to their right place --- compiler/error/source/400_BAD_REQUEST.tsv | 4 +--- compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index ae51c7f5..dfb2c062 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -57,6 +57,4 @@ 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 -RPC_CALL_FAIL The method can't be called because Telegram is having internal problems. Please try again later -RPC_MCGET_FAIL The method can't be called because Telegram is having internal problems. Please try again later \ No newline at end of file +EMAIL_INVALID The email provided is invalid \ 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..dcda1e38 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 The method can't be called because Telegram is having internal problems. Please try again later +RPC_MCGET_FAIL The method can't be called because Telegram is having internal problems. Please try again later \ No newline at end of file From f5efb6672aa5d633130e741fb0c0f173f422f71d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 11:24:19 +0200 Subject: [PATCH 281/285] Add some new RPCErrors as well --- compiler/error/source/400_BAD_REQUEST.tsv | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index dfb2c062..2ec8b7fd 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -57,4 +57,6 @@ 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 \ No newline at end of file +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 From 639828f5508dff9e3bd744bf0c5ea43a04a6e8f0 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 15:17:46 +0200 Subject: [PATCH 282/285] Retry on internal server errors, up to MAX_RETRIES times Also add support for custom retry count --- pyrogram/session/session.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) 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) From ba1ffdb92b6b5462b46a2c229fd29179335769f2 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 15:18:51 +0200 Subject: [PATCH 283/285] More concise error description --- compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv index dcda1e38..dcdc1ee0 100644 --- a/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv +++ b/compiler/error/source/500_INTERNAL_SERVER_ERROR.tsv @@ -1,4 +1,4 @@ id message AUTH_RESTART User authorization has restarted -RPC_CALL_FAIL The method can't be called because Telegram is having internal problems. Please try again later -RPC_MCGET_FAIL The method can't be called because Telegram is having internal problems. Please try again later \ No newline at end of file +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 From 1e7e1abdac0dbb8137adb2e4bfc4e3e6bddc9af7 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 15:34:29 +0200 Subject: [PATCH 284/285] Update README.rst --- README.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c6e18e6e..67dbd3a5 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ |header| -Pyrogram -======== +Pyrogram |twitter| +================== .. code-block:: python @@ -50,8 +50,8 @@ Getting Started - 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 and feel free to join our Community_! -- For other requests you can send me an Email_ or a Message_. +- 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 ------------ @@ -111,6 +111,9 @@ Copyright & 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 From 069dbb3e7a6d69c75a41443e6b75100a33831054 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 18 Apr 2018 15:38:01 +0200 Subject: [PATCH 285/285] Update to v0.7.0 --- pyrogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index a6e93332..4dd090be 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès