Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-03-18 10:23:48 +01:00
commit 1047e9c3d5
25 changed files with 664 additions and 448 deletions

View File

@ -1,4 +1,3 @@
cryptg cryptg
pysocks pysocks
hachoir3 hachoir3
sqlalchemy

View File

@ -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
-------------------------- --------------------------

View File

@ -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
**************** ****************

View File

@ -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:

View File

@ -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

View File

@ -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)
========================= =========================

View File

@ -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
*********************** ***********************

View File

@ -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.

View File

@ -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')

View File

@ -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?

View File

@ -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
---------------------------- ----------------------------

View File

@ -1,4 +1,9 @@
.. _telethon-events-package:
telethon\.events package telethon\.events package
======================== ========================
.. automodule:: telethon.events
:members:
:undoc-members:
:show-inheritance:

View File

@ -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:

View File

@ -1,2 +1,3 @@
pyaes pyaes
rsa rsa
typing

View File

@ -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
View 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)

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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 "{}". '

View File

@ -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):

View 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'

View File

@ -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

View File

@ -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