mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 20:50:22 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
1047e9c3d5
|
@ -1,4 +1,3 @@
|
||||||
cryptg
|
cryptg
|
||||||
pysocks
|
pysocks
|
||||||
hachoir3
|
hachoir3
|
||||||
sqlalchemy
|
|
||||||
|
|
|
@ -36,77 +36,26 @@ one of the other implementations or implement your own storage.
|
||||||
To use a custom session storage, simply pass the custom session instance to
|
To use a custom session storage, simply pass the custom session instance to
|
||||||
``TelegramClient`` instead of the session name.
|
``TelegramClient`` instead of the session name.
|
||||||
|
|
||||||
Currently, there are three implementations of the abstract ``Session`` class:
|
Telethon contains two implementations of the abstract ``Session`` class:
|
||||||
* ``MemorySession``. Stores session data in Python variables.
|
|
||||||
* ``SQLiteSession``, (default). Stores sessions in their own SQLite databases.
|
|
||||||
* ``AlchemySession``. Stores all sessions in a single database via SQLAlchemy.
|
|
||||||
|
|
||||||
Using AlchemySession
|
* ``MemorySession``: stores session data in Python variables.
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
* ``SQLiteSession``, (default): stores sessions in their own SQLite databases.
|
||||||
The ``AlchemySession`` implementation can store multiple Sessions in the same
|
|
||||||
database, but to do this, each session instance needs to have access to the
|
|
||||||
same models and database session.
|
|
||||||
|
|
||||||
To get started, you need to create an ``AlchemySessionContainer`` which will
|
There are other community-maintained implementations available:
|
||||||
contain that shared data. The simplest way to use ``AlchemySessionContainer``
|
|
||||||
is to simply pass it the database URL:
|
|
||||||
|
|
||||||
.. code-block:: python
|
* `SQLAlchemy <https://github.com/tulir/telethon-session-sqlalchemy>`_: stores all sessions in a single database via SQLAlchemy.
|
||||||
|
* `Redis <https://github.com/ezdev128/telethon-session-redis>`_: stores all sessions in a single Redis data store.
|
||||||
container = AlchemySessionContainer('mysql://user:pass@localhost/telethon')
|
|
||||||
|
|
||||||
If you already have SQLAlchemy set up for your own project, you can also pass
|
|
||||||
the engine separately:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
my_sqlalchemy_engine = sqlalchemy.create_engine('...')
|
|
||||||
container = AlchemySessionContainer(engine=my_sqlalchemy_engine)
|
|
||||||
|
|
||||||
By default, the session container will manage table creation/schema updates/etc
|
|
||||||
automatically. If you want to manage everything yourself, you can pass your
|
|
||||||
SQLAlchemy Session and ``declarative_base`` instances and set ``manage_tables``
|
|
||||||
to ``False``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy import orm
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
session_factory = orm.sessionmaker(bind=my_sqlalchemy_engine)
|
|
||||||
session = session_factory()
|
|
||||||
my_base = declarative_base()
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
container = AlchemySessionContainer(
|
|
||||||
session=session, table_base=my_base, manage_tables=False
|
|
||||||
)
|
|
||||||
|
|
||||||
You always need to provide either ``engine`` or ``session`` to the container.
|
|
||||||
If you set ``manage_tables=False`` and provide a ``session``, ``engine`` is not
|
|
||||||
needed. In any other case, ``engine`` is always required.
|
|
||||||
|
|
||||||
After you have your ``AlchemySessionContainer`` instance created, you can
|
|
||||||
create new sessions by calling ``new_session``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
session = container.new_session('some session id')
|
|
||||||
client = TelegramClient(session)
|
|
||||||
|
|
||||||
where ``some session id`` is an unique identifier for the session.
|
|
||||||
|
|
||||||
Creating your own storage
|
Creating your own storage
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The easiest way to create your own implementation is to use ``MemorySession``
|
The easiest way to create your own storage implementation is to use ``MemorySession``
|
||||||
as the base and check out how ``SQLiteSession`` or ``AlchemySession`` work.
|
as the base and check out how ``SQLiteSession`` or one of the community-maintained
|
||||||
You can find the relevant Python files under the ``sessions`` directory.
|
implementations work. You can find the relevant Python files under the ``sessions``
|
||||||
|
directory in Telethon.
|
||||||
|
|
||||||
|
After you have made your own implementation, you can add it to the community-maintained
|
||||||
|
session implementation list above with a pull request.
|
||||||
|
|
||||||
SQLite Sessions and Heroku
|
SQLite Sessions and Heroku
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
|
@ -10,6 +10,14 @@ The library widely uses the concept of "entities". An entity will refer
|
||||||
to any ``User``, ``Chat`` or ``Channel`` object that the API may return
|
to any ``User``, ``Chat`` or ``Channel`` object that the API may return
|
||||||
in response to certain methods, such as ``GetUsersRequest``.
|
in response to certain methods, such as ``GetUsersRequest``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When something "entity-like" is required, it means that you need to
|
||||||
|
provide something that can be turned into an entity. These things include,
|
||||||
|
but are not limited to, usernames, exact titles, IDs, ``Peer`` objects,
|
||||||
|
or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone
|
||||||
|
numbers from people you have in your contacts.
|
||||||
|
|
||||||
Getting entities
|
Getting entities
|
||||||
****************
|
****************
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,11 @@ TelegramClient
|
||||||
Introduction
|
Introduction
|
||||||
************
|
************
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Check the :ref:`telethon-package` if you're looking for the methods
|
||||||
|
reference instead of this tutorial.
|
||||||
|
|
||||||
The ``TelegramClient`` is the central class of the library, the one
|
The ``TelegramClient`` is the central class of the library, the one
|
||||||
you will be using most of the time. For this reason, it's important
|
you will be using most of the time. For this reason, it's important
|
||||||
to know what it offers.
|
to know what it offers.
|
||||||
|
@ -86,13 +91,7 @@ Please refer to :ref:`accessing-the-full-api` if these aren't enough,
|
||||||
and don't be afraid to read the source code of the InteractiveTelegramClient_
|
and don't be afraid to read the source code of the InteractiveTelegramClient_
|
||||||
or even the TelegramClient_ itself to learn how it works.
|
or even the TelegramClient_ itself to learn how it works.
|
||||||
|
|
||||||
|
To see the methods available in the client, see :ref:`telethon-package`.
|
||||||
|
|
||||||
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
|
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
|
||||||
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
|
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.telegram_client
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
|
@ -7,7 +7,9 @@ Working with Updates
|
||||||
|
|
||||||
The library comes with the :mod:`events` module. *Events* are an abstraction
|
The library comes with the :mod:`events` module. *Events* are an abstraction
|
||||||
over what Telegram calls `updates`__, and are meant to ease simple and common
|
over what Telegram calls `updates`__, and are meant to ease simple and common
|
||||||
usage when dealing with them, since there are many updates. Let's dive in!
|
usage when dealing with them, since there are many updates. If you're looking
|
||||||
|
for the method reference, check :ref:`telethon-events-package`, otherwise,
|
||||||
|
let's dive in!
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
@ -114,12 +116,15 @@ for example:
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
||||||
@client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True))
|
# Either a single item or a list of them will work for the chats.
|
||||||
|
# You can also use the IDs, Peers, or even User/Chat/Channel objects.
|
||||||
|
@client.on(events.NewMessage(chats=('TelethonChat', 'TelethonOffTopic')))
|
||||||
def normal_handler(event):
|
def normal_handler(event):
|
||||||
if 'roll' in event.raw_text:
|
if 'roll' in event.raw_text:
|
||||||
event.reply(str(random.randint(1, 6)))
|
event.reply(str(random.randint(1, 6)))
|
||||||
|
|
||||||
|
|
||||||
|
# Similarly, you can use incoming=True for messages that you receive
|
||||||
@client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True))
|
@client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True))
|
||||||
def admin_handler(event):
|
def admin_handler(event):
|
||||||
if event.raw_text.startswith('eval'):
|
if event.raw_text.startswith('eval'):
|
||||||
|
@ -135,6 +140,20 @@ random number, while if you say ``'eval 4+4'``, you will reply with the
|
||||||
solution. Try it!
|
solution. Try it!
|
||||||
|
|
||||||
|
|
||||||
|
Events without decorators
|
||||||
|
*************************
|
||||||
|
|
||||||
|
If for any reason you can't use the ``@client.on`` syntax, don't worry.
|
||||||
|
You can call ``client.add_event_handler(callback, event)`` to achieve
|
||||||
|
the same effect.
|
||||||
|
|
||||||
|
Similar to that method, you also have :meth:`client.remove_event_handler`
|
||||||
|
and :meth:`client.list_event_handlers` which do as they names indicate.
|
||||||
|
|
||||||
|
The ``event`` type is optional in all methods and defaults to ``events.Raw``
|
||||||
|
for adding, and ``None`` when removing (so all callbacks would be removed).
|
||||||
|
|
||||||
|
|
||||||
Stopping propagation of Updates
|
Stopping propagation of Updates
|
||||||
*******************************
|
*******************************
|
||||||
|
|
||||||
|
@ -162,14 +181,8 @@ propagation of the update through your handlers to stop:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
Events module
|
Remember to check :ref:`telethon-events-package` if you're looking for
|
||||||
*************
|
the methods reference.
|
||||||
|
|
||||||
.. automodule:: telethon.events
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/types/update.html
|
__ https://lonamiwebs.github.io/Telethon/types/update.html
|
||||||
|
|
|
@ -14,6 +14,69 @@ it can take advantage of new goodies!
|
||||||
.. contents:: List of All Versions
|
.. contents:: List of All Versions
|
||||||
|
|
||||||
|
|
||||||
|
Iterator methods (v0.18.1)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
*Published at 2018/03/17*
|
||||||
|
|
||||||
|
All the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_``
|
||||||
|
counterpart, so you can do operations while retrieving items from them.
|
||||||
|
For instance, you can ``client.iter_dialogs()`` and ``break`` once you
|
||||||
|
find what you're looking for instead fetching them all at once.
|
||||||
|
|
||||||
|
Another big thing, you can get entities by just their positive ID. This
|
||||||
|
may cause some collisions (although it's very unlikely), and you can (should)
|
||||||
|
still be explicit about the type you want. However, it's a lot more convenient
|
||||||
|
and less confusing.
|
||||||
|
|
||||||
|
Breaking changes
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- The library only offers the default ``SQLiteSession`` again.
|
||||||
|
See :ref:`sessions` for more on how to use a different storage from now on.
|
||||||
|
|
||||||
|
Additions
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
- Events now override ``__str__`` and implement ``.stringify()``, just like
|
||||||
|
every other ``TLObject`` does.
|
||||||
|
- ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and
|
||||||
|
:meth:`delete` for the message that triggered it.
|
||||||
|
- :meth:`client.iter_participants` (and its :meth:`client.get_participants`
|
||||||
|
counterpart) now expose the ``filter`` argument, and the returned users
|
||||||
|
also expose the ``.participant`` they are.
|
||||||
|
- You can now use :meth:`client.remove_event_handler` and
|
||||||
|
:meth:`client.list_event_handlers` similar how you could with normal updates.
|
||||||
|
- New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif``
|
||||||
|
to access only specific types of documents.
|
||||||
|
- The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a
|
||||||
|
new :meth:`Draft.send` to send it.
|
||||||
|
|
||||||
|
Bug fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
- ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments.
|
||||||
|
- Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``.
|
||||||
|
- Bot API style IDs not working on :meth:`client.get_input_entity`.
|
||||||
|
- :meth:`client.download_media` didn't support ``PhotoSize``.
|
||||||
|
|
||||||
|
Enhancements
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Less RPC are made when accessing the ``.sender`` and ``.chat`` of some
|
||||||
|
events (mostly those that occur in a channel).
|
||||||
|
- You can send albums larger than 10 items (they will be sliced for you),
|
||||||
|
as well as mixing normal files with photos.
|
||||||
|
- ``TLObject`` now have Python type hints.
|
||||||
|
|
||||||
|
Internal changes
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Several documentation corrections.
|
||||||
|
- :meth:`client.get_dialogs` is only called once again when an entity is
|
||||||
|
not found to avoid flood waits.
|
||||||
|
|
||||||
|
|
||||||
Sessions overhaul (v0.18)
|
Sessions overhaul (v0.18)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _api-status:
|
||||||
|
|
||||||
==========
|
==========
|
||||||
API Status
|
API Status
|
||||||
==========
|
==========
|
||||||
|
@ -10,11 +12,9 @@ anyone can query, made by `Daniil <https://github.com/danog>`__. All the
|
||||||
information sent is a ``GET`` request with the error code, error message
|
information sent is a ``GET`` request with the error code, error message
|
||||||
and method used.
|
and method used.
|
||||||
|
|
||||||
If you still would like to opt out, simply set
|
If you still would like to opt out, you can disable this feature by setting
|
||||||
``client.session.report_errors = False`` to disable this feature, or
|
``client.session.report_errors = False``. However Daniil would really thank
|
||||||
pass ``report_errors=False`` as a named parameter when creating a
|
you if you helped him (and everyone) by keeping it on!
|
||||||
``TelegramClient`` instance. However Daniil would really thank you if
|
|
||||||
you helped him (and everyone) by keeping it on!
|
|
||||||
|
|
||||||
Querying the API status
|
Querying the API status
|
||||||
***********************
|
***********************
|
||||||
|
|
|
@ -38,9 +38,9 @@ Kotlin
|
||||||
******
|
******
|
||||||
|
|
||||||
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
|
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
|
||||||
implementation written in Kotlin (the now
|
implementation written in Kotlin (one of the
|
||||||
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
|
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
|
||||||
language for
|
languages for
|
||||||
`Android <https://developer.android.com/kotlin/index.html>`__) by
|
`Android <https://developer.android.com/kotlin/index.html>`__) by
|
||||||
`@badoualy <https://github.com/badoualy>`__, currently as a beta–
|
`@badoualy <https://github.com/badoualy>`__, currently as a beta–
|
||||||
yet working.
|
yet working.
|
||||||
|
|
|
@ -8,8 +8,7 @@ To run Telethon on a test server, use the following code:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
client = TelegramClient(None, api_id, api_hash)
|
client = TelegramClient(None, api_id, api_hash)
|
||||||
client.session.server_address = '149.154.167.40'
|
client.session.set_dc(dc_id, '149.154.167.40', 80)
|
||||||
client.connect()
|
|
||||||
|
|
||||||
You can check your ``'test ip'`` on https://my.telegram.org.
|
You can check your ``'test ip'`` on https://my.telegram.org.
|
||||||
|
|
||||||
|
@ -17,16 +16,20 @@ You should set ``None`` session so to ensure you're generating a new
|
||||||
authorization key for it (it would fail if you used a session where you
|
authorization key for it (it would fail if you used a session where you
|
||||||
had previously connected to another data center).
|
had previously connected to another data center).
|
||||||
|
|
||||||
Once you're connected, you'll likely need to ``.sign_up()``. Remember
|
Note that port 443 might not work, so you can try with 80 instead.
|
||||||
`anyone can access the phone you
|
|
||||||
|
Once you're connected, you'll likely be asked to either sign in or sign up.
|
||||||
|
Remember `anyone can access the phone you
|
||||||
choose <https://core.telegram.org/api/datacenter#testing-redirects>`__,
|
choose <https://core.telegram.org/api/datacenter#testing-redirects>`__,
|
||||||
so don't store sensitive data here:
|
so don't store sensitive data here.
|
||||||
|
|
||||||
|
Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and
|
||||||
|
``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would
|
||||||
|
be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five
|
||||||
|
times, in this case, ``22222`` so we can hardcode that:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from random import randint
|
client = TelegramClient(None, api_id, api_hash)
|
||||||
|
client.session.set_dc(2, '149.154.167.40', 80)
|
||||||
dc_id = '2' # Change this to the DC id of the test server you chose
|
client.start(phone='9996621234', code_callback=lambda: '22222')
|
||||||
phone = '99966' + dc_id + str(randint(9999)).zfill(4)
|
|
||||||
client.send_code_request(phone)
|
|
||||||
client.sign_up(dc_id * 5, 'Some', 'Name')
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ or use the menu on the left. Remember to read the :ref:`changelog`
|
||||||
when you upgrade!
|
when you upgrade!
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
If you're new here, you want to read :ref:`getting-started`.
|
If you're new here, you want to read :ref:`getting-started`. If you're
|
||||||
|
looking for the method reference, you should check :ref:`telethon-package`.
|
||||||
|
|
||||||
|
|
||||||
What is this?
|
What is this?
|
||||||
|
|
|
@ -42,14 +42,6 @@ telethon\.crypto\.factorization module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
telethon\.crypto\.libssl module
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.crypto.libssl
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.crypto\.rsa module
|
telethon\.crypto\.rsa module
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
.. _telethon-events-package:
|
||||||
|
|
||||||
telethon\.events package
|
telethon\.events package
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
.. automodule:: telethon.events
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
.. _telethon-package:
|
||||||
|
|
||||||
|
|
||||||
telethon package
|
telethon package
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
||||||
telethon\.helpers module
|
telethon\.telegram\_client module
|
||||||
------------------------
|
---------------------------------
|
||||||
|
|
||||||
.. automodule:: telethon.helpers
|
.. automodule:: telethon.telegram_client
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
@ -18,10 +21,18 @@ telethon\.telegram\_bare\_client module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
telethon\.telegram\_client module
|
telethon\.utils module
|
||||||
---------------------------------
|
----------------------
|
||||||
|
|
||||||
.. automodule:: telethon.telegram_client
|
.. automodule:: telethon.utils
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
telethon\.helpers module
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: telethon.helpers
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
@ -42,18 +53,10 @@ telethon\.update\_state module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
telethon\.utils module
|
telethon\.sessions module
|
||||||
----------------------
|
-------------------------
|
||||||
|
|
||||||
.. automodule:: telethon.utils
|
.. automodule:: telethon.sessions
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.session module
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.session
|
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pyaes
|
pyaes
|
||||||
rsa
|
rsa
|
||||||
|
typing
|
||||||
|
|
8
setup.py
8
setup.py
|
@ -13,7 +13,7 @@ Extra supported commands are:
|
||||||
|
|
||||||
# To use a consistent encoding
|
# To use a consistent encoding
|
||||||
from codecs import open
|
from codecs import open
|
||||||
from sys import argv
|
from sys import argv, version_info
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -154,10 +154,10 @@ def main():
|
||||||
'telethon_generator/parser/tl_object.py',
|
'telethon_generator/parser/tl_object.py',
|
||||||
'telethon_generator/parser/tl_parser.py',
|
'telethon_generator/parser/tl_parser.py',
|
||||||
]),
|
]),
|
||||||
install_requires=['pyaes', 'rsa'],
|
install_requires=['pyaes', 'rsa',
|
||||||
|
'typing' if version_info < (3, 5) else ""],
|
||||||
extras_require={
|
extras_require={
|
||||||
'cryptg': ['cryptg'],
|
'cryptg': ['cryptg']
|
||||||
'sqlalchemy': ['sqlalchemy']
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
150
telethon/update_state.py
Normal file
150
telethon/update_state.py
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from queue import Queue, Empty
|
||||||
|
from threading import RLock, Thread
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
from .tl import types as tl
|
||||||
|
|
||||||
|
__log__ = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateState:
|
||||||
|
"""Used to hold the current state of processed updates.
|
||||||
|
To retrieve an update, .poll() should be called.
|
||||||
|
"""
|
||||||
|
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers
|
||||||
|
|
||||||
|
def __init__(self, workers=None):
|
||||||
|
"""
|
||||||
|
:param workers: This integer parameter has three possible cases:
|
||||||
|
workers is None: Updates will *not* be stored on self.
|
||||||
|
workers = 0: Another thread is responsible for calling self.poll()
|
||||||
|
workers > 0: 'workers' background threads will be spawned, any
|
||||||
|
any of them will invoke the self.handler.
|
||||||
|
"""
|
||||||
|
self._workers = workers
|
||||||
|
self._worker_threads = []
|
||||||
|
|
||||||
|
self.handler = None
|
||||||
|
self._updates_lock = RLock()
|
||||||
|
self._updates = Queue()
|
||||||
|
|
||||||
|
# https://core.telegram.org/api/updates
|
||||||
|
self._state = tl.updates.State(0, 0, datetime.now(), 0, 0)
|
||||||
|
|
||||||
|
def can_poll(self):
|
||||||
|
"""Returns True if a call to .poll() won't lock"""
|
||||||
|
return not self._updates.empty()
|
||||||
|
|
||||||
|
def poll(self, timeout=None):
|
||||||
|
"""
|
||||||
|
Polls an update or blocks until an update object is available.
|
||||||
|
If 'timeout is not None', it should be a floating point value,
|
||||||
|
and the method will 'return None' if waiting times out.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._updates.get(timeout=timeout)
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_workers(self):
|
||||||
|
return self._workers
|
||||||
|
|
||||||
|
def set_workers(self, n):
|
||||||
|
"""Changes the number of workers running.
|
||||||
|
If 'n is None', clears all pending updates from memory.
|
||||||
|
"""
|
||||||
|
if n is None:
|
||||||
|
self.stop_workers()
|
||||||
|
else:
|
||||||
|
self._workers = n
|
||||||
|
self.setup_workers()
|
||||||
|
|
||||||
|
workers = property(fget=get_workers, fset=set_workers)
|
||||||
|
|
||||||
|
def stop_workers(self):
|
||||||
|
"""
|
||||||
|
Waits for all the worker threads to stop.
|
||||||
|
"""
|
||||||
|
# Put dummy ``None`` objects so that they don't need to timeout.
|
||||||
|
n = self._workers
|
||||||
|
self._workers = None
|
||||||
|
if n:
|
||||||
|
with self._updates_lock:
|
||||||
|
for _ in range(n):
|
||||||
|
self._updates.put(None)
|
||||||
|
|
||||||
|
for t in self._worker_threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
self._worker_threads.clear()
|
||||||
|
self._workers = n
|
||||||
|
|
||||||
|
def setup_workers(self):
|
||||||
|
if self._worker_threads or not self._workers:
|
||||||
|
# There already are workers, or workers is None or 0. Do nothing.
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(self._workers):
|
||||||
|
thread = Thread(
|
||||||
|
target=UpdateState._worker_loop,
|
||||||
|
name='UpdateWorker{}'.format(i),
|
||||||
|
daemon=True,
|
||||||
|
args=(self, i)
|
||||||
|
)
|
||||||
|
self._worker_threads.append(thread)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _worker_loop(self, wid):
|
||||||
|
while self._workers is not None:
|
||||||
|
try:
|
||||||
|
update = self.poll(timeout=UpdateState.WORKER_POLL_TIMEOUT)
|
||||||
|
if update and self.handler:
|
||||||
|
self.handler(update)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
# We don't want to crash a worker thread due to any reason
|
||||||
|
__log__.exception('Unhandled exception on worker %d', wid)
|
||||||
|
|
||||||
|
def process(self, update):
|
||||||
|
"""Processes an update object. This method is normally called by
|
||||||
|
the library itself.
|
||||||
|
"""
|
||||||
|
if self._workers is None:
|
||||||
|
return # No processing needs to be done if nobody's working
|
||||||
|
|
||||||
|
with self._updates_lock:
|
||||||
|
if isinstance(update, tl.updates.State):
|
||||||
|
__log__.debug('Saved new updates state')
|
||||||
|
self._state = update
|
||||||
|
return # Nothing else to be done
|
||||||
|
|
||||||
|
if hasattr(update, 'pts'):
|
||||||
|
self._state.pts = update.pts
|
||||||
|
|
||||||
|
# After running the script for over an hour and receiving over
|
||||||
|
# 1000 updates, the only duplicates received were users going
|
||||||
|
# online or offline. We can trust the server until new reports.
|
||||||
|
#
|
||||||
|
# TODO Note somewhere that all updates are modified to include
|
||||||
|
# .entities, which is a dictionary you can access but may be empty.
|
||||||
|
# This should only be used as read-only.
|
||||||
|
if isinstance(update, tl.UpdateShort):
|
||||||
|
update.update.entities = {}
|
||||||
|
self._updates.put(update.update)
|
||||||
|
# Expand "Updates" into "Update", and pass these to callbacks.
|
||||||
|
# Since .users and .chats have already been processed, we
|
||||||
|
# don't need to care about those either.
|
||||||
|
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
|
||||||
|
entities = {utils.get_peer_id(x): x for x in
|
||||||
|
itertools.chain(update.users, update.chats)}
|
||||||
|
for u in update.updates:
|
||||||
|
u.entities = entities
|
||||||
|
self._updates.put(u)
|
||||||
|
# TODO Handle "tl.UpdatesTooLong"
|
||||||
|
else:
|
||||||
|
update.entities = {}
|
||||||
|
self._updates.put(update)
|
|
@ -2,11 +2,12 @@ import abc
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
|
import warnings
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from ..errors import RPCError
|
from ..errors import RPCError
|
||||||
from ..extensions import markdown
|
from ..extensions import markdown
|
||||||
from ..tl import types, functions
|
from ..tl import TLObject, types, functions
|
||||||
|
|
||||||
|
|
||||||
async def _into_id_set(client, chats):
|
async def _into_id_set(client, chats):
|
||||||
|
@ -71,6 +72,7 @@ class _EventCommon(abc.ABC):
|
||||||
"""Intermediate class with common things to all events"""
|
"""Intermediate class with common things to all events"""
|
||||||
|
|
||||||
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
||||||
|
self._entities = {}
|
||||||
self._client = None
|
self._client = None
|
||||||
self._chat_peer = chat_peer
|
self._chat_peer = chat_peer
|
||||||
self._message_id = msg_id
|
self._message_id = msg_id
|
||||||
|
@ -86,12 +88,15 @@ class _EventCommon(abc.ABC):
|
||||||
)
|
)
|
||||||
self.is_channel = isinstance(chat_peer, types.PeerChannel)
|
self.is_channel = isinstance(chat_peer, types.PeerChannel)
|
||||||
|
|
||||||
async def _get_input_entity(self, msg_id, entity_id, chat=None):
|
async def _get_entity(self, msg_id, entity_id, chat=None):
|
||||||
"""
|
"""
|
||||||
Helper function to call GetMessages on the give msg_id and
|
Helper function to call GetMessages on the give msg_id and
|
||||||
return the input entity whose ID is the given entity ID.
|
return the input entity whose ID is the given entity ID.
|
||||||
|
|
||||||
If ``chat`` is present it must be an InputPeer.
|
If ``chat`` is present it must be an InputPeer.
|
||||||
|
|
||||||
|
Returns a tuple of (entity, input_peer) if it was found, or
|
||||||
|
a tuple of (None, None) if it couldn't be.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if isinstance(chat, types.InputPeerChannel):
|
if isinstance(chat, types.InputPeerChannel):
|
||||||
|
@ -103,14 +108,17 @@ class _EventCommon(abc.ABC):
|
||||||
functions.messages.GetMessagesRequest([msg_id])
|
functions.messages.GetMessagesRequest([msg_id])
|
||||||
)
|
)
|
||||||
except RPCError:
|
except RPCError:
|
||||||
return
|
return None, None
|
||||||
|
|
||||||
entity = {
|
entity = {
|
||||||
utils.get_peer_id(x): x for x in itertools.chain(
|
utils.get_peer_id(x): x for x in itertools.chain(
|
||||||
getattr(result, 'chats', []),
|
getattr(result, 'chats', []),
|
||||||
getattr(result, 'users', []))
|
getattr(result, 'users', []))
|
||||||
}.get(entity_id)
|
}.get(entity_id)
|
||||||
if entity:
|
if entity:
|
||||||
return utils.get_input_peer(entity)
|
return entity, utils.get_input_peer(entity)
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def input_chat(self):
|
async def input_chat(self):
|
||||||
|
@ -134,7 +142,7 @@ class _EventCommon(abc.ABC):
|
||||||
# TODO For channels, getDifference? Maybe looking
|
# TODO For channels, getDifference? Maybe looking
|
||||||
# in the dialogs (which is already done) is enough.
|
# in the dialogs (which is already done) is enough.
|
||||||
if self._message_id is not None:
|
if self._message_id is not None:
|
||||||
self._input_chat = await self._get_input_entity(
|
self._chat, self._input_chat = await self._get_entity(
|
||||||
self._message_id,
|
self._message_id,
|
||||||
utils.get_peer_id(self._chat_peer)
|
utils.get_peer_id(self._chat_peer)
|
||||||
)
|
)
|
||||||
|
@ -147,14 +155,32 @@ class _EventCommon(abc.ABC):
|
||||||
async def chat(self):
|
async def chat(self):
|
||||||
"""
|
"""
|
||||||
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
|
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
|
||||||
the event occurred. This property will make an API call the first time
|
the event occurred. This property may make an API call the first time
|
||||||
to get the most up to date version of the chat, so use with care as
|
to get the most up to date version of the chat (mostly when the event
|
||||||
there is no caching besides local caching yet.
|
doesn't belong to a channel), so keep that in mind.
|
||||||
"""
|
"""
|
||||||
if self._chat is None and await self.input_chat:
|
if not await self.input_chat:
|
||||||
self._chat = await self._client.get_entity(await self._input_chat)
|
return None
|
||||||
|
|
||||||
|
if self._chat is None:
|
||||||
|
self._chat = self._entities.get(utils.get_peer_id(self._input_chat))
|
||||||
|
|
||||||
|
if self._chat is None:
|
||||||
|
self._chat = await self._client.get_entity(self._input_chat)
|
||||||
|
|
||||||
return self._chat
|
return self._chat
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return TLObject.pretty_format(self.to_dict())
|
||||||
|
|
||||||
|
def stringify(self):
|
||||||
|
return TLObject.pretty_format(self.to_dict(), indent=0)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
|
||||||
|
d['_'] = self.__class__.__name__
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
class Raw(_EventBuilder):
|
class Raw(_EventBuilder):
|
||||||
"""
|
"""
|
||||||
|
@ -167,9 +193,19 @@ class Raw(_EventBuilder):
|
||||||
return update
|
return update
|
||||||
|
|
||||||
|
|
||||||
|
def _name_inner_event(cls):
|
||||||
|
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
|
||||||
|
if hasattr(cls, 'Event'):
|
||||||
|
cls.Event.__name__ = '{}.Event'.format(cls.__name__)
|
||||||
|
else:
|
||||||
|
warnings.warn('Class {} does not have a inner Event'.format(cls))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
# Classes defined here are actually Event builders
|
# Classes defined here are actually Event builders
|
||||||
# for their inner Event classes. Inner ._client is
|
# for their inner Event classes. Inner ._client is
|
||||||
# set later by the creator TelegramClient.
|
# set later by the creator TelegramClient.
|
||||||
|
@_name_inner_event
|
||||||
class NewMessage(_EventBuilder):
|
class NewMessage(_EventBuilder):
|
||||||
"""
|
"""
|
||||||
Represents a new message event builder.
|
Represents a new message event builder.
|
||||||
|
@ -247,6 +283,10 @@ class NewMessage(_EventBuilder):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
event._entities = update.entities
|
||||||
|
return self._message_filter_event(event)
|
||||||
|
|
||||||
|
def _message_filter_event(self, event):
|
||||||
# Short-circuit if we let pass all events
|
# Short-circuit if we let pass all events
|
||||||
if all(x is None for x in (self.incoming, self.outgoing, self.chats,
|
if all(x is None for x in (self.incoming, self.outgoing, self.chats,
|
||||||
self.pattern)):
|
self.pattern)):
|
||||||
|
@ -299,8 +339,6 @@ class NewMessage(_EventBuilder):
|
||||||
self.message = message
|
self.message = message
|
||||||
self._text = None
|
self._text = None
|
||||||
|
|
||||||
self._input_chat = None
|
|
||||||
self._chat = None
|
|
||||||
self._input_sender = None
|
self._input_sender = None
|
||||||
self._sender = None
|
self._sender = None
|
||||||
|
|
||||||
|
@ -384,7 +422,7 @@ class NewMessage(_EventBuilder):
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
# We can rely on self.input_chat for this
|
# We can rely on self.input_chat for this
|
||||||
self._input_sender = await self._get_input_entity(
|
self._sender, self._input_sender = await self._get_entity(
|
||||||
self.message.id,
|
self.message.id,
|
||||||
self.message.from_id,
|
self.message.from_id,
|
||||||
chat=await self.input_chat
|
chat=await self.input_chat
|
||||||
|
@ -395,14 +433,22 @@ class NewMessage(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def sender(self):
|
async def sender(self):
|
||||||
"""
|
"""
|
||||||
This (:obj:`User`) will make an API call the first time to get
|
This (:obj:`User`) may make an API call the first time to get
|
||||||
the most up to date version of the sender, so use with care as
|
the most up to date version of the sender (mostly when the event
|
||||||
there is no caching besides local caching yet.
|
doesn't belong to a channel), so keep that in mind.
|
||||||
|
|
||||||
``input_sender`` needs to be available (often the case).
|
``input_sender`` needs to be available (often the case).
|
||||||
"""
|
"""
|
||||||
if self._sender is None and await self.input_sender:
|
if not await self.input_sender:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._sender is None:
|
||||||
|
self._sender = \
|
||||||
|
self._entities.get(utils.get_peer_id(self._input_sender))
|
||||||
|
|
||||||
|
if self._sender is None:
|
||||||
self._sender = await self._client.get_entity(self._input_sender)
|
self._sender = await self._client.get_entity(self._input_sender)
|
||||||
|
|
||||||
return self._sender
|
return self._sender
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -556,6 +602,7 @@ class NewMessage(_EventBuilder):
|
||||||
return self.message.out
|
return self.message.out
|
||||||
|
|
||||||
|
|
||||||
|
@_name_inner_event
|
||||||
class ChatAction(_EventBuilder):
|
class ChatAction(_EventBuilder):
|
||||||
"""
|
"""
|
||||||
Represents an action in a chat (such as user joined, left, or new pin).
|
Represents an action in a chat (such as user joined, left, or new pin).
|
||||||
|
@ -621,6 +668,7 @@ class ChatAction(_EventBuilder):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
event._entities = update.entities
|
||||||
return self._filter_event(event)
|
return self._filter_event(event)
|
||||||
|
|
||||||
class Event(_EventCommon):
|
class Event(_EventCommon):
|
||||||
|
@ -764,7 +812,13 @@ class ChatAction(_EventBuilder):
|
||||||
The user who added ``users``, if applicable (``None`` otherwise).
|
The user who added ``users``, if applicable (``None`` otherwise).
|
||||||
"""
|
"""
|
||||||
if self._added_by and not isinstance(self._added_by, types.User):
|
if self._added_by and not isinstance(self._added_by, types.User):
|
||||||
self._added_by = await self._client.get_entity(self._added_by)
|
self._added_by =\
|
||||||
|
self._entities.get(utils.get_peer_id(self._added_by))
|
||||||
|
|
||||||
|
if not self._added_by:
|
||||||
|
self._added_by =\
|
||||||
|
await self._client.get_entity(self._added_by)
|
||||||
|
|
||||||
return self._added_by
|
return self._added_by
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -773,7 +827,13 @@ class ChatAction(_EventBuilder):
|
||||||
The user who kicked ``users``, if applicable (``None`` otherwise).
|
The user who kicked ``users``, if applicable (``None`` otherwise).
|
||||||
"""
|
"""
|
||||||
if self._kicked_by and not isinstance(self._kicked_by, types.User):
|
if self._kicked_by and not isinstance(self._kicked_by, types.User):
|
||||||
self._kicked_by = await self._client.get_entity(self._kicked_by)
|
self._kicked_by =\
|
||||||
|
self._entities.get(utils.get_peer_id(self._kicked_by))
|
||||||
|
|
||||||
|
if not self._kicked_by:
|
||||||
|
self._kicked_by =\
|
||||||
|
await self._client.get_entity(self._kicked_by)
|
||||||
|
|
||||||
return self._kicked_by
|
return self._kicked_by
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -803,11 +863,24 @@ class ChatAction(_EventBuilder):
|
||||||
Might be empty if the information can't be retrieved or there
|
Might be empty if the information can't be retrieved or there
|
||||||
are no users taking part.
|
are no users taking part.
|
||||||
"""
|
"""
|
||||||
if self._users is None and self._user_peers:
|
if not self._user_peers:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if self._users is None:
|
||||||
|
have, missing = [], []
|
||||||
|
for peer in self._user_peers:
|
||||||
|
user = self._entities.get(utils.get_peer_id(peer))
|
||||||
|
if user:
|
||||||
|
have.append(user)
|
||||||
|
else:
|
||||||
|
missing.append(peer)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._users = await self._client.get_entity(self._user_peers)
|
missing = await self._client.get_entity(missing)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
self._users = []
|
missing = []
|
||||||
|
|
||||||
|
self._user_peers = have + missing
|
||||||
|
|
||||||
return self._users
|
return self._users
|
||||||
|
|
||||||
|
@ -828,6 +901,7 @@ class ChatAction(_EventBuilder):
|
||||||
return self._input_users
|
return self._input_users
|
||||||
|
|
||||||
|
|
||||||
|
@_name_inner_event
|
||||||
class UserUpdate(_EventBuilder):
|
class UserUpdate(_EventBuilder):
|
||||||
"""
|
"""
|
||||||
Represents an user update (gone online, offline, joined Telegram).
|
Represents an user update (gone online, offline, joined Telegram).
|
||||||
|
@ -839,6 +913,7 @@ class UserUpdate(_EventBuilder):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
event._entities = update.entities
|
||||||
return self._filter_event(event)
|
return self._filter_event(event)
|
||||||
|
|
||||||
class Event(_EventCommon):
|
class Event(_EventCommon):
|
||||||
|
@ -975,6 +1050,7 @@ class UserUpdate(_EventBuilder):
|
||||||
return self.chat
|
return self.chat
|
||||||
|
|
||||||
|
|
||||||
|
@_name_inner_event
|
||||||
class MessageEdited(NewMessage):
|
class MessageEdited(NewMessage):
|
||||||
"""
|
"""
|
||||||
Event fired when a message has been edited.
|
Event fired when a message has been edited.
|
||||||
|
@ -986,9 +1062,11 @@ class MessageEdited(NewMessage):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
return self._filter_event(event)
|
event._entities = update.entities
|
||||||
|
return self._message_filter_event(event)
|
||||||
|
|
||||||
|
|
||||||
|
@_name_inner_event
|
||||||
class MessageDeleted(_EventBuilder):
|
class MessageDeleted(_EventBuilder):
|
||||||
"""
|
"""
|
||||||
Event fired when one or more messages are deleted.
|
Event fired when one or more messages are deleted.
|
||||||
|
@ -1007,6 +1085,7 @@ class MessageDeleted(_EventBuilder):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
event._entities = update.entities
|
||||||
return self._filter_event(event)
|
return self._filter_event(event)
|
||||||
|
|
||||||
class Event(_EventCommon):
|
class Event(_EventCommon):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from .abstract import Session
|
from .abstract import Session
|
||||||
from .memory import MemorySession
|
from .memory import MemorySession
|
||||||
from .sqlite import SQLiteSession
|
from .sqlite import SQLiteSession
|
||||||
from .sqlalchemy import AlchemySessionContainer, AlchemySession
|
|
||||||
|
|
|
@ -1,236 +0,0 @@
|
||||||
try:
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy import Column, String, Integer, LargeBinary, orm
|
|
||||||
import sqlalchemy as sql
|
|
||||||
except ImportError:
|
|
||||||
sql = None
|
|
||||||
|
|
||||||
from .memory import MemorySession, _SentFileType
|
|
||||||
from .. import utils
|
|
||||||
from ..crypto import AuthKey
|
|
||||||
from ..tl.types import (
|
|
||||||
InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel
|
|
||||||
)
|
|
||||||
|
|
||||||
LATEST_VERSION = 1
|
|
||||||
|
|
||||||
|
|
||||||
class AlchemySessionContainer:
|
|
||||||
def __init__(self, engine=None, session=None, table_prefix='',
|
|
||||||
table_base=None, manage_tables=True):
|
|
||||||
if not sql:
|
|
||||||
raise ImportError('SQLAlchemy not imported')
|
|
||||||
if isinstance(engine, str):
|
|
||||||
engine = sql.create_engine(engine)
|
|
||||||
|
|
||||||
self.db_engine = engine
|
|
||||||
if not session:
|
|
||||||
db_factory = orm.sessionmaker(bind=self.db_engine)
|
|
||||||
self.db = orm.scoping.scoped_session(db_factory)
|
|
||||||
else:
|
|
||||||
self.db = session
|
|
||||||
|
|
||||||
table_base = table_base or declarative_base()
|
|
||||||
(self.Version, self.Session, self.Entity,
|
|
||||||
self.SentFile) = self.create_table_classes(self.db, table_prefix,
|
|
||||||
table_base)
|
|
||||||
|
|
||||||
if manage_tables:
|
|
||||||
table_base.metadata.bind = self.db_engine
|
|
||||||
if not self.db_engine.dialect.has_table(self.db_engine,
|
|
||||||
self.Version.__tablename__):
|
|
||||||
table_base.metadata.create_all()
|
|
||||||
self.db.add(self.Version(version=LATEST_VERSION))
|
|
||||||
self.db.commit()
|
|
||||||
else:
|
|
||||||
self.check_and_upgrade_database()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_table_classes(db, prefix, Base):
|
|
||||||
class Version(Base):
|
|
||||||
query = db.query_property()
|
|
||||||
__tablename__ = '{prefix}version'.format(prefix=prefix)
|
|
||||||
version = Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
class Session(Base):
|
|
||||||
query = db.query_property()
|
|
||||||
__tablename__ = '{prefix}sessions'.format(prefix=prefix)
|
|
||||||
|
|
||||||
session_id = Column(String, primary_key=True)
|
|
||||||
dc_id = Column(Integer, primary_key=True)
|
|
||||||
server_address = Column(String)
|
|
||||||
port = Column(Integer)
|
|
||||||
auth_key = Column(LargeBinary)
|
|
||||||
|
|
||||||
class Entity(Base):
|
|
||||||
query = db.query_property()
|
|
||||||
__tablename__ = '{prefix}entities'.format(prefix=prefix)
|
|
||||||
|
|
||||||
session_id = Column(String, primary_key=True)
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
hash = Column(Integer, nullable=False)
|
|
||||||
username = Column(String)
|
|
||||||
phone = Column(Integer)
|
|
||||||
name = Column(String)
|
|
||||||
|
|
||||||
class SentFile(Base):
|
|
||||||
query = db.query_property()
|
|
||||||
__tablename__ = '{prefix}sent_files'.format(prefix=prefix)
|
|
||||||
|
|
||||||
session_id = Column(String, primary_key=True)
|
|
||||||
md5_digest = Column(LargeBinary, primary_key=True)
|
|
||||||
file_size = Column(Integer, primary_key=True)
|
|
||||||
type = Column(Integer, primary_key=True)
|
|
||||||
id = Column(Integer)
|
|
||||||
hash = Column(Integer)
|
|
||||||
|
|
||||||
return Version, Session, Entity, SentFile
|
|
||||||
|
|
||||||
def check_and_upgrade_database(self):
|
|
||||||
row = self.Version.query.all()
|
|
||||||
version = row[0].version if row else 1
|
|
||||||
if version == LATEST_VERSION:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.Version.query.delete()
|
|
||||||
|
|
||||||
# Implement table schema updates here and increase version
|
|
||||||
|
|
||||||
self.db.add(self.Version(version=version))
|
|
||||||
self.db.commit()
|
|
||||||
|
|
||||||
def new_session(self, session_id):
|
|
||||||
return AlchemySession(self, session_id)
|
|
||||||
|
|
||||||
def list_sessions(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
self.db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class AlchemySession(MemorySession):
|
|
||||||
def __init__(self, container, session_id):
|
|
||||||
super().__init__()
|
|
||||||
self.container = container
|
|
||||||
self.db = container.db
|
|
||||||
self.Version, self.Session, self.Entity, self.SentFile = (
|
|
||||||
container.Version, container.Session, container.Entity,
|
|
||||||
container.SentFile)
|
|
||||||
self.session_id = session_id
|
|
||||||
self._load_session()
|
|
||||||
|
|
||||||
def _load_session(self):
|
|
||||||
sessions = self._db_query(self.Session).all()
|
|
||||||
session = sessions[0] if sessions else None
|
|
||||||
if session:
|
|
||||||
self._dc_id = session.dc_id
|
|
||||||
self._server_address = session.server_address
|
|
||||||
self._port = session.port
|
|
||||||
self._auth_key = AuthKey(data=session.auth_key)
|
|
||||||
|
|
||||||
def clone(self, to_instance=None):
|
|
||||||
return super().clone(MemorySession())
|
|
||||||
|
|
||||||
def set_dc(self, dc_id, server_address, port):
|
|
||||||
super().set_dc(dc_id, server_address, port)
|
|
||||||
self._update_session_table()
|
|
||||||
|
|
||||||
sessions = self._db_query(self.Session).all()
|
|
||||||
session = sessions[0] if sessions else None
|
|
||||||
if session and session.auth_key:
|
|
||||||
self._auth_key = AuthKey(data=session.auth_key)
|
|
||||||
else:
|
|
||||||
self._auth_key = None
|
|
||||||
|
|
||||||
@MemorySession.auth_key.setter
|
|
||||||
def auth_key(self, value):
|
|
||||||
self._auth_key = value
|
|
||||||
self._update_session_table()
|
|
||||||
|
|
||||||
def _update_session_table(self):
|
|
||||||
self.Session.query.filter(
|
|
||||||
self.Session.session_id == self.session_id).delete()
|
|
||||||
new = self.Session(session_id=self.session_id, dc_id=self._dc_id,
|
|
||||||
server_address=self._server_address,
|
|
||||||
port=self._port,
|
|
||||||
auth_key=(self._auth_key.key
|
|
||||||
if self._auth_key else b''))
|
|
||||||
self.db.merge(new)
|
|
||||||
|
|
||||||
def _db_query(self, dbclass, *args):
|
|
||||||
return dbclass.query.filter(
|
|
||||||
dbclass.session_id == self.session_id, *args
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
self.container.save()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
# Nothing to do here, connection is managed by AlchemySessionContainer.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
self._db_query(self.Session).delete()
|
|
||||||
self._db_query(self.Entity).delete()
|
|
||||||
self._db_query(self.SentFile).delete()
|
|
||||||
|
|
||||||
def _entity_values_to_row(self, id, hash, username, phone, name):
|
|
||||||
return self.Entity(session_id=self.session_id, id=id, hash=hash,
|
|
||||||
username=username, phone=phone, name=name)
|
|
||||||
|
|
||||||
def process_entities(self, tlo):
|
|
||||||
rows = self._entities_to_rows(tlo)
|
|
||||||
if not rows:
|
|
||||||
return
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
self.db.merge(row)
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def get_entity_rows_by_phone(self, key):
|
|
||||||
row = self._db_query(self.Entity,
|
|
||||||
self.Entity.phone == key).one_or_none()
|
|
||||||
return (row.id, row.hash) if row else None
|
|
||||||
|
|
||||||
def get_entity_rows_by_username(self, key):
|
|
||||||
row = self._db_query(self.Entity,
|
|
||||||
self.Entity.username == key).one_or_none()
|
|
||||||
return (row.id, row.hash) if row else None
|
|
||||||
|
|
||||||
def get_entity_rows_by_name(self, key):
|
|
||||||
row = self._db_query(self.Entity,
|
|
||||||
self.Entity.name == key).one_or_none()
|
|
||||||
return (row.id, row.hash) if row else None
|
|
||||||
|
|
||||||
def get_entity_rows_by_id(self, key, exact=True):
|
|
||||||
if exact:
|
|
||||||
query = self._db_query(self.Entity, self.Entity.id == key)
|
|
||||||
else:
|
|
||||||
ids = (
|
|
||||||
utils.get_peer_id(PeerUser(key)),
|
|
||||||
utils.get_peer_id(PeerChat(key)),
|
|
||||||
utils.get_peer_id(PeerChannel(key))
|
|
||||||
)
|
|
||||||
query = self._db_query(self.Entity, self.Entity.id in ids)
|
|
||||||
|
|
||||||
row = query.one_or_none()
|
|
||||||
return (row.id, row.hash) if row else None
|
|
||||||
|
|
||||||
def get_file(self, md5_digest, file_size, cls):
|
|
||||||
row = self._db_query(self.SentFile,
|
|
||||||
self.SentFile.md5_digest == md5_digest,
|
|
||||||
self.SentFile.file_size == file_size,
|
|
||||||
self.SentFile.type == _SentFileType.from_type(
|
|
||||||
cls).value).one_or_none()
|
|
||||||
return (row.id, row.hash) if row else None
|
|
||||||
|
|
||||||
def cache_file(self, md5_digest, file_size, instance):
|
|
||||||
if not isinstance(instance, (InputDocument, InputPhoto)):
|
|
||||||
raise TypeError('Cannot cache %s instance' % type(instance))
|
|
||||||
|
|
||||||
self.db.merge(
|
|
||||||
self.SentFile(session_id=self.session_id, md5_digest=md5_digest,
|
|
||||||
type=_SentFileType.from_type(type(instance)).value,
|
|
||||||
id=instance.id, hash=instance.access_hash))
|
|
||||||
self.save()
|
|
|
@ -70,6 +70,7 @@ class TelegramBareClient:
|
||||||
proxy=None,
|
proxy=None,
|
||||||
timeout=timedelta(seconds=5),
|
timeout=timedelta(seconds=5),
|
||||||
loop=None,
|
loop=None,
|
||||||
|
report_errors=True,
|
||||||
device_model=None,
|
device_model=None,
|
||||||
system_version=None,
|
system_version=None,
|
||||||
app_version=None,
|
app_version=None,
|
||||||
|
@ -102,6 +103,7 @@ class TelegramBareClient:
|
||||||
DEFAULT_PORT
|
DEFAULT_PORT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
session.report_errors = report_errors
|
||||||
self.session = session
|
self.session = session
|
||||||
self.api_id = int(api_id)
|
self.api_id = int(api_id)
|
||||||
self.api_hash = api_hash
|
self.api_hash = api_hash
|
||||||
|
|
|
@ -146,9 +146,13 @@ class TelegramClient(TelegramBareClient):
|
||||||
instantly, as soon as they arrive. Can still be disabled
|
instantly, as soon as they arrive. Can still be disabled
|
||||||
if you want to run the library without any additional thread.
|
if you want to run the library without any additional thread.
|
||||||
|
|
||||||
|
report_errors (:obj:`bool`, optional):
|
||||||
|
Whether to report RPC errors or not. Defaults to ``True``,
|
||||||
|
see :ref:`api-status` for more information.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
Extra parameters will be forwarded to the ``Session`` file.
|
Some extra parameters are required when establishing the first
|
||||||
Most relevant parameters are:
|
connection. These are are (along with their default values):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -157,7 +161,6 @@ class TelegramClient(TelegramBareClient):
|
||||||
app_version = TelegramClient.__version__
|
app_version = TelegramClient.__version__
|
||||||
lang_code = 'en'
|
lang_code = 'en'
|
||||||
system_lang_code = lang_code
|
system_lang_code = lang_code
|
||||||
report_errors = True
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# region Initialization
|
# region Initialization
|
||||||
|
@ -168,6 +171,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
proxy=None,
|
proxy=None,
|
||||||
timeout=timedelta(seconds=5),
|
timeout=timedelta(seconds=5),
|
||||||
loop=None,
|
loop=None,
|
||||||
|
report_errors=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
session, api_id, api_hash,
|
session, api_id, api_hash,
|
||||||
|
@ -176,6 +180,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
proxy=proxy,
|
proxy=proxy,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
|
report_errors=report_errors,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,6 +195,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
# Sometimes we need to know who we are, cache the self peer
|
# Sometimes we need to know who we are, cache the self peer
|
||||||
self._self_input_peer = None
|
self._self_input_peer = None
|
||||||
|
|
||||||
|
# Don't call .get_dialogs() every time a .get_entity() fails
|
||||||
|
self._called_get_dialogs = False
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Telegram requests functions
|
# region Telegram requests functions
|
||||||
|
@ -1156,9 +1164,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
raise TypeError('Invalid message type: {}'.format(type(message)))
|
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||||
|
|
||||||
async def iter_participants(self, entity, limit=None, search='',
|
async def iter_participants(self, entity, limit=None, search='',
|
||||||
aggressive=False, _total_box=None):
|
filter=None, aggressive=False,
|
||||||
|
_total_box=None):
|
||||||
"""
|
"""
|
||||||
Gets the list of participants from the specified entity.
|
Iterator over the participants belonging to the specified chat.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
entity (:obj:`entity`):
|
entity (:obj:`entity`):
|
||||||
|
@ -1170,6 +1179,12 @@ class TelegramClient(TelegramBareClient):
|
||||||
search (:obj:`str`, optional):
|
search (:obj:`str`, optional):
|
||||||
Look for participants with this string in name/username.
|
Look for participants with this string in name/username.
|
||||||
|
|
||||||
|
filter (:obj:`ChannelParticipantsFilter`, optional):
|
||||||
|
The filter to be used, if you want e.g. only admins. See
|
||||||
|
https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html.
|
||||||
|
Note that you might not have permissions for some filter.
|
||||||
|
This has no effect for normal chats or users.
|
||||||
|
|
||||||
aggressive (:obj:`bool`, optional):
|
aggressive (:obj:`bool`, optional):
|
||||||
Aggressively looks for all participants in the chat in
|
Aggressively looks for all participants in the chat in
|
||||||
order to get more than 10,000 members (a hard limit
|
order to get more than 10,000 members (a hard limit
|
||||||
|
@ -1178,16 +1193,32 @@ class TelegramClient(TelegramBareClient):
|
||||||
participants on groups with 100,000 members.
|
participants on groups with 100,000 members.
|
||||||
|
|
||||||
This has no effect for groups or channels with less than
|
This has no effect for groups or channels with less than
|
||||||
10,000 members.
|
10,000 members, or if a ``filter`` is given.
|
||||||
|
|
||||||
_total_box (:obj:`_Box`, optional):
|
_total_box (:obj:`_Box`, optional):
|
||||||
A _Box instance to pass the total parameter by reference.
|
A _Box instance to pass the total parameter by reference.
|
||||||
|
|
||||||
Returns:
|
Yields:
|
||||||
A list of participants with an additional .total variable on the
|
The ``User`` objects returned by ``GetParticipantsRequest``
|
||||||
list indicating the total amount of members in this group/channel.
|
with an additional ``.participant`` attribute which is the
|
||||||
|
matched ``ChannelParticipant`` type for channels/megagroups
|
||||||
|
or ``ChatParticipants`` for normal chats.
|
||||||
"""
|
"""
|
||||||
|
if isinstance(filter, type):
|
||||||
|
filter = filter()
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
|
if search and (filter or not isinstance(entity, InputPeerChannel)):
|
||||||
|
# We need to 'search' ourselves unless we have a PeerChannel
|
||||||
|
search = search.lower()
|
||||||
|
|
||||||
|
def filter_entity(ent):
|
||||||
|
return search in utils.get_display_name(ent).lower() or\
|
||||||
|
search in (getattr(ent, 'username', '') or None).lower()
|
||||||
|
else:
|
||||||
|
def filter_entity(ent):
|
||||||
|
return True
|
||||||
|
|
||||||
limit = float('inf') if limit is None else int(limit)
|
limit = float('inf') if limit is None else int(limit)
|
||||||
if isinstance(entity, InputPeerChannel):
|
if isinstance(entity, InputPeerChannel):
|
||||||
total = (await self(GetFullChannelRequest(
|
total = (await self(GetFullChannelRequest(
|
||||||
|
@ -1200,7 +1231,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
return
|
return
|
||||||
|
|
||||||
seen = set()
|
seen = set()
|
||||||
if total > 10000 and aggressive:
|
if total > 10000 and aggressive and not filter:
|
||||||
requests = [GetParticipantsRequest(
|
requests = [GetParticipantsRequest(
|
||||||
channel=entity,
|
channel=entity,
|
||||||
filter=ChannelParticipantsSearch(search + chr(x)),
|
filter=ChannelParticipantsSearch(search + chr(x)),
|
||||||
|
@ -1211,7 +1242,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
else:
|
else:
|
||||||
requests = [GetParticipantsRequest(
|
requests = [GetParticipantsRequest(
|
||||||
channel=entity,
|
channel=entity,
|
||||||
filter=ChannelParticipantsSearch(search),
|
filter=filter or ChannelParticipantsSearch(search),
|
||||||
offset=0,
|
offset=0,
|
||||||
limit=200,
|
limit=200,
|
||||||
hash=0
|
hash=0
|
||||||
|
@ -1237,31 +1268,47 @@ class TelegramClient(TelegramBareClient):
|
||||||
if not participants.users:
|
if not participants.users:
|
||||||
requests.pop(i)
|
requests.pop(i)
|
||||||
else:
|
else:
|
||||||
requests[i].offset += len(participants.users)
|
requests[i].offset += len(participants.participants)
|
||||||
for user in participants.users:
|
users = {user.id: user for user in participants.users}
|
||||||
if user.id not in seen:
|
for participant in participants.participants:
|
||||||
seen.add(user.id)
|
user = users[participant.user_id]
|
||||||
yield user
|
if not filter_entity(user) or user.id in seen:
|
||||||
if len(seen) >= limit:
|
continue
|
||||||
return
|
|
||||||
|
seen.add(participant.user_id)
|
||||||
|
user = users[participant.user_id]
|
||||||
|
user.participant = participant
|
||||||
|
yield user
|
||||||
|
if len(seen) >= limit:
|
||||||
|
return
|
||||||
|
|
||||||
elif isinstance(entity, InputPeerChat):
|
elif isinstance(entity, InputPeerChat):
|
||||||
users = (await self(GetFullChatRequest(entity.chat_id))).users
|
# TODO We *could* apply the `filter` here ourselves
|
||||||
|
full = await self(GetFullChatRequest(entity.chat_id))
|
||||||
if _total_box:
|
if _total_box:
|
||||||
_total_box.x = len(users)
|
_total_box.x = len(full.full_chat.participants.participants)
|
||||||
|
|
||||||
have = 0
|
have = 0
|
||||||
for user in users:
|
users = {user.id: user for user in full.users}
|
||||||
|
for participant in full.full_chat.participants.participants:
|
||||||
|
user = users[participant.user_id]
|
||||||
|
if not filter_entity(user):
|
||||||
|
continue
|
||||||
have += 1
|
have += 1
|
||||||
if have > limit:
|
if have > limit:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
user = users[participant.user_id]
|
||||||
|
user.participant = participant
|
||||||
yield user
|
yield user
|
||||||
else:
|
else:
|
||||||
if _total_box:
|
if _total_box:
|
||||||
_total_box.x = 1
|
_total_box.x = 1
|
||||||
if limit != 0:
|
if limit != 0:
|
||||||
yield await self.get_entity(entity)
|
user = await self.get_entity(entity)
|
||||||
|
if filter_entity(user):
|
||||||
|
user.participant = None
|
||||||
|
yield user
|
||||||
|
|
||||||
async def get_participants(self, *args, **kwargs):
|
async def get_participants(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -1308,6 +1355,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
photo or similar) so that it can be resent without the need
|
photo or similar) so that it can be resent without the need
|
||||||
to download and re-upload it again.
|
to download and re-upload it again.
|
||||||
|
|
||||||
|
If a list or similar is provided, the files in it will be
|
||||||
|
sent as an album in the order in which they appear, sliced
|
||||||
|
in chunks of 10 if more than 10 are given.
|
||||||
|
|
||||||
caption (:obj:`str`, optional):
|
caption (:obj:`str`, optional):
|
||||||
Optional caption for the sent media message.
|
Optional caption for the sent media message.
|
||||||
|
|
||||||
|
@ -1353,23 +1404,33 @@ class TelegramClient(TelegramBareClient):
|
||||||
# First check if the user passed an iterable, in which case
|
# First check if the user passed an iterable, in which case
|
||||||
# we may want to send as an album if all are photo files.
|
# we may want to send as an album if all are photo files.
|
||||||
if utils.is_list_like(file):
|
if utils.is_list_like(file):
|
||||||
# Convert to tuple so we can iterate several times
|
# TODO Fix progress_callback
|
||||||
file = tuple(x for x in file)
|
images = []
|
||||||
if all(utils.is_image(x) for x in file):
|
documents = []
|
||||||
return await self._send_album(
|
for x in file:
|
||||||
entity, file, caption=caption,
|
if utils.is_image(x):
|
||||||
|
images.append(x)
|
||||||
|
else:
|
||||||
|
documents.append(x)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
while images:
|
||||||
|
result += await self._send_album(
|
||||||
|
entity, images[:10], caption=caption,
|
||||||
progress_callback=progress_callback, reply_to=reply_to,
|
progress_callback=progress_callback, reply_to=reply_to,
|
||||||
parse_mode=parse_mode
|
parse_mode=parse_mode
|
||||||
)
|
)
|
||||||
# Not all are images, so send all the files one by one
|
images = images[10:]
|
||||||
return [
|
|
||||||
|
result.extend(
|
||||||
await self.send_file(
|
await self.send_file(
|
||||||
entity, x, allow_cache=False,
|
entity, x, allow_cache=False,
|
||||||
caption=caption, force_document=force_document,
|
caption=caption, force_document=force_document,
|
||||||
progress_callback=progress_callback, reply_to=reply_to,
|
progress_callback=progress_callback, reply_to=reply_to,
|
||||||
attributes=attributes, thumb=thumb, **kwargs
|
attributes=attributes, thumb=thumb, **kwargs
|
||||||
) for x in file
|
) for x in documents
|
||||||
]
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
reply_to = self._get_message_id(reply_to)
|
reply_to = self._get_message_id(reply_to)
|
||||||
|
@ -1509,6 +1570,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
# we need to produce right now to send albums (uploadMedia), and
|
# we need to produce right now to send albums (uploadMedia), and
|
||||||
# cache only makes a difference for documents where the user may
|
# cache only makes a difference for documents where the user may
|
||||||
# want the attributes used on them to change.
|
# want the attributes used on them to change.
|
||||||
|
#
|
||||||
|
# In theory documents can be sent inside the albums but they appear
|
||||||
|
# as different messages (not inside the album), and the logic to set
|
||||||
|
# the attributes/avoid cache is already written in .send_file().
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
if not utils.is_list_like(caption):
|
if not utils.is_list_like(caption):
|
||||||
caption = (caption,)
|
caption = (caption,)
|
||||||
|
@ -2001,7 +2066,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
input_location (:obj:`InputFileLocation`):
|
input_location (:obj:`InputFileLocation`):
|
||||||
The file location from which the file will be downloaded.
|
The file location from which the file will be downloaded.
|
||||||
|
|
||||||
file (:obj:`str` | :obj:`file`, optional):
|
file (:obj:`str` | :obj:`file`):
|
||||||
The output file path, directory, or stream-like object.
|
The output file path, directory, or stream-like object.
|
||||||
If the path exists and is a file, it will be overwritten.
|
If the path exists and is a file, it will be overwritten.
|
||||||
|
|
||||||
|
@ -2168,22 +2233,46 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
self._event_builders.append((event, callback))
|
self._event_builders.append((event, callback))
|
||||||
|
|
||||||
def add_update_handler(self, handler):
|
def remove_event_handler(self, callback, event=None):
|
||||||
"""Adds an update handler (a function which takes a TLObject,
|
"""
|
||||||
an update, as its parameter) and listens for updates"""
|
Inverse operation of :meth:`add_event_handler`.
|
||||||
|
|
||||||
|
If no event is given, all events for this callback are removed.
|
||||||
|
Returns how many callbacks were removed.
|
||||||
|
"""
|
||||||
|
found = 0
|
||||||
|
if event and not isinstance(event, type):
|
||||||
|
event = type(event)
|
||||||
|
|
||||||
|
for i, ec in enumerate(self._event_builders):
|
||||||
|
ev, cb = ec
|
||||||
|
if cb == callback and (not event or isinstance(ev, event)):
|
||||||
|
del self._event_builders[i]
|
||||||
|
found += 1
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
def list_event_handlers(self):
|
||||||
|
"""
|
||||||
|
Lists all added event handlers, returning a list of pairs
|
||||||
|
consisting of (callback, event).
|
||||||
|
"""
|
||||||
|
return [(callback, event) for event, callback in self._event_builders]
|
||||||
|
|
||||||
|
async def add_update_handler(self, handler):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'add_update_handler is deprecated, use the @client.on syntax '
|
'add_update_handler is deprecated, use the @client.on syntax '
|
||||||
'or add_event_handler(callback, events.Raw) instead (see '
|
'or add_event_handler(callback, events.Raw) instead (see '
|
||||||
'https://telethon.rtfd.io/en/latest/extra/basic/working-'
|
'https://telethon.rtfd.io/en/latest/extra/basic/working-'
|
||||||
'with-updates.html)'
|
'with-updates.html)'
|
||||||
)
|
)
|
||||||
self.add_event_handler(handler, events.Raw)
|
return await self.add_event_handler(handler, events.Raw)
|
||||||
|
|
||||||
def remove_update_handler(self, handler):
|
def remove_update_handler(self, handler):
|
||||||
pass
|
return self.remove_event_handler(handler)
|
||||||
|
|
||||||
def list_update_handlers(self):
|
def list_update_handlers(self):
|
||||||
return []
|
return [callback for callback, _ in self.list_event_handlers()]
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -2293,7 +2382,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
return await self.get_me()
|
return await self.get_me()
|
||||||
result = await self(ResolveUsernameRequest(username))
|
result = await self(ResolveUsernameRequest(username))
|
||||||
for entity in itertools.chain(result.users, result.chats):
|
for entity in itertools.chain(result.users, result.chats):
|
||||||
if entity.username.lower() == username:
|
if getattr(entity, 'username', None) or ''\
|
||||||
|
.lower() == username:
|
||||||
return entity
|
return entity
|
||||||
try:
|
try:
|
||||||
# Nobody with this username, maybe it's an exact name/title
|
# Nobody with this username, maybe it's an exact name/title
|
||||||
|
@ -2350,16 +2440,18 @@ class TelegramClient(TelegramBareClient):
|
||||||
# Add the mark to the peers if the user passed a Peer (not an int),
|
# Add the mark to the peers if the user passed a Peer (not an int),
|
||||||
# or said ID is negative. If it's negative it's been marked already.
|
# or said ID is negative. If it's negative it's been marked already.
|
||||||
# Look in the dialogs with the hope to find it.
|
# Look in the dialogs with the hope to find it.
|
||||||
mark = not isinstance(peer, int) or peer < 0
|
if not self._called_get_dialogs:
|
||||||
target_id = utils.get_peer_id(peer)
|
self._called_get_dialogs = True
|
||||||
if mark:
|
mark = not isinstance(peer, int) or peer < 0
|
||||||
async for dialog in self.iter_dialogs():
|
target_id = utils.get_peer_id(peer)
|
||||||
if utils.get_peer_id(dialog.entity) == target_id:
|
if mark:
|
||||||
return utils.get_input_peer(dialog.entity)
|
async for dialog in self.get_dialogs(100):
|
||||||
else:
|
if utils.get_peer_id(dialog.entity) == target_id:
|
||||||
async for dialog in self.iter_dialogs():
|
return utils.get_input_peer(dialog.entity)
|
||||||
if dialog.entity.id == target_id:
|
else:
|
||||||
return utils.get_input_peer(dialog.entity)
|
async for dialog in self.get_dialogs(100):
|
||||||
|
if dialog.entity.id == target_id:
|
||||||
|
return utils.get_input_peer(dialog.entity)
|
||||||
|
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'Could not find the input entity corresponding to "{}". '
|
'Could not find the input entity corresponding to "{}". '
|
||||||
|
|
|
@ -4,10 +4,11 @@ to convert between an entity like an User, Chat, etc. into its Input version)
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import types
|
import types
|
||||||
from collections import UserList
|
from collections import UserList
|
||||||
from mimetypes import add_type, guess_extension
|
from mimetypes import guess_extension
|
||||||
|
|
||||||
from .tl.types import (
|
from .tl.types import (
|
||||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||||
|
@ -315,11 +316,13 @@ def get_input_media(media, is_photo=False):
|
||||||
|
|
||||||
|
|
||||||
def is_image(file):
|
def is_image(file):
|
||||||
"""Returns True if the file extension looks like an image file"""
|
"""
|
||||||
|
Returns True if the file extension looks like an image file to Telegram.
|
||||||
|
"""
|
||||||
if not isinstance(file, str):
|
if not isinstance(file, str):
|
||||||
return False
|
return False
|
||||||
mime = mimetypes.guess_type(file)[0] or ''
|
_, ext = os.path.splitext(file)
|
||||||
return mime.startswith('image/') and not mime.endswith('/webp')
|
return re.match(r'\.(png|jpe?g)', ext, re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def is_audio(file):
|
def is_audio(file):
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# Versions should comply with PEP440.
|
# Versions should comply with PEP440.
|
||||||
# This line is parsed in setup.py:
|
# This line is parsed in setup.py:
|
||||||
__version__ = '0.18'
|
__version__ = '0.18.1'
|
||||||
|
|
|
@ -254,7 +254,7 @@ class TLArg:
|
||||||
|
|
||||||
self.generic_definition = generic_definition
|
self.generic_definition = generic_definition
|
||||||
|
|
||||||
def type_hint(self):
|
def doc_type_hint(self):
|
||||||
result = {
|
result = {
|
||||||
'int': 'int',
|
'int': 'int',
|
||||||
'long': 'int',
|
'long': 'int',
|
||||||
|
@ -272,6 +272,27 @@ class TLArg:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def python_type_hint(self):
|
||||||
|
type = self.type
|
||||||
|
if '.' in type:
|
||||||
|
type = type.split('.')[1]
|
||||||
|
result = {
|
||||||
|
'int': 'int',
|
||||||
|
'long': 'int',
|
||||||
|
'int128': 'int',
|
||||||
|
'int256': 'int',
|
||||||
|
'string': 'str',
|
||||||
|
'date': 'Optional[datetime]', # None date = 0 timestamp
|
||||||
|
'bytes': 'bytes',
|
||||||
|
'true': 'bool',
|
||||||
|
}.get(type, "Type{}".format(type))
|
||||||
|
if self.is_vector:
|
||||||
|
result = 'List[{}]'.format(result)
|
||||||
|
if self.is_flag and type != 'date':
|
||||||
|
result = 'Optional[{}]'.format(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# Find the real type representation by updating it as required
|
# Find the real type representation by updating it as required
|
||||||
real_type = self.type
|
real_type = self.type
|
||||||
|
|
|
@ -138,6 +138,7 @@ class TLGenerator:
|
||||||
builder.writeln(
|
builder.writeln(
|
||||||
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
||||||
)
|
)
|
||||||
|
builder.writeln('from typing import Optional, List, Union, TYPE_CHECKING')
|
||||||
|
|
||||||
# Add the relative imports to the namespaces,
|
# Add the relative imports to the namespaces,
|
||||||
# unless we already are in a namespace.
|
# unless we already are in a namespace.
|
||||||
|
@ -154,13 +155,81 @@ class TLGenerator:
|
||||||
# Import struct for the .__bytes__(self) serialization
|
# Import struct for the .__bytes__(self) serialization
|
||||||
builder.writeln('import struct')
|
builder.writeln('import struct')
|
||||||
|
|
||||||
|
tlobjects.sort(key=lambda x: x.name)
|
||||||
|
|
||||||
|
type_names = set()
|
||||||
|
type_defs = []
|
||||||
|
|
||||||
|
# Find all the types in this file and generate type definitions
|
||||||
|
# based on the types. The type definitions are written to the
|
||||||
|
# file at the end.
|
||||||
|
for t in tlobjects:
|
||||||
|
if not t.is_function:
|
||||||
|
type_name = t.result
|
||||||
|
if '.' in type_name:
|
||||||
|
type_name = type_name[type_name.rindex('.'):]
|
||||||
|
if type_name in type_names:
|
||||||
|
continue
|
||||||
|
type_names.add(type_name)
|
||||||
|
constructors = type_constructors[type_name]
|
||||||
|
if not constructors:
|
||||||
|
pass
|
||||||
|
elif len(constructors) == 1:
|
||||||
|
type_defs.append('Type{} = {}'.format(
|
||||||
|
type_name, constructors[0].class_name()))
|
||||||
|
else:
|
||||||
|
type_defs.append('Type{} = Union[{}]'.format(
|
||||||
|
type_name, ','.join(c.class_name()
|
||||||
|
for c in constructors)))
|
||||||
|
|
||||||
|
imports = {}
|
||||||
|
primitives = ('int', 'long', 'int128', 'int256', 'string',
|
||||||
|
'date', 'bytes', 'true')
|
||||||
|
# Find all the types in other files that are used in this file
|
||||||
|
# and generate the information required to import those types.
|
||||||
|
for t in tlobjects:
|
||||||
|
for arg in t.args:
|
||||||
|
name = arg.type
|
||||||
|
if not name or name in primitives:
|
||||||
|
continue
|
||||||
|
|
||||||
|
import_space = '{}.tl.types'.format('.' * depth)
|
||||||
|
if '.' in name:
|
||||||
|
namespace = name.split('.')[0]
|
||||||
|
name = name.split('.')[1]
|
||||||
|
import_space += '.{}'.format(namespace)
|
||||||
|
|
||||||
|
if name not in type_names:
|
||||||
|
type_names.add(name)
|
||||||
|
if name == 'date':
|
||||||
|
imports['datetime'] = ['datetime']
|
||||||
|
continue
|
||||||
|
elif not import_space in imports:
|
||||||
|
imports[import_space] = set()
|
||||||
|
imports[import_space].add('Type{}'.format(name))
|
||||||
|
|
||||||
|
# Add imports required for type checking.
|
||||||
|
builder.writeln('if TYPE_CHECKING:')
|
||||||
|
for namespace, names in imports.items():
|
||||||
|
builder.writeln('from {} import {}'.format(
|
||||||
|
namespace, ', '.join(names)))
|
||||||
|
else:
|
||||||
|
builder.writeln('pass')
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
# Generate the class for every TLObject
|
# Generate the class for every TLObject
|
||||||
for t in sorted(tlobjects, key=lambda x: x.name):
|
for t in tlobjects:
|
||||||
TLGenerator._write_source_code(
|
TLGenerator._write_source_code(
|
||||||
t, builder, depth, type_constructors
|
t, builder, depth, type_constructors
|
||||||
)
|
)
|
||||||
builder.current_indent = 0
|
builder.current_indent = 0
|
||||||
|
|
||||||
|
# Write the type definitions generated earlier.
|
||||||
|
builder.writeln('')
|
||||||
|
for line in type_defs:
|
||||||
|
builder.writeln(line)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _write_source_code(tlobject, builder, depth, type_constructors):
|
def _write_source_code(tlobject, builder, depth, type_constructors):
|
||||||
"""Writes the source code corresponding to the given TLObject
|
"""Writes the source code corresponding to the given TLObject
|
||||||
|
@ -218,7 +287,7 @@ class TLGenerator:
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if not arg.flag_indicator:
|
if not arg.flag_indicator:
|
||||||
builder.writeln(':param {} {}:'.format(
|
builder.writeln(':param {} {}:'.format(
|
||||||
arg.type_hint(), arg.name
|
arg.doc_type_hint(), arg.name
|
||||||
))
|
))
|
||||||
builder.current_indent -= 1 # It will auto-indent (':')
|
builder.current_indent -= 1 # It will auto-indent (':')
|
||||||
|
|
||||||
|
@ -258,7 +327,8 @@ class TLGenerator:
|
||||||
|
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if not arg.can_be_inferred:
|
if not arg.can_be_inferred:
|
||||||
builder.writeln('self.{0} = {0}'.format(arg.name))
|
builder.writeln('self.{0} = {0} # type: {1}'.format(
|
||||||
|
arg.name, arg.python_type_hint()))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Currently the only argument that can be
|
# Currently the only argument that can be
|
||||||
|
|
Loading…
Reference in New Issue
Block a user