diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 675b6d18..75cfc115 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,18 +10,6 @@ updates: assignees: - alexander-akhmetov ignore: - - dependency-name: pylint - versions: - - 2.7.0 - - 2.7.3 - - 2.7.4 - - 2.8.1 - - dependency-name: black - versions: - - 21.4b0 - - dependency-name: flake8 - versions: - - 3.9.0 - dependency-name: ipdb versions: - 0.13.5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 917914ac..bebb76c2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 3f59f7f3..13007138 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ MANIFEST *.pyc .DS_Store .vscode +telegram/_version.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..1f2b89db --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/source/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6296a280..50d9f8fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,33 +2,31 @@ Pull requests are welcome! -Feel free to open an issue if you found a bug, have new ideas, suggestions or found a mistake -in [documentation](https://python-telegram.readthedocs.io/en/latest/). +Feel free to open an issue if you find a bug, have new ideas, suggestions, +or spot a mistake in the [documentation](https://python-telegram.readthedocs.io/en/latest/). ## Reporting bugs We use [GitHub Issues](https://github.com/alexander-akhmetov/python-telegram/issues) to track -bugs. If you found a bug, please, open a new issue. +bugs. If you find a bug, please open a new issue. -Try to include steps to reproduce and detailed description of the bug and maybe -some sample code. +Try to include steps to reproduce the bug, a detailed description, and some sample code if possible. ## Pull request process 1. Fork the repository and create a new branch from `master`. -2. Make your changes and do not forget about new tests :) +2. Make your changes and don't forget to add new tests :) 3. Ensure the tests pass with your changes. 4. Create a new PR! ## Coding style -The project uses [black](https://github.com/psf/black) as a autoformatter tool -and checker and a few linters. +The project uses [ruff](https://docs.astral.sh/ruff/) as an autoformatter and linter. ## Tests -To run tests you have to install [tox](https://tox.readthedocs.io/en/latest/). +To run tests you need to install [tox](https://tox.readthedocs.io/en/latest/). Run tests: @@ -36,8 +34,8 @@ Run tests: tox ``` -Run a specific test using python 3.11: +Run a specific test using python 3.12: ```shell -tox -e py311 -- -k test_add_message_handler +tox -e py312 -- -k test_add_message_handler ``` diff --git a/Dockerfile b/Dockerfile index 0158d4ea..3f69a6bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.0-bullseye +FROM python:3.12.4-slim-bullseye RUN python3 -m pip install python-telegram diff --git a/Makefile b/Makefile index 755af2c9..cd0f456a 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,37 @@ -docker-build: +.PHONY: docker/build +docker/build: docker build -f Dockerfile . -t akhmetov/python-telegram -docker-send-message: +.PHONY: docker/send-message +docker/send-message: docker run -i -t \ -v /tmp/docker-python-telegram/:/tmp/ \ akhmetov/python-telegram \ python3 /app/examples/send_message.py $(API_ID) $(API_HASH) $(PHONE) $(CHAT_ID) $(TEXT) -docker-echo-bot: +.PHONY: docker/echo-bot +docker/echo-bot: docker run -i -t \ -v /tmp/docker-python-telegram/:/tmp/ \ akhmetov/python-telegram \ python3 /app/examples/echo_bot.py $(API_ID) $(API_HASH) $(PHONE) -docker-get-instant-view: +.PHONY: docker/get-instant-view +docker/get-instant-view: docker run -i -t \ -v /tmp/docker-python-telegram/:/tmp/ \ akhmetov/python-telegram \ python3 /app/examples/echo_bot.py $(API_ID) $(API_HASH) $(PHONE) -release-pypi: - test -n "$(VERSION)" - python setup.py sdist - twine upload dist/python-telegram-$(VERSION).tar.gz +.PHONY: clean +clean: + rm -rf dist + +.PHONY: build-pypi +build-pypi: clean + python3 -m build + +.PHONY: release-pypi +release-pypi:build-pypi + twine upload dist/* diff --git a/README.md b/README.md index 8a9ea6e7..bcbb1722 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It helps you build your own Telegram clients. ## Installation -This library requires Python 3.8+ and Linux or MacOS. Windows is not supported. +This library requires Python 3.9+ and Linux or MacOS. Windows is not supported. ```shell pip install python-telegram @@ -22,6 +22,13 @@ pip install python-telegram See [documentation](http://python-telegram.readthedocs.io/en/latest/#installation) for more details. +### tdlib + +`python-telegram` comes with a precompiled `tdlib` library for Linux and MacOS. But it is highly recommended to [compile](https://tdlib.github.io/td/build.html) it yourself. +The precompiled library may not work on some systems, it is dynamically linked and requires specific versions of additional libraries. + +```shell + ### Docker This library has a [docker image](https://hub.docker.com/r/akhmetov/python-telegram/): @@ -33,9 +40,9 @@ docker run -i -t --rm \ python3 /app/examples/send_message.py $(API_ID) $(API_HASH) $(PHONE) $(CHAT_ID) $(TEXT) ``` -## How to use +## How to use the library -Have a look at the [tutorial](http://python-telegram.readthedocs.io/en/latest/tutorial.html) :) +Check out the [tutorial](http://python-telegram.readthedocs.io/en/latest/tutorial.html) for more details. Basic example: @@ -52,19 +59,20 @@ tg = Telegram( ) tg.login() -# if this is the first run, library needs to preload all chats -# otherwise the message will not be sent +# If this is the first run, the library needs to preload all chats. +# Otherwise, the message will not be sent. result = tg.get_chats() result.wait() chat_id: int result = tg.send_message(chat_id, Spoiler('Hello world!')) -# `tdlib` is asynchronous, so `python-telegram` always returns you an `AsyncResult` object. + +# `tdlib` is asynchronous, so `python-telegram` always returns an `AsyncResult` object. # You can receive a result with the `wait` method of this object. result.wait() print(result.update) -tg.stop() # you must call `stop` at the end of the script +tg.stop() # You must call `stop` at the end of the script. ``` You can also use `call_method` to call any [tdlib method](https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1_function.html): @@ -73,11 +81,11 @@ You can also use `call_method` to call any [tdlib method](https://core.telegram. tg.call_method('getUser', params={'user_id': user_id}) ``` -More examples you can find in the [/examples/ directory](/examples/). +More examples can be found in the [/examples/ directory](/examples/). --- -More information in the [documentation](http://python-telegram.readthedocs.io). +More information is available in the [documentation](http://python-telegram.readthedocs.io). ## Development diff --git a/docs/requirements.txt b/docs/requirements.txt index 358c674f..b8c259ce 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx sphinx-autobuild autodoc +sphinx-immaterial diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ddcb2ffc..32dcd9c9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,7 +4,10 @@ Changelog [unreleased] -- Python 3.7 is no longer supported. +[0.19.0] 2024-06-23 + +- Python versions 3.7 and 3.8 are no longer supported. +- tdlib 1.8.31. [0.18.0] - 2023-03-13 diff --git a/docs/source/conf.py b/docs/source/conf.py index 9e4732bc..fae538f0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,21 +14,22 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath("../..")) # -- Project information ----------------------------------------------------- -project = 'python-telegram' -copyright = '2018, Alexander Akhmetov' -author = 'Alexander Akhmetov' +project = "python-telegram" +copyright = "2024, Alexander Akhmetov" +author = "Alexander Akhmetov" html_show_sourcelink = False # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" # -- General configuration --------------------------------------------------- @@ -40,21 +41,19 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'sphinx.ext.autodoc', -] +extensions = ["sphinx.ext.autodoc", "sphinx_immaterial"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -69,7 +68,7 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- @@ -77,32 +76,36 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "sphinx_immaterial" # 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 = { - 'logo_name': '123', - 'github_user': 'alexander-akhmetov', - 'github_repo': 'python-telegram', - 'github_button': True, - 'github_type': 'star', - 'extra_nav_links': { - 'Main page': '/', - '1': '', - 'GitHub': 'https://github.com/alexander-akhmetov/python-telegram', - 'DockerHub': 'https://hub.docker.com/r/akhmetov/python-telegram/', - 'PyPi': 'https://pypi.org/project/python-telegram/', - '2': '', - } + "logo_name": "123", + "github_user": "alexander-akhmetov", + "github_repo": "python-telegram", + "github_button": True, + "github_type": "star", + "extra_nav_links": { + "Main page": "/", + "1": "", + "GitHub": "https://github.com/alexander-akhmetov/python-telegram", + "DockerHub": "https://hub.docker.com/r/akhmetov/python-telegram/", + "PyPi": "https://pypi.org/project/python-telegram/", + "2": "", + }, + "palette": { + "primary": "deep-orange", + "accent": "deep-orange", + }, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -113,19 +116,18 @@ # 'searchbox.html']``. # html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'searchbox.html', + "**": [ + "about.html", + "navigation.html", + "searchbox.html", ] } - # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'python-telegramdoc' +htmlhelp_basename = "python-telegramdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -134,15 +136,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -152,8 +151,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'python-telegram.tex', 'python-telegram Documentation', - 'Alexander Akhmetov', 'manual'), + ( + master_doc, + "python-telegram.tex", + "python-telegram Documentation", + "Alexander Akhmetov", + "manual", + ), ] @@ -161,10 +165,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'python-telegram', 'python-telegram Documentation', - [author], 1) -] +man_pages = [(master_doc, "python-telegram", "python-telegram Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -173,9 +174,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'python-telegram', 'python-telegram Documentation', - author, 'python-telegram', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "python-telegram", + "python-telegram Documentation", + author, + "python-telegram", + "One line description of project.", + "Miscellaneous", + ), ] diff --git a/docs/source/index.rst b/docs/source/index.rst index c9408c1d..f6dc6aee 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,19 +13,19 @@ Welcome to python-telegram's documentation proxy changelog -Client for the `tdlib `_ library (at very early stage :) ). +Client for the `tdlib `_ library. Installation ------------ -At first you must install ``tdlib``. +First you need to install ``tdlib``. How to build tdlib ~~~~~~~~~~~~~~~~~~ `Official documentation `_ -Do not forget install it after: +After building the library, you need to install it: .. code-block:: bash @@ -35,26 +35,26 @@ Do not forget install it after: Library installation ~~~~~~~~~~~~~~~~~~~~ -This library works with Python 3.8+ only. +This library requires Python 3.9 or higher. .. code-block:: bash python3 -m pip install python-telegram -After you must `register `_ a new Telegram application. +Next, you need to `register `_ a new Telegram application. Now you can start using the library: :ref:`tutorial`. .. note:: - You can find more examples `here `_. + More examples can be found `here `_. .. note:: - The ``tdlib`` binary for Linux provided by ``python-shortcuts`` is built on Ubuntu with libc since version ``0.10.0``. Before ``0.10.0``, Alpine Linux was used with ``musl``. + The ``tdlib`` binary for Linux provided by ``python-shortcuts`` has been built on Ubuntu with libc strating from version ``0.10.0``. Versions before ``0.10.0`` were built on Alpine Linux with ``musl``. Docker ------ -This library has a `docker image `_ +A Docker image for this library is available `here `_ .. code-block:: bash diff --git a/docs/source/proxy.rst b/docs/source/proxy.rst index 667aa828..feb73bbf 100644 --- a/docs/source/proxy.rst +++ b/docs/source/proxy.rst @@ -7,7 +7,7 @@ Proxy How to use a proxy with tdlib ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Define proxy parameters and pass them to ``Telegram``: +To use a proxy with tdlib, define the proxy parameters and pass them to the ``Telegram`` class: .. code-block:: python diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 53386af0..ff9c48ac 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -13,9 +13,9 @@ Install the library: python3 -m pip install python-telegram -Let's create a simple echo-bot, which sends "pong" when it receives "ping". +Let's create a simple echo-bot that sends "pong" when it receives "ping". -Initialize a new telegram client with your credentials: +Initialize a new Telegram client with your credentials: .. code-block:: python @@ -29,32 +29,31 @@ Initialize a new telegram client with your credentials: ) .. note:: - The library (actually ``tdlib``) stores messages database and received files in the ``/tmp/.tdlib_files/{phone_number}/``. + The library configures ``tdlib`` to store the messages database and received files in the ``/tmp/.tdlib_files/{phone_number}/``. You can change this behaviour with the ``files_directory`` parameter. .. note:: - You can use bot tokens: just pass ``bot_token`` instead of ``phone``. + You can pass bot tokens by passing ``bot_token`` instead of ``phone``. -After that, you have to login: +Now you need to login to the Telegram. You can do it by calling the `login` method: .. code-block:: python tg.login() -In this example we use a blocking version of the `login` function. You can find an example for non-blocking usage here: :ref:`non_blocking_login`. -Telegram will send you a code in an SMS or as a Telegram message. If you have enabled 2FA, you will be asked for your password too. After successful login you can start using the library: +In this example, we use a blocking version of the ``login`` function. You can find an example for non-blocking usage here: :ref:`non_blocking_login`. +Telegram will send you a code via SMS or as a Telegram message. If you have enabled 2FA, you will also be asked for your password. After successful login, you can start using the library: .. code-block:: python - # this function will be called - # for each received message + # This function will be called for each received message. def new_message_handler(update): print('New message!') tg.add_message_handler(new_message_handler) - tg.idle() # blocking waiting for CTRL+C + tg.idle() # Blocking, waiting for CTRL+C. -This code adds a new message handler which prints a simple text every time it receives a new message. +This code adds a new message handler that prints a simple text every time it receives a new message. ``tg.idle()`` is neccessary to block your script and wait for an exit shortcut (``CTRL+C``). If you run this code, you will see something like that: @@ -69,7 +68,7 @@ Let's add more logic to the message handler: .. code-block:: python def new_message_handler(update): - # we want to process only text messages + # We want to process only text messages. message_content = update['message']['content'].get('text', {}) message_text = message_content.get('text', '').lower() is_outgoing = update['message']['is_outgoing'] @@ -97,7 +96,7 @@ Full code of our new bot: tg.login() def new_message_handler(update): - # we want to process only text messages + # We want to process only text messages. message_content = update['message']['content'].get('text', {}) message_text = message_content.get('text', '').lower() is_outgoing = update['message']['is_outgoing'] diff --git a/examples/bot_login.py b/examples/bot_login.py index 5b1767af..b229862b 100644 --- a/examples/bot_login.py +++ b/examples/bot_login.py @@ -8,7 +8,7 @@ def bot_get_me(api_id, api_hash, token): api_id=api_id, api_hash=api_hash, bot_token=token, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", ) # you must call login method before others tg.login() @@ -19,10 +19,10 @@ def bot_get_me(api_id, api_hash, token): tg.stop() -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps - parser.add_argument('api_hash', help='API hash') - parser.add_argument('token', help='Bot token') + parser.add_argument("api_id", help="API id") # https://my.telegram.org/apps + parser.add_argument("api_hash", help="API hash") + parser.add_argument("token", help="Bot token") args = parser.parse_args() bot_get_me(args.api_id, args.api_hash, args.token) diff --git a/examples/chat_stats.py b/examples/chat_stats.py index 17d088d7..2f81681b 100644 --- a/examples/chat_stats.py +++ b/examples/chat_stats.py @@ -28,50 +28,50 @@ def retreive_messages(telegram, chat_id, receive_limit): ) response.wait() - for message in response.update['messages']: - if message['content']['@type'] == 'messageText': - stats_data[message['id']] = message['content']['text']['text'] - from_message_id = message['id'] + for message in response.update["messages"]: + if message["content"]["@type"] == "messageText": + stats_data[message["id"]] = message["content"]["text"]["text"] + from_message_id = message["id"] total_messages = len(stats_data) - if total_messages > receive_limit or not response.update['total_count']: + if total_messages > receive_limit or not response.update["total_count"]: receive = False - print(f'[{total_messages}/{receive_limit}] received') + print(f"[{total_messages}/{receive_limit}] received") return stats_data def print_stats(stats_data, most_common_count): words = Counter() - translator = str.maketrans('', '', string.punctuation) + translator = str.maketrans("", "", string.punctuation) for _, message in stats_data.items(): - for word in message.split(' '): + for word in message.split(" "): word = word.translate(translator).lower() if len(word) > 3: words[word] += 1 for word, count in words.most_common(most_common_count): - print(f'{word}: {count}') + print(f"{word}: {count}") -if __name__ == '__main__': +if __name__ == "__main__": setup_logging(level=logging.INFO) parser = argparse.ArgumentParser() - parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps - parser.add_argument('api_hash', help='API hash') - parser.add_argument('phone', help='Phone') - parser.add_argument('chat_id', help='Chat ID') - parser.add_argument('--limit', help='Messages to retrieve', type=int, default=1000) - parser.add_argument('--most-common', help='Most common count', type=int, default=30) + parser.add_argument("api_id", help="API id") # https://my.telegram.org/apps + parser.add_argument("api_hash", help="API hash") + parser.add_argument("phone", help="Phone") + parser.add_argument("chat_id", help="Chat ID") + parser.add_argument("--limit", help="Messages to retrieve", type=int, default=1000) + parser.add_argument("--most-common", help="Most common count", type=int, default=30) args = parser.parse_args() tg = Telegram( api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", ) # you must call login method before others tg.login() diff --git a/examples/clear_group_messages.py b/examples/clear_group_messages.py index 799b150b..b2ea29fe 100644 --- a/examples/clear_group_messages.py +++ b/examples/clear_group_messages.py @@ -8,11 +8,13 @@ logger = logging.getLogger(__name__) + def confirm(message): - sure = input(message + ' ') - if sure.lower() not in ['y', 'yes']: + sure = input(message + " ") + if sure.lower() not in ["y", "yes"]: exit(0) + def dump_my_msgs(tg, chat_id): msg_id = 0 num_msgs = 0 @@ -20,37 +22,38 @@ def dump_my_msgs(tg, chat_id): all_mine = [] last_timestamp = 0 while True: - last_date = '' if last_timestamp == 0 else str(datetime.fromtimestamp(last_timestamp)) - print(f'.. Fetching {num_my_msgs}/{num_msgs} msgs @{msg_id} {last_date}') + last_date = "" if last_timestamp == 0 else str(datetime.fromtimestamp(last_timestamp)) + print(f".. Fetching {num_my_msgs}/{num_msgs} msgs @{msg_id} {last_date}") r = tg.get_chat_history(chat_id, 1000, msg_id) r.wait() - if not r.update['total_count']: + if not r.update["total_count"]: break - msgs = r.update['messages'] - my_msgs = [m for m in msgs if m['sender_user_id'] == me] + msgs = r.update["messages"] + my_msgs = [m for m in msgs if m["sender_user_id"] == me] all_mine.extend(my_msgs) num_msgs += len(msgs) - msg_id = msgs[-1]['id'] - last_timestamp = msgs[-1]['date'] + msg_id = msgs[-1]["id"] + last_timestamp = msgs[-1]["date"] - deletable_msg_ids = [m['id'] for m in all_mine if m['can_be_deleted_for_all_users']] + deletable_msg_ids = [m["id"] for m in all_mine if m["can_be_deleted_for_all_users"]] - print('msgs:', num_msgs) - print('mine:', len(all_mine)) - print('deletable:', len(deletable_msg_ids)) + print("msgs:", num_msgs) + print("mine:", len(all_mine)) + print("deletable:", len(deletable_msg_ids)) return all_mine, deletable_msg_ids def delete_messages(chat_id, message_ids): - BATCH=20 + BATCH = 20 num = len(message_ids) for i in range(0, num, BATCH): - print(f'.. Deleting {i}-{i+BATCH-1} / {num}...') - r = tg.delete_messages(chat_id, message_ids[i:i+BATCH], revoke=True) + print(f".. Deleting {i}-{i+BATCH-1} / {num}...") + r = tg.delete_messages(chat_id, message_ids[i : i + BATCH], revoke=True) r.wait(raise_exc=True) -if __name__ == '__main__': + +if __name__ == "__main__": utils.setup_logging() parser = argparse.ArgumentParser() @@ -62,10 +65,10 @@ def delete_messages(chat_id, message_ids): api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", proxy_server=args.proxy_server, proxy_port=args.proxy_port, - proxy_type=utils.parse_proxy_type(args) + proxy_type=utils.parse_proxy_type(args), ) # you must call login method before others tg.login() @@ -73,57 +76,57 @@ def delete_messages(chat_id, message_ids): # get me result = tg.get_me() result.wait() - me = result.update['id'] + me = result.update["id"] print(result.update) # get chats result = tg.get_chats(9223372036854775807) # const 2^62-1: from the first result.wait() - chats = result.update['chat_ids'] + chats = result.update["chat_ids"] # get each chat - print('Chat List') + print("Chat List") chat_map = {} for chat_id in chats: r = tg.get_chat(chat_id) r.wait() - title = r.update['title'] - print(' %20d\t%s' % (chat_id, title)) + title = r.update["title"] + print(" %20d\t%s" % (chat_id, title)) chat_map[chat_id] = r.update - selected = int(input('Select a group to clear: ').strip()) + selected = int(input("Select a group to clear: ").strip()) chat_info = chat_map[selected] - print(f'You selected: {selected} {json.dumps(chat_info, indent=2)}') + print(f"You selected: {selected} {json.dumps(chat_info, indent=2)}") print(f'Chat: {chat_info["title"]}') - confirm('Are you sure?') + confirm("Are you sure?") # dump all my messages directly all_mine, deletable_msg_ids = dump_my_msgs(tg, selected) - confirm(f'Continue to delete all {len(deletable_msg_ids)}?') + confirm(f"Continue to delete all {len(deletable_msg_ids)}?") delete_messages(selected, deletable_msg_ids) # continue on basic group if it's a super group - if chat_info['type']['@type'] == 'chatTypeSupergroup': - supergroup_id = chat_info['type']['supergroup_id'] + if chat_info["type"]["@type"] == "chatTypeSupergroup": + supergroup_id = chat_info["type"]["supergroup_id"] r = tg.get_supergroup_full_info(supergroup_id) r.wait() - basic_group_id = r.update['upgraded_from_basic_group_id'] - max_message_id = r.update['upgraded_from_max_message_id'] - print(f'Found basic group: {basic_group_id} @ {max_message_id}') + basic_group_id = r.update["upgraded_from_basic_group_id"] + max_message_id = r.update["upgraded_from_max_message_id"] + print(f"Found basic group: {basic_group_id} @ {max_message_id}") r = tg.create_basic_group_chat(basic_group_id) r.wait() - basic_group_chat_id = r.update['id'] - print(f'Basic group chat: {basic_group_chat_id}') + basic_group_chat_id = r.update["id"] + print(f"Basic group chat: {basic_group_chat_id}") all_mine, deletable_msg_ids = dump_my_msgs(tg, basic_group_chat_id) - confirm(f'Continue to delete all {len(deletable_msg_ids)}?') + confirm(f"Continue to delete all {len(deletable_msg_ids)}?") delete_messages(basic_group_chat_id, deletable_msg_ids) - print('Done') + print("Done") tg.stop() diff --git a/examples/echo_bot.py b/examples/echo_bot.py index 61cd0676..64111ec3 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -12,35 +12,35 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": setup_logging(level=logging.INFO) parser = argparse.ArgumentParser() - parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps - parser.add_argument('api_hash', help='API hash') - parser.add_argument('phone', help='Phone') + parser.add_argument("api_id", help="API id") # https://my.telegram.org/apps + parser.add_argument("api_hash", help="API hash") + parser.add_argument("phone", help="Phone") args = parser.parse_args() tg = Telegram( api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", ) # you must call login method before others tg.login() def new_message_handler(update): - message_content = update['message']['content'] - message_text = message_content.get('text', {}).get('text', '').lower() - is_outgoing = update['message']['is_outgoing'] + message_content = update["message"]["content"] + message_text = message_content.get("text", {}).get("text", "").lower() + is_outgoing = update["message"]["is_outgoing"] - if not is_outgoing and message_content['@type'] == 'messageText' and message_text == 'ping': - chat_id = update['message']['chat_id'] - print(f'Ping has been received from {chat_id}') + if not is_outgoing and message_content["@type"] == "messageText" and message_text == "ping": + chat_id = update["message"]["chat_id"] + print(f"Ping has been received from {chat_id}") tg.send_message( chat_id=chat_id, - text='pong', + text="pong", ) tg.add_message_handler(new_message_handler) diff --git a/examples/get_instant_view.py b/examples/get_instant_view.py index acdca39a..b97e8793 100644 --- a/examples/get_instant_view.py +++ b/examples/get_instant_view.py @@ -12,21 +12,21 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": setup_logging(level=logging.INFO) parser = argparse.ArgumentParser() - parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps - parser.add_argument('api_hash', help='API hash') - parser.add_argument('phone', help='Phone') - parser.add_argument('url', help='Webpage URL') + parser.add_argument("api_id", help="API id") # https://my.telegram.org/apps + parser.add_argument("api_hash", help="API hash") + parser.add_argument("phone", help="Phone") + parser.add_argument("url", help="Webpage URL") args = parser.parse_args() tg = Telegram( api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", ) # you must call login method before others tg.login() @@ -39,10 +39,10 @@ result.wait() if result.error: - print(f'error: {result.error_info}') + print(f"error: {result.error_info}") else: - print('Instant view: ') - short_text = result.update['page_blocks'][0]['title']['text'] - print(f'\n {short_text}') + print("Instant view: ") + short_text = result.update["page_blocks"][0]["title"]["text"] + print(f"\n {short_text}") tg.stop() diff --git a/examples/get_me.py b/examples/get_me.py index 9b8581f1..a6c6988b 100644 --- a/examples/get_me.py +++ b/examples/get_me.py @@ -18,10 +18,10 @@ def main(): api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", proxy_server=args.proxy_server, proxy_port=args.proxy_port, - proxy_type=utils.parse_proxy_type(args) + proxy_type=utils.parse_proxy_type(args), ) # you must call login method before others tg.login() @@ -33,5 +33,5 @@ def main(): tg.stop() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/get_me_non_blocking_login.py b/examples/get_me_non_blocking_login.py index 2f3f55b4..9d8c7c72 100644 --- a/examples/get_me_non_blocking_login.py +++ b/examples/get_me_non_blocking_login.py @@ -8,7 +8,7 @@ import utils -if __name__ == '__main__': +if __name__ == "__main__": utils.setup_logging() parser = argparse.ArgumentParser() @@ -19,13 +19,13 @@ api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", ) # you must call login method before others state = tg.login(blocking=False) - print ("Checking the return state of the login(blocking=False) function call") + print("Checking the return state of the login(blocking=False) function call") if state == AuthorizationState.WAIT_CODE: print("Pin is required. In this example, the main program is asking it, not the python-telegram client") @@ -36,11 +36,11 @@ if state == AuthorizationState.WAIT_PASSWORD: print("Password is required. In this example, the main program is asking it, not the python-telegram client") - pwd = getpass.getpass('Insert password here (but please be sure that no one is spying on you): ') + pwd = getpass.getpass("Insert password here (but please be sure that no one is spying on you): ") tg.send_password(pwd) state = tg.login(blocking=False) - print('Authorization state: %s' % tg.authorization_state) + print("Authorization state: %s" % tg.authorization_state) result = tg.get_me() result.wait() diff --git a/examples/send_message.py b/examples/send_message.py index 2db263df..4c66ce00 100644 --- a/examples/send_message.py +++ b/examples/send_message.py @@ -13,22 +13,22 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": setup_logging(level=logging.INFO) parser = argparse.ArgumentParser() - parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps - parser.add_argument('api_hash', help='API hash') - parser.add_argument('phone', help='Phone') - parser.add_argument('chat_id', help='Chat id', type=int) - parser.add_argument('text', help='Message text') + parser.add_argument("api_id", help="API id") # https://my.telegram.org/apps + parser.add_argument("api_hash", help="API hash") + parser.add_argument("phone", help="Phone") + parser.add_argument("chat_id", help="Chat id", type=int) + parser.add_argument("text", help="Message text") args = parser.parse_args() tg = Telegram( api_id=args.api_id, api_hash=args.api_hash, phone=args.phone, - database_encryption_key='changeme1234', + database_encryption_key="changeme1234", ) # you must call login method before others tg.login() @@ -41,9 +41,9 @@ get_chats_result.wait() if get_chats_result.error: - print(f'get chats error: {get_chats_result.error_info}') + print(f"get chats error: {get_chats_result.error_info}") else: - print(f'chats: {get_chats_result.update}') + print(f"chats: {get_chats_result.update}") send_message_result = tg.send_message( chat_id=args.chat_id, @@ -52,7 +52,7 @@ send_message_result.wait() if send_message_result.error: - print(f'Failed to send the message: {send_message_result.error_info}') + print(f"Failed to send the message: {send_message_result.error_info}") # When python-telegram sends a message to tdlib, # it does not send it immediately. When the message is sent, tdlib sends an updateMessageSendSucceeded event. @@ -61,20 +61,21 @@ # The handler is called when the tdlib sends updateMessageSendSucceeded event def update_message_send_succeeded_handler(update): - print(f'Received updateMessageSendSucceeded: {update}') + print(f"Received updateMessageSendSucceeded: {update}") # When we sent the message, it got a temporary id. The server assigns permanent id to the message # when it receives it, and tdlib sends the updateMessageSendSucceeded event with the new id. # # Check that this event is for the message we sent. - if update['old_message_id'] == send_message_result.update['id']: - message_id = update['message']['id'] + if update["old_message_id"] == send_message_result.update["id"]: + new_message_id = update["message"]["id"] + print(f"Message has been sent. New message id: {new_message_id}") message_has_been_sent.set() # When the event is received, the handler is called. - tg.add_update_handler('updateMessageSendSucceeded', update_message_send_succeeded_handler) + tg.add_update_handler("updateMessageSendSucceeded", update_message_send_succeeded_handler) # Wait for the message to be sent message_has_been_sent.wait(timeout=60) - print(f'Message has been sent.') + print("Message has been sent.") tg.stop() diff --git a/examples/utils.py b/examples/utils.py index df140dfa..b2409da1 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -8,35 +8,43 @@ def setup_logging(level=logging.INFO): root.setLevel(level) ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s') + formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") ch.setFormatter(formatter) root.addHandler(ch) + def add_api_args(parser: argparse.ArgumentParser): - parser.add_argument('api_id', help='API id') # https://my.telegram.org/apps - parser.add_argument('api_hash', help='API hash') - parser.add_argument('phone', help='Phone') + parser.add_argument("api_id", help="API id") # https://my.telegram.org/apps + parser.add_argument("api_hash", help="API hash") + parser.add_argument("phone", help="Phone") + def add_proxy_args(parser: argparse.ArgumentParser): - parser.add_argument('--proxy_server', help='Proxy server', default='', required=False) - parser.add_argument('--proxy_port', help='Proxy port', default='', required=False) - parser.add_argument('--proxy_type', help='Proxy type (socks5, http, mtproxy)', default='', required=False) - parser.add_argument('--proxy_username', help='Proxy user name', default='', required=False) - parser.add_argument('--proxy_password', help='Proxy password', default='', required=False) - parser.add_argument('--proxy_secret', help='Proxy secret (mtproxy)', default='', required=False) + parser.add_argument("--proxy_server", help="Proxy server", default="", required=False) + parser.add_argument("--proxy_port", help="Proxy port", default="", required=False) + parser.add_argument( + "--proxy_type", + help="Proxy type (socks5, http, mtproxy)", + default="", + required=False, + ) + parser.add_argument("--proxy_username", help="Proxy user name", default="", required=False) + parser.add_argument("--proxy_password", help="Proxy password", default="", required=False) + parser.add_argument("--proxy_secret", help="Proxy secret (mtproxy)", default="", required=False) + def parse_proxy_type(args): obj_type = { - 'socks5': 'proxyTypeSocks5', - 'http': 'proxyTypeHttp', - 'mtproxy': 'proxyTypeMtproto', + "socks5": "proxyTypeSocks5", + "http": "proxyTypeHttp", + "mtproxy": "proxyTypeMtproto", } if args.proxy_type not in obj_type: return None - obj = {'@type': obj_type[args.proxy_type]} - if args.proxy_type in ['http', 'socks5']: - obj['username'] = args.proxy_username - obj['password'] = args.proxy_password + obj = {"@type": obj_type[args.proxy_type]} + if args.proxy_type in ["http", "socks5"]: + obj["username"] = args.proxy_username + obj["password"] = args.proxy_password else: - obj['secret'] = args.secret + obj["secret"] = args.secret return obj diff --git a/pyproject.toml b/pyproject.toml index 1b7e1e8a..5353f93f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,52 @@ [build-system] -requires = ["setuptools"] +requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" -[tool.black] +[project] +name = "python-telegram" +dynamic = ["version"] +description = "Python library to help you build your own Telegram clients" +readme = "README.md" +authors = [{name = "Alexander Akhmetov", email = "me@alx.cx"}] +license = {text = "MIT"} +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", +] +keywords = ["telegram", "client", "api", "tdlib", "tdjson", "td"] +dependencies = [ + "telegram-text==0.2.0", +] +requires-python = ">=3.9" + +[project.urls] +Source = "https://github.com/alexander-akhmetov/python-telegram" +Documentation = "https://python-telegram.readthedocs.io/en/latest/" +Tutorial = "https://python-telegram.readthedocs.io/en/latest/tutorial.html" +Changelog = "https://python-telegram.readthedocs.io/en/latest/changelog.html" + +[tool.setuptools] +packages = ["telegram"] +include-package-data = true +zip-safe = false + +[tool.setuptools.package-data] +telegram = [ + "lib/darwin/*", + "lib/linux/*", + "py.typed", +] + +[tool.setuptools_scm] +write_to = "telegram/_version.py" + +[tool.ruff] line-length = 119 -target_version = ['py36', 'py37', 'py38'] -skip-string-normalization = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ac142d62..00000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[metadata] -description_file = README.md - -[flake8] -max-line-length = 119 diff --git a/setup.py b/setup.py deleted file mode 100644 index 91235776..00000000 --- a/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python -import os -import re -from setuptools import setup - - -def get_version(package): - """ - Returns version of a package (`__version__` in `init.py`). - """ - init_py = open(os.path.join(package, '__init__.py')).read() - - return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) - - -version = get_version('telegram') - - -setup( - name='python-telegram', - version=version, - description='Python library to help you build your own Telegram clients', - author='Alexander Akhmetov', - author_email='me@aleks.sh', - url='https://github.com/alexander-akhmetov/python-telegram', - long_description_content_type='text/markdown', - long_description=open(os.path.join(os.path.dirname(__file__), 'README.md')).read(), - packages=[ - 'telegram', - ], - package_data={ - 'telegram': [ - 'lib/darwin/x86_64/*', - 'lib/darwin/arm64/*', - 'lib/linux/*', - 'py.typed', - ], - }, - install_requires=[ - 'telegram-text~=0.1', - ], -) diff --git a/telegram/__init__.py b/telegram/__init__.py index f4ceab13..008b53ea 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.18.0' +__version__ = "0.19.0" VERSION = __version__ diff --git a/telegram/client.py b/telegram/client.py index 9c8755a5..c2161f67 100644 --- a/telegram/client.py +++ b/telegram/client.py @@ -1,4 +1,3 @@ -import sys import hashlib import time import queue @@ -20,6 +19,7 @@ DefaultDict, Union, Tuple, + Literal, ) from types import FrameType from collections import defaultdict @@ -31,28 +31,24 @@ from telegram.worker import BaseWorker, SimpleWorker from telegram.text import Element -if sys.version_info >= (3, 8): # Backwards compatibility for python < 3.8 - from typing import Literal -else: - from typing_extensions import Literal logger = logging.getLogger(__name__) -MESSAGE_HANDLER_TYPE: str = 'updateNewMessage' +MESSAGE_HANDLER_TYPE: str = "updateNewMessage" class AuthorizationState(enum.Enum): NONE = None - WAIT_CODE = 'authorizationStateWaitCode' - WAIT_PASSWORD = 'authorizationStateWaitPassword' - WAIT_TDLIB_PARAMETERS = 'authorizationStateWaitTdlibParameters' - WAIT_ENCRYPTION_KEY = 'authorizationStateWaitEncryptionKey' - WAIT_PHONE_NUMBER = 'authorizationStateWaitPhoneNumber' - WAIT_REGISTRATION = 'authorizationStateWaitRegistration' - READY = 'authorizationStateReady' - CLOSING = 'authorizationStateClosing' - CLOSED = 'authorizationStateClosed' + WAIT_CODE = "authorizationStateWaitCode" + WAIT_PASSWORD = "authorizationStateWaitPassword" + WAIT_TDLIB_PARAMETERS = "authorizationStateWaitTdlibParameters" + WAIT_ENCRYPTION_KEY = "authorizationStateWaitEncryptionKey" + WAIT_PHONE_NUMBER = "authorizationStateWaitPhoneNumber" + WAIT_REGISTRATION = "authorizationStateWaitRegistration" + READY = "authorizationStateReady" + CLOSING = "authorizationStateClosing" + CLOSED = "authorizationStateClosed" class Telegram: @@ -68,14 +64,14 @@ def __init__( files_directory: Optional[Union[str, Path]] = None, use_test_dc: bool = False, use_message_database: bool = True, - device_model: str = 'python-telegram', + device_model: str = "python-telegram", application_version: str = VERSION, - system_version: str = 'unknown', - system_language_code: str = 'en', + system_version: str = "unknown", + system_language_code: str = "en", login: bool = False, default_workers_queue_size: int = 1000, tdlib_verbosity: int = 2, - proxy_server: str = '', + proxy_server: str = "", proxy_port: int = 0, proxy_type: Optional[Dict[str, str]] = None, use_secret_chats: bool = True, @@ -115,7 +111,7 @@ def __init__( self.authorization_state = AuthorizationState.NONE if not self.bot_token and not self.phone: - raise ValueError('You must provide bot_token or phone') + raise ValueError("You must provide bot_token or phone") self._database_encryption_key = database_encryption_key if isinstance(self._database_encryption_key, str): @@ -126,7 +122,7 @@ def __init__( if not files_directory: hasher = hashlib.md5() str_to_encode: str = self.phone or self.bot_token # type: ignore - hasher.update(str_to_encode.encode('utf-8')) + hasher.update(str_to_encode.encode("utf-8")) directory_name = hasher.hexdigest() files_directory = Path(tempfile.gettempdir()) / ".tdlib_files" / directory_name @@ -157,7 +153,7 @@ def stop(self) -> None: if self._stopped.is_set(): return - logger.info('Stopping telegram client...') + logger.info("Stopping telegram client...") self._close() self.worker.stop() @@ -166,7 +162,7 @@ def stop(self) -> None: # wait for the tdjson listener to stop self._td_listener.join() - if hasattr(self, '_tdjson'): + if hasattr(self, "_tdjson"): self._tdjson.stop() def _close(self) -> None: @@ -174,15 +170,15 @@ def _close(self) -> None: Calls `close` tdlib method and waits until authorization_state becomes CLOSED. Blocking. """ - self.call_method('close') + self.call_method("close") while self.authorization_state != AuthorizationState.CLOSED: result = self.get_authorization_state() self.authorization_state = self._wait_authorization_result(result) - logger.info('Authorization state: %s', self.authorization_state) + logger.info("Authorization state: %s", self.authorization_state) time.sleep(0.5) - def parse_text_entities(self, text: str, parse_mode: Literal['HTML', 'Markdown']) -> AsyncResult: + def parse_text_entities(self, text: str, parse_mode: Literal["HTML", "Markdown"]) -> AsyncResult: """ Parses text from 'HTML' and 'Markdown' (not MarkdownV2) into plain text and internal telegram style description. @@ -212,14 +208,14 @@ def parse_text_entities(self, text: str, parse_mode: Literal['HTML', 'Markdown'] """ parse_mode_types = { - 'HTML': 'textParseModeHTML', - 'Markdown': 'textParseModeMarkdown', + "HTML": "textParseModeHTML", + "Markdown": "textParseModeMarkdown", } data = { - '@type': 'parseTextEntities', - 'text': text, - 'parse_mode': { - '@type': parse_mode_types[parse_mode], + "@type": "parseTextEntities", + "text": text, + "parse_mode": { + "@type": parse_mode_types[parse_mode], }, } @@ -257,24 +253,24 @@ def send_message( updated_text: str if isinstance(text, Element): - result = self.parse_text_entities(text.to_html(), parse_mode='HTML') + result = self.parse_text_entities(text.to_html(), parse_mode="HTML") result.wait() assert result.update is not None update: dict = result.update - entities = update['entities'] - updated_text = update['text'] + entities = update["entities"] + updated_text = update["text"] else: updated_text = text data = { - '@type': 'sendMessage', - 'chat_id': chat_id, - 'input_message_content': { - '@type': 'inputMessageText', - 'text': { - '@type': 'formattedText', - 'text': updated_text, - 'entities': entities, + "@type": "sendMessage", + "chat_id": chat_id, + "input_message_content": { + "@type": "inputMessageText", + "text": { + "@type": "formattedText", + "text": updated_text, + "entities": entities, }, }, } @@ -320,8 +316,8 @@ def import_contacts(self, contacts: List[Dict[str, str]]) -> AsyncResult: contact["@type"] = "contact" data = { - '@type': 'importContacts', - 'contacts': contacts, + "@type": "importContacts", + "contacts": contacts, } return self._send_data(data) @@ -331,7 +327,7 @@ def get_chat(self, chat_id: int) -> AsyncResult: This is offline request, if there is no chat in your database it will not be found tdlib saves chat to the database when it receives a new message or when you call `get_chats` method. """ - data = {'@type': 'getChat', 'chat_id': chat_id} + data = {"@type": "getChat", "chat_id": chat_id} return self._send_data(data) @@ -342,7 +338,7 @@ def get_me(self) -> AsyncResult: https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_me.html """ - return self.call_method('getMe') + return self.call_method("getMe") def get_user(self, user_id: int) -> AsyncResult: """ @@ -351,7 +347,7 @@ def get_user(self, user_id: int) -> AsyncResult: https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_user.html """ - return self.call_method('getUser', params={'user_id': user_id}) + return self.call_method("getUser", params={"user_id": user_id}) def get_user_full_info(self, user_id: int) -> AsyncResult: """ @@ -360,7 +356,7 @@ def get_user_full_info(self, user_id: int) -> AsyncResult: https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_user_full_info.html """ - return self.call_method('getUserFullInfo', params={'user_id': user_id}) + return self.call_method("getUserFullInfo", params={"user_id": user_id}) def get_chats(self, offset_order: int = 0, offset_chat_id: int = 0, limit: int = 100) -> AsyncResult: """ @@ -376,10 +372,10 @@ def get_chats(self, offset_order: int = 0, offset_chat_id: int = 0, limit: int = } """ data = { - '@type': 'getChats', - 'offset_order': offset_order, - 'offset_chat_id': offset_chat_id, - 'limit': limit, + "@type": "getChats", + "offset_order": offset_order, + "offset_chat_id": offset_chat_id, + "limit": limit, } return self._send_data(data) @@ -403,12 +399,12 @@ def get_chat_history( only_local """ data = { - '@type': 'getChatHistory', - 'chat_id': chat_id, - 'limit': limit, - 'from_message_id': from_message_id, - 'offset': offset, - 'only_local': only_local, + "@type": "getChatHistory", + "chat_id": chat_id, + "limit": limit, + "from_message_id": from_message_id, + "offset": offset, + "only_local": only_local, } return self._send_data(data) @@ -438,9 +434,9 @@ def get_message( } """ data = { - '@type': 'getMessage', - 'chat_id': chat_id, - 'message_id': message_id, + "@type": "getMessage", + "chat_id": chat_id, + "message_id": message_id, } return self._send_data(data) @@ -457,10 +453,10 @@ def delete_messages(self, chat_id: int, message_ids: List[int], revoke: bool = T return self._send_data( { - '@type': 'deleteMessages', - 'chat_id': chat_id, - 'message_ids': message_ids, - 'revoke': revoke, + "@type": "deleteMessages", + "chat_id": chat_id, + "message_ids": message_ids, + "revoke": revoke, } ) @@ -472,7 +468,7 @@ def get_supergroup_full_info(self, supergroup_id: int) -> AsyncResult: supergroup_id """ - return self._send_data({'@type': 'getSupergroupFullInfo', 'supergroup_id': supergroup_id}) + return self._send_data({"@type": "getSupergroupFullInfo", "supergroup_id": supergroup_id}) def create_basic_group_chat(self, basic_group_id: int) -> AsyncResult: """ @@ -482,7 +478,7 @@ def create_basic_group_chat(self, basic_group_id: int) -> AsyncResult: basic_group_id """ - return self._send_data({'@type': 'createBasicGroupChat', 'basic_group_id': basic_group_id}) + return self._send_data({"@type": "createBasicGroupChat", "basic_group_id": basic_group_id}) def get_web_page_instant_view(self, url: str, force_full: bool = False) -> AsyncResult: """ @@ -493,7 +489,7 @@ def get_web_page_instant_view(self, url: str, force_full: bool = False) -> Async url: URL of a webpage force_full: If true, the full instant view for the web page will be returned """ - data = {'@type': 'getWebPageInstantView', 'url': url, 'force_full': force_full} + data = {"@type": "getWebPageInstantView", "url": url, "force_full": force_full} return self._send_data(data) @@ -510,7 +506,7 @@ def call_method( method_name: Name of the method params: parameters """ - data = {'@type': method_name} + data = {"@type": method_name} if params: data.update(params) @@ -525,7 +521,7 @@ def _run(self) -> None: self.worker.run() def _listen_to_td(self) -> None: - logger.info('[Telegram.td_listener] started') + logger.info("[Telegram.td_listener] started") while not self._stopped.is_set(): update = self._tdjson.receive() @@ -537,20 +533,20 @@ def _listen_to_td(self) -> None: def _update_async_result(self, update: Dict[Any, Any]) -> typing.Optional[AsyncResult]: async_result = None - _special_types = ('updateAuthorizationState',) # for authorizationProcess @extra.request_id doesn't work + _special_types = ("updateAuthorizationState",) # for authorizationProcess @extra.request_id doesn't work - if update.get('@type') in _special_types: - request_id = update['@type'] + if update.get("@type") in _special_types: + request_id = update["@type"] else: - request_id = update.get('@extra', {}).get('request_id') + request_id = update.get("@extra", {}).get("request_id") if not request_id: - logger.debug('request_id has not been found in the update') + logger.debug("request_id has not been found in the update") else: async_result = self._results.get(request_id) if not async_result: - logger.debug('async_result has not been found in by request_id=%s', request_id) + logger.debug("async_result has not been found in by request_id=%s", request_id) else: done = async_result.parse_update(update) @@ -560,7 +556,7 @@ def _update_async_result(self, update: Dict[Any, Any]) -> typing.Optional[AsyncR return async_result def _run_handlers(self, update: Dict[Any, Any]) -> None: - update_type: str = update.get('@type', 'unknown') + update_type: str = update.get("@type", "unknown") for handler in self._update_handlers[update_type]: self._workers_queue.put((handler, update), timeout=self._queue_put_timeout) @@ -594,14 +590,14 @@ def _send_data( If `block`is True, waits for the result """ - if '@extra' not in data: - data['@extra'] = {} + if "@extra" not in data: + data["@extra"] = {} - if not result_id and 'request_id' in data['@extra']: - result_id = data['@extra']['request_id'] + if not result_id and "request_id" in data["@extra"]: + result_id = data["@extra"]["request_id"] async_result = AsyncResult(client=self, result_id=result_id) - data['@extra']['request_id'] = async_result.id + data["@extra"]["request_id"] = async_result.id self._results[async_result.id] = async_result self._tdjson.send(data) async_result.request = data @@ -630,14 +626,14 @@ def idle( self._stopped.wait() def _stop_signal_handler(self, signum: int, frame: Optional[FrameType] = None) -> None: - logger.info('Signal %s received!', signum) + logger.info("Signal %s received!", signum) self.stop() def get_authorization_state(self) -> AsyncResult: - logger.debug('Getting authorization state') - data = {'@type': 'getAuthorizationState'} + logger.debug("Getting authorization state") + data = {"@type": "getAuthorizationState"} - return self._send_data(data, result_id='getAuthorizationState') + return self._send_data(data, result_id="getAuthorizationState") def _wait_authorization_result(self, result: AsyncResult) -> AuthorizationState: authorization_state = None @@ -646,12 +642,12 @@ def _wait_authorization_result(self, result: AsyncResult) -> AuthorizationState: result.wait(raise_exc=True) if result.update is None: - raise RuntimeError('Something wrong, the result update is None') + raise RuntimeError("Something wrong, the result update is None") - if result.id == 'getAuthorizationState': - authorization_state = result.update['@type'] + if result.id == "getAuthorizationState": + authorization_state = result.update["@type"] else: - authorization_state = result.update['authorization_state']['@type'] + authorization_state = result.update["authorization_state"]["@type"] return AuthorizationState(authorization_state) @@ -702,12 +698,12 @@ def login(self, blocking: bool = True) -> AuthorizationState: ) if self.phone: - logger.info('[login] Login process has been started with phone') + logger.info("[login] Login process has been started with phone") else: - logger.info('[login] Login process has been started with bot token') + logger.info("[login] Login process has been started with bot token") while self.authorization_state != AuthorizationState.READY: - logger.info('[login] current authorization state: %s', self.authorization_state) + logger.info("[login] current authorization state: %s", self.authorization_state) if not blocking and self.authorization_state in blocking_actions: return self.authorization_state @@ -723,43 +719,43 @@ def login(self, blocking: bool = True) -> AuthorizationState: def _set_initial_params(self) -> AsyncResult: logger.info( - 'Setting tdlib initial params: files_dir=%s, test_dc=%s', + "Setting tdlib initial params: files_dir=%s, test_dc=%s", self.files_directory, self.use_test_dc, ) parameters = { - 'use_test_dc': self.use_test_dc, - 'api_id': self.api_id, - 'api_hash': self.api_hash, - 'device_model': self.device_model, - 'system_version': self.system_version, - 'application_version': self.application_version, - 'system_language_code': self.system_language_code, - 'database_directory': str(self.files_directory / "database"), - 'use_message_database': self.use_message_database, - 'files_directory': str(self.files_directory / "files"), - 'use_secret_chats': self.use_secret_chats, + "use_test_dc": self.use_test_dc, + "api_id": self.api_id, + "api_hash": self.api_hash, + "device_model": self.device_model, + "system_version": self.system_version, + "application_version": self.application_version, + "system_language_code": self.system_language_code, + "database_directory": str(self.files_directory / "database"), + "use_message_database": self.use_message_database, + "files_directory": str(self.files_directory / "files"), + "use_secret_chats": self.use_secret_chats, } data: Dict[str, typing.Any] = { - '@type': 'setTdlibParameters', - 'parameters': parameters, + "@type": "setTdlibParameters", + "parameters": parameters, # since tdlib 1.8.6 - 'database_encryption_key': self._database_encryption_key, + "database_encryption_key": self._database_encryption_key, **parameters, } - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def _send_encryption_key(self) -> AsyncResult: - logger.info('Sending encryption key') + logger.info("Sending encryption key") data = { - '@type': 'checkDatabaseEncryptionKey', - 'encryption_key': self._database_encryption_key, + "@type": "checkDatabaseEncryptionKey", + "encryption_key": self._database_encryption_key, } - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def _send_phone_number_or_bot_token(self) -> AsyncResult: """Sends phone number or a bot_token""" @@ -769,45 +765,45 @@ def _send_phone_number_or_bot_token(self) -> AsyncResult: elif self.bot_token: return self._send_bot_token() else: - raise RuntimeError('Unknown mode: both bot_token and phone are None') + raise RuntimeError("Unknown mode: both bot_token and phone are None") def _send_phone_number(self) -> AsyncResult: - logger.info('Sending phone number') + logger.info("Sending phone number") data = { - '@type': 'setAuthenticationPhoneNumber', - 'phone_number': self.phone, - 'allow_flash_call': False, - 'is_current_phone_number': True, + "@type": "setAuthenticationPhoneNumber", + "phone_number": self.phone, + "allow_flash_call": False, + "is_current_phone_number": True, } - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def _send_add_proxy(self) -> AsyncResult: - logger.info('Sending addProxy') + logger.info("Sending addProxy") data = { - '@type': 'addProxy', - 'server': self.proxy_server, - 'port': self.proxy_port, - 'enable': True, - 'type': self.proxy_type, + "@type": "addProxy", + "server": self.proxy_server, + "port": self.proxy_port, + "enable": True, + "type": self.proxy_type, } - return self._send_data(data, result_id='setProxy') + return self._send_data(data, result_id="setProxy") def _send_bot_token(self) -> AsyncResult: - logger.info('Sending bot token') - data = {'@type': 'checkAuthenticationBotToken', 'token': self.bot_token} + logger.info("Sending bot token") + data = {"@type": "checkAuthenticationBotToken", "token": self.bot_token} - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def _send_telegram_code(self, code: Optional[str] = None) -> AsyncResult: - logger.info('Sending code') + logger.info("Sending code") if code is None: - code = input('Enter code:') - data = {'@type': 'checkAuthenticationCode', 'code': str(code)} + code = input("Enter code:") + data = {"@type": "checkAuthenticationCode", "code": str(code)} - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def send_code(self, code: str) -> AuthorizationState: """ @@ -828,13 +824,13 @@ def send_code(self, code: str) -> AuthorizationState: return self.authorization_state def _send_password(self, password: Optional[str] = None) -> AsyncResult: - logger.info('Sending password') + logger.info("Sending password") if password is None: - password = getpass.getpass('Password:') - data = {'@type': 'checkAuthenticationPassword', 'password': password} + password = getpass.getpass("Password:") + data = {"@type": "checkAuthenticationPassword", "password": password} - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def send_password(self, password: str) -> AuthorizationState: """ @@ -857,21 +853,21 @@ def send_password(self, password: str) -> AuthorizationState: return self.authorization_state def _register_user(self, first: Optional[str] = None, last: Optional[str] = None) -> AsyncResult: - logger.info('Registering user') + logger.info("Registering user") if first is None: - first = input('Enter first name: ') + first = input("Enter first name: ") if last is None: - last = input('Enter last name: ') + last = input("Enter last name: ") data = { - '@type': 'registerUser', - 'first_name': first, - 'last_name': last, + "@type": "registerUser", + "first_name": first, + "last_name": last, } - return self._send_data(data, result_id='updateAuthorizationState') + return self._send_data(data, result_id="updateAuthorizationState") def register_user(self, first: str, last: str) -> AuthorizationState: """ diff --git a/telegram/lib/darwin/x86_64/libtdjson.dylib b/telegram/lib/darwin/x86_64/libtdjson.dylib index 37b06e64..128d5452 100755 Binary files a/telegram/lib/darwin/x86_64/libtdjson.dylib and b/telegram/lib/darwin/x86_64/libtdjson.dylib differ diff --git a/telegram/lib/linux/libtdjson.so b/telegram/lib/linux/libtdjson.so index b74bc50f..f8be47f6 100755 Binary files a/telegram/lib/linux/libtdjson.so and b/telegram/lib/linux/libtdjson.so differ diff --git a/telegram/tdjson.py b/telegram/tdjson.py index d4f881e9..439de7eb 100644 --- a/telegram/tdjson.py +++ b/telegram/tdjson.py @@ -4,8 +4,7 @@ import ctypes.util from ctypes import CDLL, CFUNCTYPE, c_int, c_char_p, c_double, c_void_p, c_longlong from typing import Any, Dict, Optional, Union - -import pkg_resources +import importlib.resources logger = logging.getLogger(__name__) @@ -16,13 +15,13 @@ def _get_tdjson_lib_path() -> str: if system_library is not None: return system_library - if platform.system().lower() == 'darwin': + if platform.system().lower() == "darwin": platform_architecture = platform.machine() - lib_name = f'darwin/{platform_architecture}/libtdjson.dylib' + lib_name = f"darwin/{platform_architecture}/libtdjson.dylib" else: - lib_name = 'linux/libtdjson.so' + lib_name = "linux/libtdjson.so" - return pkg_resources.resource_filename('telegram', f'lib/{lib_name}') + return str(importlib.resources.files("telegram").joinpath(f"lib/{lib_name}")) class TDJson: @@ -34,7 +33,9 @@ def __init__(self, library_path: Optional[str] = None, verbosity: int = 2) -> No self._build_client(library_path, verbosity) def __del__(self) -> None: - if hasattr(self, '_tdjson') and hasattr(self._tdjson, '_td_json_client_destroy'): + if hasattr(self, "_tdjson") and hasattr( + self._tdjson, "_td_json_client_destroy" + ): self.stop() def _build_client(self, library_path: str, verbosity: int) -> None: @@ -79,39 +80,41 @@ def _build_client(self, library_path: str, verbosity: int) -> None: fatal_error_callback_type = CFUNCTYPE(None, c_char_p) - self._td_set_log_fatal_error_callback = self._tdjson.td_set_log_fatal_error_callback + self._td_set_log_fatal_error_callback = ( + self._tdjson.td_set_log_fatal_error_callback + ) self._td_set_log_fatal_error_callback.restype = None self._td_set_log_fatal_error_callback.argtypes = [fatal_error_callback_type] # initialize TDLib log with desired parameters def on_fatal_error_callback(error_message: str) -> None: - logger.error('TDLib fatal error: %s', error_message) + logger.error("TDLib fatal error: %s", error_message) c_on_fatal_error_callback = fatal_error_callback_type(on_fatal_error_callback) self._td_set_log_fatal_error_callback(c_on_fatal_error_callback) def send(self, query: Dict[Any, Any]) -> None: - dumped_query = json.dumps(query).encode('utf-8') + dumped_query = json.dumps(query).encode("utf-8") self._td_json_client_send(self.td_json_client, dumped_query) - logger.debug('[me ==>] Sent %s', dumped_query) + logger.debug("[me ==>] Sent %s", dumped_query) def receive(self) -> Union[None, Dict[Any, Any]]: result_str = self._td_json_client_receive(self.td_json_client, 1.0) if result_str: - result: Dict[Any, Any] = json.loads(result_str.decode('utf-8')) - logger.debug('[me <==] Received %s', result) + result: Dict[Any, Any] = json.loads(result_str.decode("utf-8")) + logger.debug("[me <==] Received %s", result) return result return None def td_execute(self, query: Dict[Any, Any]) -> Union[Dict[Any, Any], Any]: - dumped_query = json.dumps(query).encode('utf-8') + dumped_query = json.dumps(query).encode("utf-8") result_str = self._td_json_client_execute(self.td_json_client, dumped_query) if result_str: - result: Dict[Any, Any] = json.loads(result_str.decode('utf-8')) + result: Dict[Any, Any] = json.loads(result_str.decode("utf-8")) return result diff --git a/telegram/text.py b/telegram/text.py index 81aef4ec..b1842a56 100644 --- a/telegram/text.py +++ b/telegram/text.py @@ -25,22 +25,22 @@ from telegram_text.bases import Element __all__ = [ - 'Bold', - 'Chain', - 'Code', - 'Element', - 'Hashtag', - 'InlineCode', - 'InlineUser', - 'Italic', - 'Link', - 'OrderedList', - 'PlainText', - 'Spoiler', - 'Strikethrough', - 'TOMLSection', - 'Text', - 'Underline', - 'UnorderedList', - 'User', + "Bold", + "Chain", + "Code", + "Element", + "Hashtag", + "InlineCode", + "InlineUser", + "Italic", + "Link", + "OrderedList", + "PlainText", + "Spoiler", + "Strikethrough", + "TOMLSection", + "Text", + "Underline", + "UnorderedList", + "User", ] diff --git a/telegram/utils.py b/telegram/utils.py index 2615bf70..ec1c0f78 100644 --- a/telegram/utils.py +++ b/telegram/utils.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional if TYPE_CHECKING: - from telegram.client import Telegram # noqa pylint: disable=cyclic-import + from telegram.client import Telegram logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class AsyncResult: After each API call, you receive AsyncResult object, which you can use to get results back. """ - def __init__(self, client: 'Telegram', result_id: Optional[str] = None) -> None: + def __init__(self, client: "Telegram", result_id: Optional[str] = None) -> None: self.client = client if result_id: @@ -32,7 +32,7 @@ def __init__(self, client: 'Telegram', result_id: Optional[str] = None) -> None: self._ready = threading.Event() def __str__(self) -> str: - return f'AsyncResult <{self.id}>' + return f"AsyncResult <{self.id}>" def wait(self, timeout: Optional[int] = None, raise_exc: bool = False) -> None: """ @@ -42,21 +42,21 @@ def wait(self, timeout: Optional[int] = None, raise_exc: bool = False) -> None: if result is False: raise TimeoutError() if raise_exc and self.error: - raise RuntimeError(f'Telegram error: {self.error_info}') + raise RuntimeError(f"Telegram error: {self.error_info}") def parse_update(self, update: Dict[Any, Any]) -> bool: - update_type = update.get('@type') + update_type = update.get("@type") - logger.debug('update id=%s type=%s received', self.id, update_type) + logger.debug("update id=%s type=%s received", self.id, update_type) - if update_type == 'ok': + if update_type == "ok": self.ok_received = True - if self.id == 'updateAuthorizationState': + if self.id == "updateAuthorizationState": # For updateAuthorizationState commands tdlib sends # @type: ok responses # but we want to wait longer to receive the new authorization state return False - elif update_type == 'error': + elif update_type == "error": self.error = True self.error_info = update else: diff --git a/telegram/worker.py b/telegram/worker.py index 6e14be15..2d102040 100644 --- a/telegram/worker.py +++ b/telegram/worker.py @@ -28,12 +28,12 @@ class SimpleWorker(BaseWorker): """Simple one-thread worker""" def run(self) -> None: - self._thread = threading.Thread(target=self._run_thread) # pylint: disable=attribute-defined-outside-init + self._thread = threading.Thread(target=self._run_thread) self._thread.daemon = True self._thread.start() def _run_thread(self) -> None: - logger.info('[SimpleWorker] started') + logger.info("[SimpleWorker] started") while self._is_enabled: try: diff --git a/tests/requirements.txt b/tests/requirements.txt index d976e65a..e701f8db 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,7 +1,5 @@ -pytest==8.1.1 -flake8==7.0.0 -pylint==3.1.0 +ruff==0.4.10 +pytest==8.2.2 ipdb==0.13.13 -mypy==1.9.0 -black==24.3.0 +mypy==1.10.0 types-pkg-resources==0.1.3 diff --git a/tests/test_tdjson.py b/tests/test_tdjson.py index b9199efc..dda01a76 100644 --- a/tests/test_tdjson.py +++ b/tests/test_tdjson.py @@ -5,65 +5,80 @@ class TestGetTdjsonTdlibPath: def test_for_darwin_x86_64(self): - mocked_system = Mock(return_value='Darwin') - mocked_machine_name = Mock(return_value='x86_64') + mocked_system = Mock(return_value="Darwin") + mocked_machine_name = Mock(return_value="x86_64") mocked_resource = Mock() mocked_find_library = Mock(return_value=None) - with patch('telegram.tdjson.platform.system', mocked_system): - with patch('telegram.tdjson.platform.machine', mocked_machine_name): - with patch('telegram.tdjson.pkg_resources.resource_filename', mocked_resource): - with patch('telegram.tdjson.ctypes.util.find_library', mocked_find_library): + with patch("telegram.tdjson.platform.system", mocked_system): + with patch("telegram.tdjson.platform.machine", mocked_machine_name): + with patch( + "telegram.tdjson.pkg_resources.resource_filename", mocked_resource + ): + with patch( + "telegram.tdjson.ctypes.util.find_library", mocked_find_library + ): _get_tdjson_lib_path() - mocked_resource.assert_called_once_with('telegram', 'lib/darwin/x86_64/libtdjson.dylib') + mocked_resource.assert_called_once_with( + "telegram", "lib/darwin/x86_64/libtdjson.dylib" + ) def test_for_darwin_arm64(self): - mocked_system = Mock(return_value='Darwin') - mocked_machine_name = Mock(return_value='arm64') + mocked_system = Mock(return_value="Darwin") + mocked_machine_name = Mock(return_value="arm64") mocked_resource = Mock() mocked_find_library = Mock(return_value=None) - with patch('telegram.tdjson.platform.system', mocked_system): - with patch('telegram.tdjson.platform.machine', mocked_machine_name): - with patch('telegram.tdjson.pkg_resources.resource_filename', mocked_resource): - with patch('telegram.tdjson.ctypes.util.find_library', mocked_find_library): + with patch("telegram.tdjson.platform.system", mocked_system): + with patch("telegram.tdjson.platform.machine", mocked_machine_name): + with patch( + "telegram.tdjson.pkg_resources.resource_filename", mocked_resource + ): + with patch( + "telegram.tdjson.ctypes.util.find_library", mocked_find_library + ): _get_tdjson_lib_path() - mocked_resource.assert_called_once_with('telegram', 'lib/darwin/arm64/libtdjson.dylib') + mocked_resource.assert_called_once_with( + "telegram", "lib/darwin/arm64/libtdjson.dylib" + ) - def test_for_linux(self): - mocked_system = Mock(return_value='Linux') - mocked_resource = Mock(return_value='/tmp/') - mocked_find_library = Mock(return_value=None) + def test_for_darwin(self): + mocked_system = Mock(return_value="Darwin") + mocked_files = Mock() + mocked_joinpath = Mock() - with patch('telegram.tdjson.platform.system', mocked_system): - with patch('telegram.tdjson.pkg_resources.resource_filename', mocked_resource): - with patch('telegram.tdjson.ctypes.util.find_library', mocked_find_library): - _get_tdjson_lib_path() + with patch("telegram.tdjson.platform.system", mocked_system): + with patch("importlib.resources.files", mocked_files): + mocked_files.return_value.joinpath = mocked_joinpath + _get_tdjson_lib_path() - mocked_resource.assert_called_once_with('telegram', 'lib/linux/libtdjson.so') + mocked_files.assert_called_once_with("telegram") + mocked_joinpath.assert_called_once_with("lib/darwin/libtdjson.dylib") - def test_for_windows(self): - mocked_system = Mock(return_value='Windows') - mocked_resource = Mock(return_value='/tmp/') - mocked_find_library = Mock(return_value=None) + def test_for_linux(self): + mocked_system = Mock(return_value="Linux") + mocked_files = Mock() + mocked_joinpath = Mock() - with patch('telegram.tdjson.platform.system', mocked_system): - with patch('telegram.tdjson.pkg_resources.resource_filename', mocked_resource): - with patch('telegram.tdjson.ctypes.util.find_library', mocked_find_library): - _get_tdjson_lib_path() + with patch("telegram.tdjson.platform.system", mocked_system): + with patch("importlib.resources.files", mocked_files): + mocked_files.return_value.joinpath = mocked_joinpath + _get_tdjson_lib_path() - mocked_resource.assert_called_once_with('telegram', 'lib/linux/libtdjson.so') + mocked_files.assert_called_once_with("telegram") + mocked_joinpath.assert_called_once_with("lib/linux/libtdjson.so") def test_unknown(self): - mocked_system = Mock(return_value='Unknown') - mocked_resource = Mock(return_value='/tmp/') - mocked_find_library = Mock(return_value=None) + mocked_system = Mock(return_value="Unknown") + mocked_files = Mock() + mocked_joinpath = Mock() - with patch('telegram.tdjson.platform.system', mocked_system): - with patch('telegram.tdjson.pkg_resources.resource_filename', mocked_resource): - with patch('telegram.tdjson.ctypes.util.find_library', mocked_find_library): - _get_tdjson_lib_path() + with patch("telegram.tdjson.platform.system", mocked_system): + with patch("importlib.resources.files", mocked_files): + mocked_files.return_value.joinpath = mocked_joinpath + _get_tdjson_lib_path() - mocked_resource.assert_called_once_with('telegram', 'lib/linux/libtdjson.so') + mocked_files.assert_called_once_with("telegram") + mocked_joinpath.assert_called_once_with("lib/linux/libtdjson.so") diff --git a/tests/test_telegram_methods.py b/tests/test_telegram_methods.py index 0e95fe78..ddbd347b 100644 --- a/tests/test_telegram_methods.py +++ b/tests/test_telegram_methods.py @@ -8,28 +8,28 @@ from telegram.text import Spoiler API_ID = 1 -API_HASH = 'hash' -PHONE = '+71234567890' -LIBRARY_PATH = '/lib/' -DATABASE_ENCRYPTION_KEY = 'changeme1234' +API_HASH = "hash" +PHONE = "+71234567890" +LIBRARY_PATH = "/lib/" +DATABASE_ENCRYPTION_KEY = "changeme1234" @pytest.fixture def telegram(): - with patch('telegram.client.TDJson'): - with patch('telegram.client.threading'): + with patch("telegram.client.TDJson"): + with patch("telegram.client.threading"): return _get_telegram_instance() def _get_telegram_instance(**kwargs): - kwargs.setdefault('api_id', API_ID) - kwargs.setdefault('api_hash', API_HASH) - kwargs.setdefault('phone', PHONE) - kwargs.setdefault('library_path', LIBRARY_PATH) - kwargs.setdefault('database_encryption_key', DATABASE_ENCRYPTION_KEY) - - with patch('telegram.client.TDJson'): - with patch('telegram.client.threading'): + kwargs.setdefault("api_id", API_ID) + kwargs.setdefault("api_hash", API_HASH) + kwargs.setdefault("phone", PHONE) + kwargs.setdefault("library_path", LIBRARY_PATH) + kwargs.setdefault("database_encryption_key", DATABASE_ENCRYPTION_KEY) + + with patch("telegram.client.TDJson"): + with patch("telegram.client.threading"): tg = Telegram(**kwargs) return tg @@ -44,45 +44,45 @@ def test_phone_bot_token_init(self): library_path=LIBRARY_PATH, database_encryption_key=DATABASE_ENCRYPTION_KEY, ) - assert 'You must provide bot_token or phone' in str(excinfo.value) + assert "You must provide bot_token or phone" in str(excinfo.value) def test_send_message(self, telegram): chat_id = 1 - text = 'Hello world' + text = "Hello world" async_result = telegram.send_message(chat_id=chat_id, text=text) exp_data = { - '@type': 'sendMessage', - 'chat_id': chat_id, - 'input_message_content': { - '@type': 'inputMessageText', - 'text': { - '@type': 'formattedText', - 'text': text, - 'entities': [], + "@type": "sendMessage", + "chat_id": chat_id, + "input_message_content": { + "@type": "inputMessageText", + "text": { + "@type": "formattedText", + "text": text, + "entities": [], }, }, - '@extra': { - 'request_id': async_result.id, + "@extra": { + "request_id": async_result.id, }, } telegram._tdjson.send.assert_called_once_with(exp_data) def test_parse_text_entities(self, telegram): - text = Spoiler('Hello world!').to_html() + text = Spoiler("Hello world!").to_html() - async_result = telegram.parse_text_entities(text=text, parse_mode='HTML') + async_result = telegram.parse_text_entities(text=text, parse_mode="HTML") exp_data = { - '@type': 'parseTextEntities', - 'text': 'Hello world!', - 'parse_mode': { - '@type': 'textParseModeHTML', + "@type": "parseTextEntities", + "text": 'Hello world!', + "parse_mode": { + "@type": "textParseModeHTML", }, - '@extra': { - 'request_id': async_result.id, + "@extra": { + "request_id": async_result.id, }, } @@ -90,8 +90,8 @@ def test_parse_text_entities(self, telegram): def test_send_phone_number_or_bot_token(self, telegram): # check that the dunction calls _send_phone_number or _send_bot_token - with patch.object(telegram, '_send_phone_number'), patch.object(telegram, '_send_bot_token'): - telegram.phone = '123' + with patch.object(telegram, "_send_phone_number"), patch.object(telegram, "_send_bot_token"): + telegram.phone = "123" telegram.bot_token = None telegram._send_phone_number_or_bot_token() @@ -100,19 +100,19 @@ def test_send_phone_number_or_bot_token(self, telegram): assert telegram._send_bot_token.call_count == 0 telegram.phone = None - telegram.bot_token = 'some-token' + telegram.bot_token = "some-token" telegram._send_phone_number_or_bot_token() telegram._send_bot_token.assert_called_once() def test_send_bot_token(self, telegram): - telegram.bot_token = 'some-token' + telegram.bot_token = "some-token" - with patch.object(telegram, '_send_data'): + with patch.object(telegram, "_send_data"): telegram._send_bot_token() - exp_data = {'@type': 'checkAuthenticationBotToken', 'token': 'some-token'} - telegram._send_data.assert_called_once_with(exp_data, result_id='updateAuthorizationState') + exp_data = {"@type": "checkAuthenticationBotToken", "token": "some-token"} + telegram._send_data.assert_called_once_with(exp_data, result_id="updateAuthorizationState") def test_add_message_handler(self, telegram): # check that add_message_handler @@ -152,7 +152,7 @@ def my_handler(): def test_add_update_handler(self, telegram): # check that add_update_handler function # appends passsed func to _update_handlers[type] list - my_update_type = 'update' + my_update_type = "update" assert telegram._update_handlers[my_update_type] == [] def my_handler(): @@ -168,8 +168,8 @@ def my_handler(): telegram.add_message_handler(my_handler) - with patch.object(telegram._workers_queue, 'put') as mocked_put: - update = {'@type': MESSAGE_HANDLER_TYPE} + with patch.object(telegram._workers_queue, "put") as mocked_put: + update = {"@type": MESSAGE_HANDLER_TYPE} telegram._run_handlers(update) mocked_put.assert_called_once_with((my_handler, update), timeout=10) @@ -180,34 +180,34 @@ def my_handler(): telegram.add_message_handler(my_handler) - with patch.object(telegram._workers_queue, 'put') as mocked_put: - update = {'@type': 'some-type'} + with patch.object(telegram._workers_queue, "put") as mocked_put: + update = {"@type": "some-type"} telegram._run_handlers(update) assert mocked_put.call_count == 0 def test_call_method(self, telegram): - method_name = 'someMethod' - params = {'param_1': 'value_1', 'param_2': 2} + method_name = "someMethod" + params = {"param_1": "value_1", "param_2": 2} async_result = telegram.call_method(method_name=method_name, params=params) - exp_data = {'@type': method_name, '@extra': {'request_id': async_result.id}} + exp_data = {"@type": method_name, "@extra": {"request_id": async_result.id}} exp_data.update(params) telegram._tdjson.send.assert_called_once_with(exp_data) def test_get_web_page_instant_view(self, telegram): - url = 'https://yandex.ru/' + url = "https://yandex.ru/" force_full = False async_result = telegram.get_web_page_instant_view(url=url, force_full=force_full) exp_data = { - '@type': 'getWebPageInstantView', - 'url': url, - 'force_full': force_full, - '@extra': {'request_id': async_result.id}, + "@type": "getWebPageInstantView", + "url": url, + "force_full": force_full, + "@extra": {"request_id": async_result.id}, } telegram._tdjson.send.assert_called_once_with(exp_data) @@ -215,7 +215,7 @@ def test_get_web_page_instant_view(self, telegram): def test_get_me(self, telegram): async_result = telegram.get_me() - exp_data = {'@type': 'getMe', '@extra': {'request_id': async_result.id}} + exp_data = {"@type": "getMe", "@extra": {"request_id": async_result.id}} telegram._tdjson.send.assert_called_once_with(exp_data) @@ -225,9 +225,9 @@ def test_get_user(self, telegram): async_result = telegram.get_user(user_id=user_id) exp_data = { - '@type': 'getUser', - 'user_id': user_id, - '@extra': {'request_id': async_result.id}, + "@type": "getUser", + "user_id": user_id, + "@extra": {"request_id": async_result.id}, } telegram._tdjson.send.assert_called_once_with(exp_data) @@ -238,9 +238,9 @@ def test_get_user_full_info(self, telegram): async_result = telegram.get_user_full_info(user_id=user_id) exp_data = { - '@type': 'getUserFullInfo', - 'user_id': user_id, - '@extra': {'request_id': async_result.id}, + "@type": "getUserFullInfo", + "user_id": user_id, + "@extra": {"request_id": async_result.id}, } telegram._tdjson.send.assert_called_once_with(exp_data) @@ -251,9 +251,9 @@ def test_get_chat(self, telegram): async_result = telegram.get_chat(chat_id=chat_id) exp_data = { - '@type': 'getChat', - 'chat_id': chat_id, - '@extra': {'request_id': async_result.id}, + "@type": "getChat", + "chat_id": chat_id, + "@extra": {"request_id": async_result.id}, } telegram._tdjson.send.assert_called_once_with(exp_data) @@ -266,11 +266,11 @@ def test_get_chats(self, telegram): async_result = telegram.get_chats(offset_order=offset_order, offset_chat_id=offset_chat_id, limit=limit) exp_data = { - '@type': 'getChats', - 'offset_order': offset_order, - 'offset_chat_id': offset_chat_id, - 'limit': limit, - '@extra': {'request_id': async_result.id}, + "@type": "getChats", + "offset_order": offset_order, + "offset_chat_id": offset_chat_id, + "limit": limit, + "@extra": {"request_id": async_result.id}, } telegram._tdjson.send.assert_called_once_with(exp_data) @@ -291,50 +291,50 @@ def test_get_chat_history(self, telegram): ) exp_data = { - '@type': 'getChatHistory', - 'chat_id': chat_id, - 'limit': limit, - 'from_message_id': from_message_id, - 'offset': offset, - 'only_local': only_local, - '@extra': {'request_id': async_result.id}, + "@type": "getChatHistory", + "chat_id": chat_id, + "limit": limit, + "from_message_id": from_message_id, + "offset": offset, + "only_local": only_local, + "@extra": {"request_id": async_result.id}, } telegram._tdjson.send.assert_called_once_with(exp_data) @patch("telegram.client.tempfile.gettempdir", return_value="/tmp") def test_set_initial_params(self, _mocked_gettempdir): - telegram = _get_telegram_instance(database_encryption_key='key') + telegram = _get_telegram_instance(database_encryption_key="key") async_result = telegram._set_initial_params() - phone_md5 = '69560384b84c896952ef20352fbce705' + phone_md5 = "69560384b84c896952ef20352fbce705" parameters = { - 'use_test_dc': False, - 'api_id': API_ID, - 'api_hash': API_HASH, - 'device_model': 'python-telegram', - 'system_version': 'unknown', - 'application_version': VERSION, - 'system_language_code': 'en', - 'database_directory': f'/tmp/.tdlib_files/{phone_md5}/database', - 'use_message_database': True, - 'files_directory': f'/tmp/.tdlib_files/{phone_md5}/files', - 'use_secret_chats': True, + "use_test_dc": False, + "api_id": API_ID, + "api_hash": API_HASH, + "device_model": "python-telegram", + "system_version": "unknown", + "application_version": VERSION, + "system_language_code": "en", + "database_directory": f"/tmp/.tdlib_files/{phone_md5}/database", + "use_message_database": True, + "files_directory": f"/tmp/.tdlib_files/{phone_md5}/files", + "use_secret_chats": True, } exp_data = { - '@type': 'setTdlibParameters', - 'parameters': parameters, + "@type": "setTdlibParameters", + "parameters": parameters, **parameters, - 'database_encryption_key': 'a2V5', - '@extra': {'request_id': 'updateAuthorizationState'}, + "database_encryption_key": "a2V5", + "@extra": {"request_id": "updateAuthorizationState"}, } telegram._tdjson.send.assert_called_once_with(exp_data) - assert async_result.id == 'updateAuthorizationState' + assert async_result.id == "updateAuthorizationState" @pytest.mark.parametrize( - 'key, exp_key', - [('key', 'a2V5'), (b'byte-key', 'Ynl0ZS1rZXk='), ('', ''), (b'', '')], + "key, exp_key", + [("key", "a2V5"), (b"byte-key", "Ynl0ZS1rZXk="), ("", ""), (b"", "")], ) def test_send_encryption_key(self, key, exp_key): # check that _send_encryption_key calls tdlib with @@ -344,9 +344,9 @@ def test_send_encryption_key(self, key, exp_key): tg._send_encryption_key() exp_data = { - '@type': 'checkDatabaseEncryptionKey', - 'encryption_key': exp_key, - '@extra': {'request_id': 'updateAuthorizationState'}, + "@type": "checkDatabaseEncryptionKey", + "encryption_key": exp_key, + "@extra": {"request_id": "updateAuthorizationState"}, } tg._tdjson.send.assert_called_once_with(exp_data) @@ -360,22 +360,22 @@ def test_update_async_result_returns_async_result_with_same_id(self, telegram): assert async_result.id in telegram._results - update = {'@extra': {'request_id': async_result.id}} + update = {"@extra": {"request_id": async_result.id}} new_async_result = telegram._update_async_result(update=update) assert async_result == new_async_result def test_result_id_should_be_replaced_if_it_is_auth_process(self, telegram): - async_result = AsyncResult(client=telegram, result_id='updateAuthorizationState') - telegram._results['updateAuthorizationState'] = async_result + async_result = AsyncResult(client=telegram, result_id="updateAuthorizationState") + telegram._results["updateAuthorizationState"] = async_result update = { - '@type': 'updateAuthorizationState', - '@extra': {'request_id': 'blablabla'}, + "@type": "updateAuthorizationState", + "@extra": {"request_id": "blablabla"}, } new_async_result = telegram._update_async_result(update=update) - assert new_async_result.id == 'updateAuthorizationState' + assert new_async_result.id == "updateAuthorizationState" class TestTelegram__login: @@ -399,24 +399,24 @@ def _get_async_result(data, request_id=None): # login process chain telegram.get_authorization_state = lambda: _get_async_result( - data={'@type': 'authorizationStateWaitTdlibParameters'}, - request_id='getAuthorizationState', + data={"@type": "authorizationStateWaitTdlibParameters"}, + request_id="getAuthorizationState", ) telegram._set_initial_params = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitEncryptionKey'}} + data={"authorization_state": {"@type": "authorizationStateWaitEncryptionKey"}} ) telegram._send_encryption_key = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitPhoneNumber'}} + data={"authorization_state": {"@type": "authorizationStateWaitPhoneNumber"}} ) telegram._send_phone_number_or_bot_token = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitCode'}} + data={"authorization_state": {"@type": "authorizationStateWaitCode"}} ) telegram._send_telegram_code = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitPassword'}} + data={"authorization_state": {"@type": "authorizationStateWaitPassword"}} ) telegram._send_password = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateReady'}} + data={"authorization_state": {"@type": "authorizationStateReady"}} ) telegram.login() @@ -439,40 +439,40 @@ def _get_async_result(data, request_id=None): # login process chain telegram.get_authorization_state = lambda: _get_async_result( - data={'@type': 'authorizationStateWaitTdlibParameters'}, - request_id='getAuthorizationState', + data={"@type": "authorizationStateWaitTdlibParameters"}, + request_id="getAuthorizationState", ) telegram._set_initial_params = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitEncryptionKey'}} + data={"authorization_state": {"@type": "authorizationStateWaitEncryptionKey"}} ) telegram._send_encryption_key = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitPhoneNumber'}} + data={"authorization_state": {"@type": "authorizationStateWaitPhoneNumber"}} ) telegram._send_phone_number_or_bot_token = lambda: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitCode'}} + data={"authorization_state": {"@type": "authorizationStateWaitCode"}} ) telegram._send_telegram_code = lambda _: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitRegistration'}} + data={"authorization_state": {"@type": "authorizationStateWaitRegistration"}} ) telegram._register_user = lambda _, __: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateWaitPassword'}} + data={"authorization_state": {"@type": "authorizationStateWaitPassword"}} ) telegram._send_password = lambda _: _get_async_result( - data={'authorization_state': {'@type': 'authorizationStateReady'}} + data={"authorization_state": {"@type": "authorizationStateReady"}} ) state = telegram.login(blocking=False) assert state == AuthorizationState.WAIT_CODE - telegram.send_code('123') + telegram.send_code("123") state = telegram.login(blocking=False) assert state == AuthorizationState.WAIT_REGISTRATION - telegram.register_user('new', 'user') + telegram.register_user("new", "user") state = telegram.login(blocking=False) assert state == AuthorizationState.WAIT_PASSWORD - telegram.send_password('456') + telegram.send_password("456") state = telegram.login(blocking=False) assert state == telegram.authorization_state == AuthorizationState.READY diff --git a/tests/test_utils.py b/tests/test_utils.py index f0dcef5d..f35018ee 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,24 +8,24 @@ class TestAsyncResult: def test_initial_params(self): mocked_uuid = Mock() - mocked_uuid.uuid4().hex = 'some-id' - with patch('telegram.utils.uuid', mocked_uuid): - async_result = AsyncResult(client='123') + mocked_uuid.uuid4().hex = "some-id" + with patch("telegram.utils.uuid", mocked_uuid): + async_result = AsyncResult(client="123") - assert async_result.client == '123' - assert async_result.id == 'some-id' + assert async_result.client == "123" + assert async_result.id == "some-id" def test_str(self): mocked_uuid = Mock() - mocked_uuid.uuid4().hex = 'some-id' - with patch('telegram.utils.uuid', mocked_uuid): + mocked_uuid.uuid4().hex = "some-id" + with patch("telegram.utils.uuid", mocked_uuid): async_result = AsyncResult(client=None) - assert async_result.__str__() == f'AsyncResult ' + assert async_result.__str__() == "AsyncResult " def test_parse_update_with_error(self): async_result = AsyncResult(client=None) - update = {'@type': 'error', 'some': 'data'} + update = {"@type": "error", "some": "data"} assert async_result.error is False assert async_result.error_info is None @@ -40,7 +40,7 @@ def test_parse_update_with_error(self): def test_parse_update_ok(self): async_result = AsyncResult(client=None) - update = {'@type': 'ok', 'some': 'data'} + update = {"@type": "ok", "some": "data"} async_result.parse_update(update) @@ -58,9 +58,9 @@ def test_parse_update_authorization_state_ok(self): # next message with result_id=updateAuthorizationState async_result = AsyncResult( client=None, - result_id='updateAuthorizationState', + result_id="updateAuthorizationState", ) - update = {'@type': 'ok', 'some': 'data'} + update = {"@type": "ok", "some": "data"} async_result.parse_update(update) @@ -72,7 +72,7 @@ def test_parse_update_authorization_state_ok(self): def test_parse_update(self): async_result = AsyncResult(client=None) - update = {'@type': 'some_type', 'some': 'data'} + update = {"@type": "some_type", "some": "data"} async_result.parse_update(update) @@ -90,14 +90,14 @@ def test_wait_with_timeout(self): def test_wait_with_update(self): async_result = AsyncResult(client=None) - async_result.update = '123' + async_result.update = "123" async_result._ready.set() async_result.wait(timeout=0.01) def test_wait_with_error_and_raise_exc(self): async_result = AsyncResult(client=None) async_result.error = True - async_result.error_info = 'some_error' + async_result.error_info = "some_error" async_result._ready.set() with pytest.raises(RuntimeError): @@ -106,6 +106,6 @@ def test_wait_with_error_and_raise_exc(self): def test_wait_with_error_and_without_raise_exc(self): async_result = AsyncResult(client=None) async_result.error = True - async_result.error_info = 'some_error' + async_result.error_info = "some_error" async_result._ready.set() async_result.wait(timeout=0.01) diff --git a/tox.ini b/tox.ini index 39b821d4..b0da0340 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,26 @@ [tox] ignore_basepython_conflict = true -envlist = mypy,flake8,pylint,py38,py39,py310,py311 +envlist = mypy,ruff,ruff-format,py39,py310,py311,py312 [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 - 3.11: py311, mypy, flake8, pylint, black + 3.11: py311 + 3.12: py312, mypy, ruff, ruff-format [testenv] basepython = python3 deps = -rtests/requirements.txt commands = pytest -v {posargs} -[testenv:flake8] -commands = flake8 telegram +[testenv:ruff] +deps = ruff +commands = ruff check -[testenv:pylint] -commands = pylint telegram - -[testenv:black] -commands = black --check telegram +[testenv:ruff-format] +deps = ruff +commands = ruff format --check [testenv:mypy] commands =