mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-03 11:40:11 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
1047e9c3d5
|
@ -1,4 +1,3 @@
|
|||
cryptg
|
||||
pysocks
|
||||
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
|
||||
``TelegramClient`` instead of the session name.
|
||||
|
||||
Currently, there are three 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.
|
||||
Telethon contains two implementations of the abstract ``Session`` class:
|
||||
|
||||
Using AlchemySession
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
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.
|
||||
* ``MemorySession``: stores session data in Python variables.
|
||||
* ``SQLiteSession``, (default): stores sessions in their own SQLite databases.
|
||||
|
||||
To get started, you need to create an ``AlchemySessionContainer`` which will
|
||||
contain that shared data. The simplest way to use ``AlchemySessionContainer``
|
||||
is to simply pass it the database URL:
|
||||
There are other community-maintained implementations available:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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.
|
||||
* `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.
|
||||
|
||||
Creating your own storage
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The easiest way to create your own implementation is to use ``MemorySession``
|
||||
as the base and check out how ``SQLiteSession`` or ``AlchemySession`` work.
|
||||
You can find the relevant Python files under the ``sessions`` directory.
|
||||
The easiest way to create your own storage implementation is to use ``MemorySession``
|
||||
as the base and check out how ``SQLiteSession`` or one of the community-maintained
|
||||
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
|
||||
--------------------------
|
||||
|
|
|
@ -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
|
||||
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
|
||||
****************
|
||||
|
||||
|
|
|
@ -8,6 +8,11 @@ TelegramClient
|
|||
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
|
||||
you will be using most of the time. For this reason, it's important
|
||||
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_
|
||||
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
|
||||
.. _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
|
||||
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::
|
||||
|
@ -114,12 +116,15 @@ for example:
|
|||
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):
|
||||
if 'roll' in event.raw_text:
|
||||
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))
|
||||
def admin_handler(event):
|
||||
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!
|
||||
|
||||
|
||||
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
|
||||
*******************************
|
||||
|
||||
|
@ -162,14 +181,8 @@ propagation of the update through your handlers to stop:
|
|||
pass
|
||||
|
||||
|
||||
Events module
|
||||
*************
|
||||
|
||||
.. automodule:: telethon.events
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Remember to check :ref:`telethon-events-package` if you're looking for
|
||||
the methods reference.
|
||||
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/types/update.html
|
||||
|
|
|
@ -14,6 +14,69 @@ it can take advantage of new goodies!
|
|||
.. 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)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
.. _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
|
||||
and method used.
|
||||
|
||||
If you still would like to opt out, simply set
|
||||
``client.session.report_errors = False`` to disable this feature, or
|
||||
pass ``report_errors=False`` as a named parameter when creating a
|
||||
``TelegramClient`` instance. However Daniil would really thank you if
|
||||
you helped him (and everyone) by keeping it on!
|
||||
If you still would like to opt out, you can disable this feature by setting
|
||||
``client.session.report_errors = False``. However Daniil would really thank
|
||||
you if you helped him (and everyone) by keeping it on!
|
||||
|
||||
Querying the API status
|
||||
***********************
|
||||
|
|
|
@ -38,9 +38,9 @@ Kotlin
|
|||
******
|
||||
|
||||
`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/>`__
|
||||
language for
|
||||
languages for
|
||||
`Android <https://developer.android.com/kotlin/index.html>`__) by
|
||||
`@badoualy <https://github.com/badoualy>`__, currently as a beta–
|
||||
yet working.
|
||||
|
|
|
@ -8,8 +8,7 @@ To run Telethon on a test server, use the following code:
|
|||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(None, api_id, api_hash)
|
||||
client.session.server_address = '149.154.167.40'
|
||||
client.connect()
|
||||
client.session.set_dc(dc_id, '149.154.167.40', 80)
|
||||
|
||||
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
|
||||
had previously connected to another data center).
|
||||
|
||||
Once you're connected, you'll likely need to ``.sign_up()``. Remember
|
||||
`anyone can access the phone you
|
||||
Note that port 443 might not work, so you can try with 80 instead.
|
||||
|
||||
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>`__,
|
||||
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
|
||||
|
||||
from random import randint
|
||||
|
||||
dc_id = '2' # Change this to the DC id of the test server you chose
|
||||
phone = '99966' + dc_id + str(randint(9999)).zfill(4)
|
||||
client.send_code_request(phone)
|
||||
client.sign_up(dc_id * 5, 'Some', 'Name')
|
||||
client = TelegramClient(None, api_id, api_hash)
|
||||
client.session.set_dc(2, '149.154.167.40', 80)
|
||||
client.start(phone='9996621234', code_callback=lambda: '22222')
|
||||
|
|
|
@ -15,7 +15,8 @@ or use the menu on the left. Remember to read the :ref:`changelog`
|
|||
when you upgrade!
|
||||
|
||||
.. 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?
|
||||
|
|
|
@ -42,14 +42,6 @@ telethon\.crypto\.factorization module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.crypto\.libssl module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: telethon.crypto.libssl
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.crypto\.rsa module
|
||||
----------------------------
|
||||
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
.. _telethon-events-package:
|
||||
|
||||
telethon\.events package
|
||||
========================
|
||||
|
||||
|
||||
.. automodule:: telethon.events
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
.. _telethon-package:
|
||||
|
||||
|
||||
telethon package
|
||||
================
|
||||
|
||||
|
||||
telethon\.helpers module
|
||||
------------------------
|
||||
telethon\.telegram\_client module
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: telethon.helpers
|
||||
.. automodule:: telethon.telegram_client
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
@ -18,10 +21,18 @@ telethon\.telegram\_bare\_client module
|
|||
:undoc-members:
|
||||
: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:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
@ -42,18 +53,10 @@ telethon\.update\_state module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.utils module
|
||||
----------------------
|
||||
telethon\.sessions module
|
||||
-------------------------
|
||||
|
||||
.. automodule:: telethon.utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
telethon\.session module
|
||||
------------------------
|
||||
|
||||
.. automodule:: telethon.session
|
||||
.. automodule:: telethon.sessions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pyaes
|
||||
rsa
|
||||
typing
|
||||
|
|
8
setup.py
8
setup.py
|
@ -13,7 +13,7 @@ Extra supported commands are:
|
|||
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from sys import argv
|
||||
from sys import argv, version_info
|
||||
import os
|
||||
import re
|
||||
|
||||
|
@ -154,10 +154,10 @@ def main():
|
|||
'telethon_generator/parser/tl_object.py',
|
||||
'telethon_generator/parser/tl_parser.py',
|
||||
]),
|
||||
install_requires=['pyaes', 'rsa'],
|
||||
install_requires=['pyaes', 'rsa',
|
||||
'typing' if version_info < (3, 5) else ""],
|
||||
extras_require={
|
||||
'cryptg': ['cryptg'],
|
||||
'sqlalchemy': ['sqlalchemy']
|
||||
'cryptg': ['cryptg']
|
||||
}
|
||||
)
|
||||
|
||||
|
|
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 itertools
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from .. import utils
|
||||
from ..errors import RPCError
|
||||
from ..extensions import markdown
|
||||
from ..tl import types, functions
|
||||
from ..tl import TLObject, types, functions
|
||||
|
||||
|
||||
async def _into_id_set(client, chats):
|
||||
|
@ -71,6 +72,7 @@ class _EventCommon(abc.ABC):
|
|||
"""Intermediate class with common things to all events"""
|
||||
|
||||
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
||||
self._entities = {}
|
||||
self._client = None
|
||||
self._chat_peer = chat_peer
|
||||
self._message_id = msg_id
|
||||
|
@ -86,12 +88,15 @@ class _EventCommon(abc.ABC):
|
|||
)
|
||||
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
|
||||
return the input entity whose ID is the given entity ID.
|
||||
|
||||
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:
|
||||
if isinstance(chat, types.InputPeerChannel):
|
||||
|
@ -103,14 +108,17 @@ class _EventCommon(abc.ABC):
|
|||
functions.messages.GetMessagesRequest([msg_id])
|
||||
)
|
||||
except RPCError:
|
||||
return
|
||||
return None, None
|
||||
|
||||
entity = {
|
||||
utils.get_peer_id(x): x for x in itertools.chain(
|
||||
getattr(result, 'chats', []),
|
||||
getattr(result, 'users', []))
|
||||
}.get(entity_id)
|
||||
if entity:
|
||||
return utils.get_input_peer(entity)
|
||||
return entity, utils.get_input_peer(entity)
|
||||
else:
|
||||
return None, None
|
||||
|
||||
@property
|
||||
async def input_chat(self):
|
||||
|
@ -134,7 +142,7 @@ class _EventCommon(abc.ABC):
|
|||
# TODO For channels, getDifference? Maybe looking
|
||||
# in the dialogs (which is already done) is enough.
|
||||
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,
|
||||
utils.get_peer_id(self._chat_peer)
|
||||
)
|
||||
|
@ -147,14 +155,32 @@ class _EventCommon(abc.ABC):
|
|||
async def chat(self):
|
||||
"""
|
||||
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
|
||||
the event occurred. This property will make an API call the first time
|
||||
to get the most up to date version of the chat, so use with care as
|
||||
there is no caching besides local caching yet.
|
||||
the event occurred. This property may make an API call the first time
|
||||
to get the most up to date version of the chat (mostly when the event
|
||||
doesn't belong to a channel), so keep that in mind.
|
||||
"""
|
||||
if self._chat is None and await self.input_chat:
|
||||
self._chat = await self._client.get_entity(await self._input_chat)
|
||||
if not 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
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -167,9 +193,19 @@ class Raw(_EventBuilder):
|
|||
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
|
||||
# for their inner Event classes. Inner ._client is
|
||||
# set later by the creator TelegramClient.
|
||||
@_name_inner_event
|
||||
class NewMessage(_EventBuilder):
|
||||
"""
|
||||
Represents a new message event builder.
|
||||
|
@ -247,6 +283,10 @@ class NewMessage(_EventBuilder):
|
|||
else:
|
||||
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
|
||||
if all(x is None for x in (self.incoming, self.outgoing, self.chats,
|
||||
self.pattern)):
|
||||
|
@ -299,8 +339,6 @@ class NewMessage(_EventBuilder):
|
|||
self.message = message
|
||||
self._text = None
|
||||
|
||||
self._input_chat = None
|
||||
self._chat = None
|
||||
self._input_sender = None
|
||||
self._sender = None
|
||||
|
||||
|
@ -384,7 +422,7 @@ class NewMessage(_EventBuilder):
|
|||
)
|
||||
except (ValueError, TypeError):
|
||||
# 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.from_id,
|
||||
chat=await self.input_chat
|
||||
|
@ -395,14 +433,22 @@ class NewMessage(_EventBuilder):
|
|||
@property
|
||||
async def sender(self):
|
||||
"""
|
||||
This (:obj:`User`) will make an API call the first time to get
|
||||
the most up to date version of the sender, so use with care as
|
||||
there is no caching besides local caching yet.
|
||||
This (:obj:`User`) may make an API call the first time to get
|
||||
the most up to date version of the sender (mostly when the event
|
||||
doesn't belong to a channel), so keep that in mind.
|
||||
|
||||
``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)
|
||||
|
||||
return self._sender
|
||||
|
||||
@property
|
||||
|
@ -556,6 +602,7 @@ class NewMessage(_EventBuilder):
|
|||
return self.message.out
|
||||
|
||||
|
||||
@_name_inner_event
|
||||
class ChatAction(_EventBuilder):
|
||||
"""
|
||||
Represents an action in a chat (such as user joined, left, or new pin).
|
||||
|
@ -621,6 +668,7 @@ class ChatAction(_EventBuilder):
|
|||
else:
|
||||
return
|
||||
|
||||
event._entities = update.entities
|
||||
return self._filter_event(event)
|
||||
|
||||
class Event(_EventCommon):
|
||||
|
@ -764,7 +812,13 @@ class ChatAction(_EventBuilder):
|
|||
The user who added ``users``, if applicable (``None`` otherwise).
|
||||
"""
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -773,7 +827,13 @@ class ChatAction(_EventBuilder):
|
|||
The user who kicked ``users``, if applicable (``None`` otherwise).
|
||||
"""
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -803,11 +863,24 @@ class ChatAction(_EventBuilder):
|
|||
Might be empty if the information can't be retrieved or there
|
||||
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:
|
||||
self._users = await self._client.get_entity(self._user_peers)
|
||||
missing = await self._client.get_entity(missing)
|
||||
except (TypeError, ValueError):
|
||||
self._users = []
|
||||
missing = []
|
||||
|
||||
self._user_peers = have + missing
|
||||
|
||||
return self._users
|
||||
|
||||
|
@ -828,6 +901,7 @@ class ChatAction(_EventBuilder):
|
|||
return self._input_users
|
||||
|
||||
|
||||
@_name_inner_event
|
||||
class UserUpdate(_EventBuilder):
|
||||
"""
|
||||
Represents an user update (gone online, offline, joined Telegram).
|
||||
|
@ -839,6 +913,7 @@ class UserUpdate(_EventBuilder):
|
|||
else:
|
||||
return
|
||||
|
||||
event._entities = update.entities
|
||||
return self._filter_event(event)
|
||||
|
||||
class Event(_EventCommon):
|
||||
|
@ -975,6 +1050,7 @@ class UserUpdate(_EventBuilder):
|
|||
return self.chat
|
||||
|
||||
|
||||
@_name_inner_event
|
||||
class MessageEdited(NewMessage):
|
||||
"""
|
||||
Event fired when a message has been edited.
|
||||
|
@ -986,9 +1062,11 @@ class MessageEdited(NewMessage):
|
|||
else:
|
||||
return
|
||||
|
||||
return self._filter_event(event)
|
||||
event._entities = update.entities
|
||||
return self._message_filter_event(event)
|
||||
|
||||
|
||||
@_name_inner_event
|
||||
class MessageDeleted(_EventBuilder):
|
||||
"""
|
||||
Event fired when one or more messages are deleted.
|
||||
|
@ -1007,6 +1085,7 @@ class MessageDeleted(_EventBuilder):
|
|||
else:
|
||||
return
|
||||
|
||||
event._entities = update.entities
|
||||
return self._filter_event(event)
|
||||
|
||||
class Event(_EventCommon):
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from .abstract import Session
|
||||
from .memory import MemorySession
|
||||
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,
|
||||
timeout=timedelta(seconds=5),
|
||||
loop=None,
|
||||
report_errors=True,
|
||||
device_model=None,
|
||||
system_version=None,
|
||||
app_version=None,
|
||||
|
@ -102,6 +103,7 @@ class TelegramBareClient:
|
|||
DEFAULT_PORT
|
||||
)
|
||||
|
||||
session.report_errors = report_errors
|
||||
self.session = session
|
||||
self.api_id = int(api_id)
|
||||
self.api_hash = api_hash
|
||||
|
|
|
@ -146,9 +146,13 @@ class TelegramClient(TelegramBareClient):
|
|||
instantly, as soon as they arrive. Can still be disabled
|
||||
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:
|
||||
Extra parameters will be forwarded to the ``Session`` file.
|
||||
Most relevant parameters are:
|
||||
Some extra parameters are required when establishing the first
|
||||
connection. These are are (along with their default values):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -157,7 +161,6 @@ class TelegramClient(TelegramBareClient):
|
|||
app_version = TelegramClient.__version__
|
||||
lang_code = 'en'
|
||||
system_lang_code = lang_code
|
||||
report_errors = True
|
||||
"""
|
||||
|
||||
# region Initialization
|
||||
|
@ -168,6 +171,7 @@ class TelegramClient(TelegramBareClient):
|
|||
proxy=None,
|
||||
timeout=timedelta(seconds=5),
|
||||
loop=None,
|
||||
report_errors=True,
|
||||
**kwargs):
|
||||
super().__init__(
|
||||
session, api_id, api_hash,
|
||||
|
@ -176,6 +180,7 @@ class TelegramClient(TelegramBareClient):
|
|||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
loop=loop,
|
||||
report_errors=report_errors,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
@ -190,6 +195,9 @@ class TelegramClient(TelegramBareClient):
|
|||
# Sometimes we need to know who we are, cache the self peer
|
||||
self._self_input_peer = None
|
||||
|
||||
# Don't call .get_dialogs() every time a .get_entity() fails
|
||||
self._called_get_dialogs = False
|
||||
|
||||
# endregion
|
||||
|
||||
# region Telegram requests functions
|
||||
|
@ -1156,9 +1164,10 @@ class TelegramClient(TelegramBareClient):
|
|||
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||
|
||||
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:
|
||||
entity (:obj:`entity`):
|
||||
|
@ -1170,6 +1179,12 @@ class TelegramClient(TelegramBareClient):
|
|||
search (:obj:`str`, optional):
|
||||
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):
|
||||
Aggressively looks for all participants in the chat in
|
||||
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.
|
||||
|
||||
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):
|
||||
A _Box instance to pass the total parameter by reference.
|
||||
|
||||
Returns:
|
||||
A list of participants with an additional .total variable on the
|
||||
list indicating the total amount of members in this group/channel.
|
||||
Yields:
|
||||
The ``User`` objects returned by ``GetParticipantsRequest``
|
||||
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)
|
||||
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)
|
||||
if isinstance(entity, InputPeerChannel):
|
||||
total = (await self(GetFullChannelRequest(
|
||||
|
@ -1200,7 +1231,7 @@ class TelegramClient(TelegramBareClient):
|
|||
return
|
||||
|
||||
seen = set()
|
||||
if total > 10000 and aggressive:
|
||||
if total > 10000 and aggressive and not filter:
|
||||
requests = [GetParticipantsRequest(
|
||||
channel=entity,
|
||||
filter=ChannelParticipantsSearch(search + chr(x)),
|
||||
|
@ -1211,7 +1242,7 @@ class TelegramClient(TelegramBareClient):
|
|||
else:
|
||||
requests = [GetParticipantsRequest(
|
||||
channel=entity,
|
||||
filter=ChannelParticipantsSearch(search),
|
||||
filter=filter or ChannelParticipantsSearch(search),
|
||||
offset=0,
|
||||
limit=200,
|
||||
hash=0
|
||||
|
@ -1237,31 +1268,47 @@ class TelegramClient(TelegramBareClient):
|
|||
if not participants.users:
|
||||
requests.pop(i)
|
||||
else:
|
||||
requests[i].offset += len(participants.users)
|
||||
for user in participants.users:
|
||||
if user.id not in seen:
|
||||
seen.add(user.id)
|
||||
yield user
|
||||
if len(seen) >= limit:
|
||||
return
|
||||
requests[i].offset += len(participants.participants)
|
||||
users = {user.id: user for user in participants.users}
|
||||
for participant in participants.participants:
|
||||
user = users[participant.user_id]
|
||||
if not filter_entity(user) or user.id in seen:
|
||||
continue
|
||||
|
||||
seen.add(participant.user_id)
|
||||
user = users[participant.user_id]
|
||||
user.participant = participant
|
||||
yield user
|
||||
if len(seen) >= limit:
|
||||
return
|
||||
|
||||
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:
|
||||
_total_box.x = len(users)
|
||||
_total_box.x = len(full.full_chat.participants.participants)
|
||||
|
||||
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
|
||||
if have > limit:
|
||||
break
|
||||
else:
|
||||
user = users[participant.user_id]
|
||||
user.participant = participant
|
||||
yield user
|
||||
else:
|
||||
if _total_box:
|
||||
_total_box.x = 1
|
||||
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):
|
||||
"""
|
||||
|
@ -1308,6 +1355,10 @@ class TelegramClient(TelegramBareClient):
|
|||
photo or similar) so that it can be resent without the need
|
||||
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):
|
||||
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
|
||||
# we may want to send as an album if all are photo files.
|
||||
if utils.is_list_like(file):
|
||||
# Convert to tuple so we can iterate several times
|
||||
file = tuple(x for x in file)
|
||||
if all(utils.is_image(x) for x in file):
|
||||
return await self._send_album(
|
||||
entity, file, caption=caption,
|
||||
# TODO Fix progress_callback
|
||||
images = []
|
||||
documents = []
|
||||
for x in file:
|
||||
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,
|
||||
parse_mode=parse_mode
|
||||
)
|
||||
# Not all are images, so send all the files one by one
|
||||
return [
|
||||
images = images[10:]
|
||||
|
||||
result.extend(
|
||||
await self.send_file(
|
||||
entity, x, allow_cache=False,
|
||||
caption=caption, force_document=force_document,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
attributes=attributes, thumb=thumb, **kwargs
|
||||
) for x in file
|
||||
]
|
||||
) for x in documents
|
||||
)
|
||||
return result
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
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
|
||||
# cache only makes a difference for documents where the user may
|
||||
# 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)
|
||||
if not utils.is_list_like(caption):
|
||||
caption = (caption,)
|
||||
|
@ -2001,7 +2066,7 @@ class TelegramClient(TelegramBareClient):
|
|||
input_location (:obj:`InputFileLocation`):
|
||||
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.
|
||||
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))
|
||||
|
||||
def add_update_handler(self, handler):
|
||||
"""Adds an update handler (a function which takes a TLObject,
|
||||
an update, as its parameter) and listens for updates"""
|
||||
def remove_event_handler(self, callback, event=None):
|
||||
"""
|
||||
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(
|
||||
'add_update_handler is deprecated, use the @client.on syntax '
|
||||
'or add_event_handler(callback, events.Raw) instead (see '
|
||||
'https://telethon.rtfd.io/en/latest/extra/basic/working-'
|
||||
'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):
|
||||
pass
|
||||
return self.remove_event_handler(handler)
|
||||
|
||||
def list_update_handlers(self):
|
||||
return []
|
||||
return [callback for callback, _ in self.list_event_handlers()]
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -2293,7 +2382,8 @@ class TelegramClient(TelegramBareClient):
|
|||
return await self.get_me()
|
||||
result = await self(ResolveUsernameRequest(username))
|
||||
for entity in itertools.chain(result.users, result.chats):
|
||||
if entity.username.lower() == username:
|
||||
if getattr(entity, 'username', None) or ''\
|
||||
.lower() == username:
|
||||
return entity
|
||||
try:
|
||||
# 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),
|
||||
# or said ID is negative. If it's negative it's been marked already.
|
||||
# Look in the dialogs with the hope to find it.
|
||||
mark = not isinstance(peer, int) or peer < 0
|
||||
target_id = utils.get_peer_id(peer)
|
||||
if mark:
|
||||
async for dialog in self.iter_dialogs():
|
||||
if utils.get_peer_id(dialog.entity) == target_id:
|
||||
return utils.get_input_peer(dialog.entity)
|
||||
else:
|
||||
async for dialog in self.iter_dialogs():
|
||||
if dialog.entity.id == target_id:
|
||||
return utils.get_input_peer(dialog.entity)
|
||||
if not self._called_get_dialogs:
|
||||
self._called_get_dialogs = True
|
||||
mark = not isinstance(peer, int) or peer < 0
|
||||
target_id = utils.get_peer_id(peer)
|
||||
if mark:
|
||||
async for dialog in self.get_dialogs(100):
|
||||
if utils.get_peer_id(dialog.entity) == target_id:
|
||||
return utils.get_input_peer(dialog.entity)
|
||||
else:
|
||||
async for dialog in self.get_dialogs(100):
|
||||
if dialog.entity.id == target_id:
|
||||
return utils.get_input_peer(dialog.entity)
|
||||
|
||||
raise TypeError(
|
||||
'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 mimetypes
|
||||
import os
|
||||
import re
|
||||
import types
|
||||
from collections import UserList
|
||||
from mimetypes import add_type, guess_extension
|
||||
from mimetypes import guess_extension
|
||||
|
||||
from .tl.types import (
|
||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||
|
@ -315,11 +316,13 @@ def get_input_media(media, is_photo=False):
|
|||
|
||||
|
||||
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):
|
||||
return False
|
||||
mime = mimetypes.guess_type(file)[0] or ''
|
||||
return mime.startswith('image/') and not mime.endswith('/webp')
|
||||
_, ext = os.path.splitext(file)
|
||||
return re.match(r'\.(png|jpe?g)', ext, re.IGNORECASE)
|
||||
|
||||
|
||||
def is_audio(file):
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# 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
|
||||
|
||||
def type_hint(self):
|
||||
def doc_type_hint(self):
|
||||
result = {
|
||||
'int': 'int',
|
||||
'long': 'int',
|
||||
|
@ -272,6 +272,27 @@ class TLArg:
|
|||
|
||||
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):
|
||||
# Find the real type representation by updating it as required
|
||||
real_type = self.type
|
||||
|
|
|
@ -138,6 +138,7 @@ class TLGenerator:
|
|||
builder.writeln(
|
||||
'from {}.tl.tlobject import TLObject'.format('.' * depth)
|
||||
)
|
||||
builder.writeln('from typing import Optional, List, Union, TYPE_CHECKING')
|
||||
|
||||
# Add the relative imports to the namespaces,
|
||||
# unless we already are in a namespace.
|
||||
|
@ -154,13 +155,81 @@ class TLGenerator:
|
|||
# Import struct for the .__bytes__(self) serialization
|
||||
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
|
||||
for t in sorted(tlobjects, key=lambda x: x.name):
|
||||
for t in tlobjects:
|
||||
TLGenerator._write_source_code(
|
||||
t, builder, depth, type_constructors
|
||||
)
|
||||
builder.current_indent = 0
|
||||
|
||||
# Write the type definitions generated earlier.
|
||||
builder.writeln('')
|
||||
for line in type_defs:
|
||||
builder.writeln(line)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _write_source_code(tlobject, builder, depth, type_constructors):
|
||||
"""Writes the source code corresponding to the given TLObject
|
||||
|
@ -218,7 +287,7 @@ class TLGenerator:
|
|||
for arg in args:
|
||||
if not arg.flag_indicator:
|
||||
builder.writeln(':param {} {}:'.format(
|
||||
arg.type_hint(), arg.name
|
||||
arg.doc_type_hint(), arg.name
|
||||
))
|
||||
builder.current_indent -= 1 # It will auto-indent (':')
|
||||
|
||||
|
@ -258,7 +327,8 @@ class TLGenerator:
|
|||
|
||||
for arg in args:
|
||||
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
|
||||
|
||||
# Currently the only argument that can be
|
||||
|
|
Loading…
Reference in New Issue
Block a user