Merge branch 'master' of https://github.com/LonamiWebs/Telethon into stop-propagation-of-updates

This commit is contained in:
JosXa 2018-02-27 03:41:09 +01:00
commit ebcfd1ed99
10 changed files with 306 additions and 88 deletions

View File

@ -1,2 +1,3 @@
cryptg cryptg
pysocks
hachoir3 hachoir3

View File

@ -14,6 +14,45 @@ it can take advantage of new goodies!
.. contents:: List of All Versions .. contents:: List of All Versions
Further easing library usage (v0.17.4)
======================================
*Published at 2018/02/24*
Some new things and patches that already deserved their own release.
Additions
~~~~~~~~~
- New ``pattern`` argument to ``NewMessage`` to easily filter messages.
- New ``.get_participants()`` convenience method to get members from chats.
- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter.
- You can now ``.get_entity()`` through exact name match instead username.
- Raise ``ProxyConnectionError`` instead looping forever so you can
``except`` it on your own code and behave accordingly.
Bug fixes
~~~~~~~~~
- ``.parse_username`` would fail with ``www.`` or a trailing slash.
- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``.
- You can now send ``b'byte strings'`` directly as files again.
- ``.send_file()`` was not respecting the original captions when passing
another message (or media) as the file.
- Downloading media from a different data center would always log a warning
for the first time.
Internal changes
~~~~~~~~~~~~~~~~
- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``.
- You can use ``.get_me(input_peer=True)`` if all you need is your self ID.
- New addition to the interactive client example to show peer information.
- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so
you can always safely rely on ``.sender`` to get the right ID.
New small convenience functions (v0.17.3) New small convenience functions (v0.17.3)
========================================= =========================================

View File

@ -110,7 +110,7 @@ def main():
long_description = f.read() long_description = f.read()
with open('telethon/version.py', encoding='utf-8') as f: with open('telethon/version.py', encoding='utf-8') as f:
version = re.search(r"^__version__\s+=\s+'(.*)'$", version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
f.read(), flags=re.MULTILINE).group(1) f.read(), flags=re.MULTILINE).group(1)
setup( setup(
name='Telethon', name='Telethon',

View File

@ -1,6 +1,7 @@
import abc import abc
import datetime import datetime
import itertools import itertools
import re
from .. import utils from .. import utils
from ..errors import RPCError from ..errors import RPCError
@ -13,15 +14,14 @@ def _into_id_set(client, chats):
if chats is None: if chats is None:
return None return None
if not hasattr(chats, '__iter__') or isinstance(chats, str): if not utils.is_list_like(chats):
chats = (chats,) chats = (chats,)
result = set() result = set()
for chat in chats: for chat in chats:
chat = client.get_input_entity(chat) chat = client.get_input_entity(chat)
if isinstance(chat, types.InputPeerSelf): if isinstance(chat, types.InputPeerSelf):
chat = getattr(_into_id_set, 'me', None) or client.get_me() chat = client.get_me(input_peer=True)
_into_id_set.me = chat
result.add(utils.get_peer_id(chat)) result.add(utils.get_peer_id(chat))
return result return result
@ -42,6 +42,7 @@ class _EventBuilder(abc.ABC):
def __init__(self, chats=None, blacklist_chats=False): def __init__(self, chats=None, blacklist_chats=False):
self.chats = chats self.chats = chats
self.blacklist_chats = blacklist_chats self.blacklist_chats = blacklist_chats
self._self_id = None
@abc.abstractmethod @abc.abstractmethod
def build(self, update): def build(self, update):
@ -50,6 +51,7 @@ class _EventBuilder(abc.ABC):
def resolve(self, client): def resolve(self, client):
"""Helper method to allow event builders to be resolved before usage""" """Helper method to allow event builders to be resolved before usage"""
self.chats = _into_id_set(client, self.chats) self.chats = _into_id_set(client, self.chats)
self._self_id = client.get_me(input_peer=True).user_id
def _filter_event(self, event): def _filter_event(self, event):
""" """
@ -153,6 +155,9 @@ class Raw(_EventBuilder):
""" """
Represents a raw event. The event is the update itself. Represents a raw event. The event is the update itself.
""" """
def resolve(self, client):
pass
def build(self, update): def build(self, update):
return update return update
@ -173,19 +178,28 @@ class NewMessage(_EventBuilder):
If set to ``True``, only **outgoing** messages will be handled. If set to ``True``, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either). Mutually exclusive with ``incoming`` (can only set one of either).
Notes: pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional):
The ``message.from_id`` might not only be an integer or ``None``, If set, only messages matching this pattern will be handled.
but also ``InputPeerSelf()`` for short private messages (the API You can specify a regex-like string which will be matched
would not return such thing, this is a custom modification). against the message, a callable function that returns ``True``
if a message is acceptable, or a compiled regex pattern.
""" """
def __init__(self, incoming=None, outgoing=None, def __init__(self, incoming=None, outgoing=None,
chats=None, blacklist_chats=False): chats=None, blacklist_chats=False, pattern=None):
if incoming and outgoing: if incoming and outgoing:
raise ValueError('Can only set either incoming or outgoing') raise ValueError('Can only set either incoming or outgoing')
super().__init__(chats=chats, blacklist_chats=blacklist_chats) super().__init__(chats=chats, blacklist_chats=blacklist_chats)
self.incoming = incoming self.incoming = incoming
self.outgoing = outgoing self.outgoing = outgoing
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
def build(self, update): def build(self, update):
if isinstance(update, if isinstance(update,
@ -201,7 +215,7 @@ class NewMessage(_EventBuilder):
silent=update.silent, silent=update.silent,
id=update.id, id=update.id,
to_id=types.PeerUser(update.user_id), to_id=types.PeerUser(update.user_id),
from_id=types.InputPeerSelf() if update.out else update.user_id, from_id=self._self_id if update.out else update.user_id,
message=update.message, message=update.message,
date=update.date, date=update.date,
fwd_from=update.fwd_from, fwd_from=update.fwd_from,
@ -229,13 +243,16 @@ class NewMessage(_EventBuilder):
return return
# 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)):
return event return event
if self.incoming and event.message.out: if self.incoming and event.message.out:
return return
if self.outgoing and not event.message.out: if self.outgoing and not event.message.out:
return return
if self.pattern and not self.pattern(event.message.message or ''):
return
return self._filter_event(event) return self._filter_event(event)
@ -260,7 +277,14 @@ class NewMessage(_EventBuilder):
Whether the message is a reply to some other or not. Whether the message is a reply to some other or not.
""" """
def __init__(self, message): def __init__(self, message):
super().__init__(chat_peer=message.to_id, if not message.out and isinstance(message.to_id, types.PeerUser):
# Incoming message (e.g. from a bot) has to_id=us, and
# from_id=bot (the actual "chat" from an user's perspective).
chat_peer = types.PeerUser(message.from_id)
else:
chat_peer = message.to_id
super().__init__(chat_peer=chat_peer,
msg_id=message.id, broadcast=bool(message.post)) msg_id=message.id, broadcast=bool(message.post))
self.message = message self.message = message
@ -299,6 +323,10 @@ class NewMessage(_EventBuilder):
or the edited message otherwise. or the edited message otherwise.
""" """
if not self.message.out: if not self.message.out:
if not isinstance(self.message.to_id, types.PeerUser):
return None
me = self._client.get_me(input_peer=True)
if self.message.to_id.user_id != me.user_id:
return None return None
return self._client.edit_message(self.input_chat, return self._client.edit_message(self.input_chat,

View File

@ -2,18 +2,26 @@
This module holds a rough implementation of the C# TCP client. This module holds a rough implementation of the C# TCP client.
""" """
import errno import errno
import logging
import socket import socket
import time import time
from datetime import timedelta from datetime import timedelta
from io import BytesIO, BufferedWriter from io import BytesIO, BufferedWriter
from threading import Lock from threading import Lock
try:
import socks
except ImportError:
socks = None
MAX_TIMEOUT = 15 # in seconds MAX_TIMEOUT = 15 # in seconds
CONN_RESET_ERRNOS = { CONN_RESET_ERRNOS = {
errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH,
errno.EINVAL, errno.ENOTCONN errno.EINVAL, errno.ENOTCONN
} }
__log__ = logging.getLogger(__name__)
class TcpClient: class TcpClient:
"""A simple TCP client to ease the work with sockets and proxies.""" """A simple TCP client to ease the work with sockets and proxies."""
@ -70,6 +78,10 @@ class TcpClient:
self._socket.connect(address) self._socket.connect(address)
break # Successful connection, stop retrying to connect break # Successful connection, stop retrying to connect
except OSError as e: except OSError as e:
__log__.info('OSError "%s" raised while connecting', e)
# Stop retrying to connect if proxy connection error occurred
if socks and isinstance(e, socks.ProxyConnectionError):
raise
# There are some errors that we know how to handle, and # There are some errors that we know how to handle, and
# the loop will allow us to retry # the loop will allow us to retry
if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL,
@ -112,19 +124,22 @@ class TcpClient:
:param data: the data to send. :param data: the data to send.
""" """
if self._socket is None: if self._socket is None:
self._raise_connection_reset() self._raise_connection_reset(None)
# TODO Timeout may be an issue when sending the data, Changed in v3.5: # TODO Timeout may be an issue when sending the data, Changed in v3.5:
# The socket timeout is now the maximum total duration to send all data. # The socket timeout is now the maximum total duration to send all data.
try: try:
self._socket.sendall(data) self._socket.sendall(data)
except socket.timeout as e: except socket.timeout as e:
__log__.debug('socket.timeout "%s" while writing data', e)
raise TimeoutError() from e raise TimeoutError() from e
except ConnectionError: except ConnectionError as e:
self._raise_connection_reset() __log__.info('ConnectionError "%s" while writing data', e)
self._raise_connection_reset(e)
except OSError as e: except OSError as e:
__log__.info('OSError "%s" while writing data', e)
if e.errno in CONN_RESET_ERRNOS: if e.errno in CONN_RESET_ERRNOS:
self._raise_connection_reset() self._raise_connection_reset(e)
else: else:
raise raise
@ -136,7 +151,7 @@ class TcpClient:
:return: the read data with len(data) == size. :return: the read data with len(data) == size.
""" """
if self._socket is None: if self._socket is None:
self._raise_connection_reset() self._raise_connection_reset(None)
# TODO Remove the timeout from this method, always use previous one # TODO Remove the timeout from this method, always use previous one
with BufferedWriter(BytesIO(), buffer_size=size) as buffer: with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
@ -145,17 +160,22 @@ class TcpClient:
try: try:
partial = self._socket.recv(bytes_left) partial = self._socket.recv(bytes_left)
except socket.timeout as e: except socket.timeout as e:
# These are somewhat common if the server has nothing
# to send to us, so use a lower logging priority.
__log__.debug('socket.timeout "%s" while reading data', e)
raise TimeoutError() from e raise TimeoutError() from e
except ConnectionError: except ConnectionError as e:
self._raise_connection_reset() __log__.info('ConnectionError "%s" while reading data', e)
self._raise_connection_reset(e)
except OSError as e: except OSError as e:
__log__.info('OSError "%s" while reading data', e)
if e.errno in CONN_RESET_ERRNOS: if e.errno in CONN_RESET_ERRNOS:
self._raise_connection_reset() self._raise_connection_reset(e)
else: else:
raise raise
if len(partial) == 0: if len(partial) == 0:
self._raise_connection_reset() self._raise_connection_reset(None)
buffer.write(partial) buffer.write(partial)
bytes_left -= len(partial) bytes_left -= len(partial)
@ -164,7 +184,8 @@ class TcpClient:
buffer.flush() buffer.flush()
return buffer.raw.getvalue() return buffer.raw.getvalue()
def _raise_connection_reset(self): def _raise_connection_reset(self, original):
"""Disconnects the client and raises ConnectionResetError.""" """Disconnects the client and raises ConnectionResetError."""
self.close() # Connection reset -> flag as socket closed self.close() # Connection reset -> flag as socket closed
raise ConnectionResetError('The server has closed the connection.') raise ConnectionResetError('The server has closed the connection.')\
from original

View File

@ -227,7 +227,7 @@ class Session:
c = self._cursor() c = self._cursor()
c.execute('select auth_key from sessions') c.execute('select auth_key from sessions')
tuple_ = c.fetchone() tuple_ = c.fetchone()
if tuple_: if tuple_ and tuple_[0]:
self._auth_key = AuthKey(data=tuple_[0]) self._auth_key = AuthKey(data=tuple_[0])
else: else:
self._auth_key = None self._auth_key = None
@ -355,14 +355,14 @@ class Session:
if not self.save_entities: if not self.save_entities:
return return
if not isinstance(tlo, TLObject) and hasattr(tlo, '__iter__'): if not isinstance(tlo, TLObject) and utils.is_list_like(tlo):
# This may be a list of users already for instance # This may be a list of users already for instance
entities = tlo entities = tlo
else: else:
entities = [] entities = []
if hasattr(tlo, 'chats') and hasattr(tlo.chats, '__iter__'): if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats):
entities.extend(tlo.chats) entities.extend(tlo.chats)
if hasattr(tlo, 'users') and hasattr(tlo.users, '__iter__'): if hasattr(tlo, 'users') and utils.is_list_like(tlo.users):
entities.extend(tlo.users) entities.extend(tlo.users)
if not entities: if not entities:
return return

View File

@ -56,7 +56,7 @@ from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, SendMediaRequest, GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest,
UploadMediaRequest, EditMessageRequest UploadMediaRequest, EditMessageRequest, GetFullChatRequest
) )
from .tl.functions import channels from .tl.functions import channels
@ -66,7 +66,7 @@ from .tl.functions.users import (
GetUsersRequest GetUsersRequest
) )
from .tl.functions.channels import ( from .tl.functions.channels import (
GetChannelsRequest, GetFullChannelRequest GetChannelsRequest, GetFullChannelRequest, GetParticipantsRequest
) )
from .tl.types import ( from .tl.types import (
DocumentAttributeAudio, DocumentAttributeFilename, DocumentAttributeAudio, DocumentAttributeFilename,
@ -80,7 +80,8 @@ from .tl.types import (
InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig,
InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl,
InputMessageEntityMentionName, DocumentAttributeVideo, InputMessageEntityMentionName, DocumentAttributeVideo,
UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates,
MessageMediaWebPage, ChannelParticipantsSearch
) )
from .tl.types.messages import DialogsSlice from .tl.types.messages import DialogsSlice
from .extensions import markdown, html from .extensions import markdown, html
@ -179,6 +180,9 @@ class TelegramClient(TelegramBareClient):
self._phone_code_hash = {} self._phone_code_hash = {}
self._phone = None self._phone = None
# Sometimes we need to know who we are, cache the self peer
self._self_input_peer = None
# endregion # endregion
# region Telegram requests functions # region Telegram requests functions
@ -407,6 +411,9 @@ class TelegramClient(TelegramBareClient):
'and a password only if an RPCError was raised before.' 'and a password only if an RPCError was raised before.'
) )
self._self_input_peer = utils.get_input_peer(
result.user, allow_self=False
)
self._set_connected_and_authorized() self._set_connected_and_authorized()
return result.user return result.user
@ -436,6 +443,9 @@ class TelegramClient(TelegramBareClient):
last_name=last_name last_name=last_name
)) ))
self._self_input_peer = utils.get_input_peer(
result.user, allow_self=False
)
self._set_connected_and_authorized() self._set_connected_and_authorized()
return result.user return result.user
@ -455,16 +465,31 @@ class TelegramClient(TelegramBareClient):
self.session.delete() self.session.delete()
return True return True
def get_me(self): def get_me(self, input_peer=False):
""" """
Gets "me" (the self user) which is currently authenticated, Gets "me" (the self user) which is currently authenticated,
or None if the request fails (hence, not authenticated). or None if the request fails (hence, not authenticated).
Args:
input_peer (:obj:`bool`, optional):
Whether to return the ``InputPeerUser`` version or the normal
``User``. This can be useful if you just need to know the ID
of yourself.
Returns: Returns:
:obj:`User`: Your own user. :obj:`User`: Your own user.
""" """
if input_peer and self._self_input_peer:
return self._self_input_peer
try: try:
return self(GetUsersRequest([InputUserSelf()]))[0] me = self(GetUsersRequest([InputUserSelf()]))[0]
if not self._self_input_peer:
self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
return self._self_input_peer if input_peer else me
except UnauthorizedError: except UnauthorizedError:
return None return None
@ -641,8 +666,8 @@ class TelegramClient(TelegramBareClient):
entity (:obj:`entity`): entity (:obj:`entity`):
To who will it be sent. To who will it be sent.
message (:obj:`str`): message (:obj:`str` | :obj:`Message`):
The message to be sent. The message to be sent, or another message object to resend.
reply_to (:obj:`int` | :obj:`Message`, optional): reply_to (:obj:`int` | :obj:`Message`, optional):
Whether to reply to a message or not. If an integer is provided, Whether to reply to a message or not. If an integer is provided,
@ -661,15 +686,35 @@ class TelegramClient(TelegramBareClient):
the sent message the sent message
""" """
entity = self.get_input_entity(entity) entity = self.get_input_entity(entity)
message, msg_entities = self._parse_message_text(message, parse_mode) if isinstance(message, Message):
if (message.media
and not isinstance(message.media, MessageMediaWebPage)):
return self.send_file(entity, message.media)
if utils.get_peer_id(entity) == utils.get_peer_id(message.to_id):
reply_id = message.reply_to_msg_id
else:
reply_id = None
request = SendMessageRequest(
peer=entity,
message=message.message or '',
silent=message.silent,
reply_to_msg_id=reply_id,
reply_markup=message.reply_markup,
entities=message.entities,
no_webpage=not isinstance(message.media, MessageMediaWebPage)
)
message = message.message
else:
message, msg_ent = self._parse_message_text(message, parse_mode)
request = SendMessageRequest( request = SendMessageRequest(
peer=entity, peer=entity,
message=message, message=message,
entities=msg_entities, entities=msg_ent,
no_webpage=not link_preview, no_webpage=not link_preview,
reply_to_msg_id=self._get_message_id(reply_to) reply_to_msg_id=self._get_message_id(reply_to)
) )
result = self(request) result = self(request)
if isinstance(result, UpdateShortSentMessage): if isinstance(result, UpdateShortSentMessage):
return Message( return Message(
@ -930,7 +975,7 @@ class TelegramClient(TelegramBareClient):
""" """
if max_id is None: if max_id is None:
if message: if message:
if hasattr(message, '__iter__'): if utils.is_list_like(message):
max_id = max(msg.id for msg in message) max_id = max(msg.id for msg in message)
else: else:
max_id = message.id max_id = message.id
@ -970,11 +1015,64 @@ class TelegramClient(TelegramBareClient):
raise TypeError('Invalid message type: {}'.format(type(message))) raise TypeError('Invalid message type: {}'.format(type(message)))
def get_participants(self, entity, limit=None, search=''):
"""
Gets the list of participants from the specified entity
Args:
entity (:obj:`entity`):
The entity from which to retrieve the participants list.
limit (:obj: `int`):
Limits amount of participants fetched.
search (:obj: `str`, optional):
Look for participants with this string in name/username.
Returns:
A list of participants with an additional .total variable on the list
indicating the total amount of members in this group/channel.
"""
entity = self.get_input_entity(entity)
limit = float('inf') if limit is None else int(limit)
if isinstance(entity, InputPeerChannel):
offset = 0
all_participants = {}
search = ChannelParticipantsSearch(search)
while True:
loop_limit = min(limit - offset, 200)
participants = self(GetParticipantsRequest(
entity, search, offset, loop_limit, hash=0
))
if not participants.users:
break
for user in participants.users:
if len(all_participants) < limit:
all_participants[user.id] = user
offset += len(participants.users)
if offset > limit:
break
users = UserList(all_participants.values())
users.total = self(GetFullChannelRequest(
entity)).full_chat.participants_count
elif isinstance(entity, InputPeerChat):
users = self(GetFullChatRequest(entity.chat_id)).users
if len(users) > limit:
users = users[:limit]
users = UserList(users)
users.total = len(users)
else:
users = UserList([entity])
users.total = 1
return users
# endregion # endregion
# region Uploading files # region Uploading files
def send_file(self, entity, file, caption='', def send_file(self, entity, file, caption=None,
force_document=False, progress_callback=None, force_document=False, progress_callback=None,
reply_to=None, reply_to=None,
attributes=None, attributes=None,
@ -1042,7 +1140,7 @@ 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 hasattr(file, '__iter__') and not isinstance(file, (str, bytes)): if utils.is_list_like(file):
# Convert to tuple so we can iterate several times # Convert to tuple so we can iterate several times
file = tuple(x for x in file) file = tuple(x for x in file)
if all(utils.is_image(x) for x in file): if all(utils.is_image(x) for x in file):
@ -1086,11 +1184,11 @@ class TelegramClient(TelegramBareClient):
if isinstance(file_handle, use_cache): if isinstance(file_handle, use_cache):
# File was cached, so an instance of use_cache was returned # File was cached, so an instance of use_cache was returned
if as_image: if as_image:
media = InputMediaPhoto(file_handle, caption) media = InputMediaPhoto(file_handle, caption or '')
else: else:
media = InputMediaDocument(file_handle, caption) media = InputMediaDocument(file_handle, caption or '')
elif as_image: elif as_image:
media = InputMediaUploadedPhoto(file_handle, caption) media = InputMediaUploadedPhoto(file_handle, caption or '')
else: else:
mime_type = None mime_type = None
if isinstance(file, str): if isinstance(file, str):
@ -1128,8 +1226,9 @@ class TelegramClient(TelegramBareClient):
attr_dict[DocumentAttributeVideo] = doc attr_dict[DocumentAttributeVideo] = doc
else: else:
attr_dict = { attr_dict = {
DocumentAttributeFilename: DocumentAttributeFilename: DocumentAttributeFilename(
DocumentAttributeFilename('unnamed') os.path.basename(
getattr(file, 'name', None) or 'unnamed'))
} }
if 'is_voice_note' in kwargs: if 'is_voice_note' in kwargs:
@ -1160,7 +1259,7 @@ class TelegramClient(TelegramBareClient):
file=file_handle, file=file_handle,
mime_type=mime_type, mime_type=mime_type,
attributes=list(attr_dict.values()), attributes=list(attr_dict.values()),
caption=caption, caption=caption or '',
**input_kw **input_kw
) )
@ -1180,15 +1279,11 @@ class TelegramClient(TelegramBareClient):
return msg return msg
def send_voice_note(self, entity, file, caption='', progress_callback=None, def send_voice_note(self, *args, **kwargs):
reply_to=None): """Wrapper method around .send_file() with is_voice_note=True"""
"""Wrapper method around .send_file() with is_voice_note=()""" return self.send_file(*args, **kwargs, is_voice_note=True)
return self.send_file(entity, file, caption,
progress_callback=progress_callback,
reply_to=reply_to,
is_voice_note=()) # empty tuple is enough
def _send_album(self, entity, files, caption='', def _send_album(self, entity, files, caption=None,
progress_callback=None, reply_to=None): progress_callback=None, reply_to=None):
"""Specialized version of .send_file for albums""" """Specialized version of .send_file for albums"""
# We don't care if the user wants to avoid cache, we will use it # We don't care if the user wants to avoid cache, we will use it
@ -1197,6 +1292,7 @@ class TelegramClient(TelegramBareClient):
# 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. Caption's ignored. # want the attributes used on them to change. Caption's ignored.
entity = self.get_input_entity(entity) entity = self.get_input_entity(entity)
caption = caption or ''
reply_to = self._get_message_id(reply_to) reply_to = self._get_message_id(reply_to)
# Need to upload the media first, but only if they're not cached yet # Need to upload the media first, but only if they're not cached yet
@ -1524,18 +1620,27 @@ class TelegramClient(TelegramBareClient):
file_size = document.size file_size = document.size
kind = 'document'
possible_names = [] possible_names = []
for attr in document.attributes: for attr in document.attributes:
if isinstance(attr, DocumentAttributeFilename): if isinstance(attr, DocumentAttributeFilename):
possible_names.insert(0, attr.file_name) possible_names.insert(0, attr.file_name)
elif isinstance(attr, DocumentAttributeAudio): elif isinstance(attr, DocumentAttributeAudio):
kind = 'audio'
if attr.performer and attr.title:
possible_names.append('{} - {}'.format( possible_names.append('{} - {}'.format(
attr.performer, attr.title attr.performer, attr.title
)) ))
elif attr.performer:
possible_names.append(attr.performer)
elif attr.title:
possible_names.append(attr.title)
elif attr.voice:
kind = 'voice'
file = self._get_proper_filename( file = self._get_proper_filename(
file, 'document', utils.get_extension(document), file, kind, utils.get_extension(document),
date=date, possible_names=possible_names date=date, possible_names=possible_names
) )
@ -1787,7 +1892,7 @@ class TelegramClient(TelegramBareClient):
callback.__name__, type(update).__name__)) callback.__name__, type(update).__name__))
break break
def add_event_handler(self, callback, event): def add_event_handler(self, callback, event=None):
""" """
Registers the given callback to be called on the specified event. Registers the given callback to be called on the specified event.
@ -1795,9 +1900,12 @@ class TelegramClient(TelegramBareClient):
callback (:obj:`callable`): callback (:obj:`callable`):
The callable function accepting one parameter to be used. The callable function accepting one parameter to be used.
event (:obj:`_EventBuilder` | :obj:`type`): event (:obj:`_EventBuilder` | :obj:`type`, optional):
The event builder class or instance to be used, The event builder class or instance to be used,
for instance ``events.NewMessage``. for instance ``events.NewMessage``.
If left unspecified, ``events.Raw`` (the ``Update`` objects
with no further processing) will be passed instead.
""" """
if self.updates.workers is None: if self.updates.workers is None:
warnings.warn( warnings.warn(
@ -1809,6 +1917,8 @@ class TelegramClient(TelegramBareClient):
self.updates.handler = self._on_handler self.updates.handler = self._on_handler
if isinstance(event, type): if isinstance(event, type):
event = event() event = event()
elif not event:
event = events.Raw()
event.resolve(self) event.resolve(self)
self._event_builders.append((event, callback)) self._event_builders.append((event, callback))
@ -1857,7 +1967,7 @@ class TelegramClient(TelegramBareClient):
``User``, ``Chat`` or ``Channel`` corresponding to the input ``User``, ``Chat`` or ``Channel`` corresponding to the input
entity. entity.
""" """
if hasattr(entity, '__iter__') and not isinstance(entity, str): if utils.is_list_like(entity):
single = False single = False
else: else:
single = True single = True
@ -1940,8 +2050,8 @@ class TelegramClient(TelegramBareClient):
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
return self.get_entity(self.get_input_entity(string)) return self.get_entity(self.session.get_input_entity(string))
except (ValueError, TypeError): except ValueError:
pass pass
raise TypeError( raise TypeError(

View File

@ -5,6 +5,7 @@ to convert between an entity like an User, Chat, etc. into its Input version)
import math import math
import mimetypes import mimetypes
import re import re
import types
from mimetypes import add_type, guess_extension from mimetypes import add_type, guess_extension
from .tl.types import ( from .tl.types import (
@ -27,7 +28,7 @@ from .tl.types import (
from .tl.types.contacts import ResolvedPeer from .tl.types.contacts import ResolvedPeer
USERNAME_RE = re.compile( USERNAME_RE = re.compile(
r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
) )
VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$') VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$')
@ -341,6 +342,17 @@ def is_video(file):
(mimetypes.guess_type(file)[0] or '').startswith('video/')) (mimetypes.guess_type(file)[0] or '').startswith('video/'))
def is_list_like(obj):
"""
Returns True if the given object looks like a list.
Checking if hasattr(obj, '__iter__') and ignoring str/bytes is not
enough. Things like open() are also iterable (and probably many
other things), so just support the commonly known list-like objects.
"""
return isinstance(obj, (list, tuple, set, dict, types.GeneratorType))
def parse_phone(phone): def parse_phone(phone):
"""Parses the given phone, or returns None if it's invalid""" """Parses the given phone, or returns None if it's invalid"""
if isinstance(phone, int): if isinstance(phone, int):
@ -366,6 +378,8 @@ def parse_username(username):
is_invite = bool(m.group(1)) is_invite = bool(m.group(1))
if is_invite: if is_invite:
return username, True return username, True
else:
username = username.rstrip('/')
if VALID_USERNAME_RE.match(username): if VALID_USERNAME_RE.match(username):
return username.lower(), False return username.lower(), False

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.17.3' __version__ = '0.17.4'

View File

@ -1,12 +1,13 @@
import os import os
from getpass import getpass from getpass import getpass
from telethon import TelegramClient, ConnectionMode from telethon.utils import get_display_name
from telethon import ConnectionMode, TelegramClient
from telethon.errors import SessionPasswordNeededError from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import ( from telethon.tl.types import (
UpdateShortChatMessage, UpdateShortMessage, PeerChat PeerChat, UpdateShortChatMessage, UpdateShortMessage
) )
from telethon.utils import get_display_name
def sprint(string, *args, **kwargs): def sprint(string, *args, **kwargs):
@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient):
Telegram through Telethon, such as listing dialogs (open chats), Telegram through Telethon, such as listing dialogs (open chats),
talking to people, downloading media, and receiving updates. talking to people, downloading media, and receiving updates.
""" """
def __init__(self, session_user_id, user_phone, api_id, api_hash, def __init__(self, session_user_id, user_phone, api_id, api_hash,
proxy=None): proxy=None):
""" """
@ -190,6 +192,7 @@ class InteractiveTelegramClient(TelegramClient):
print(' !d <msg-id>: Deletes a message by its id') print(' !d <msg-id>: Deletes a message by its id')
print(' !dm <msg-id>: Downloads the given message Media (if any).') print(' !dm <msg-id>: Downloads the given message Media (if any).')
print(' !dp: Downloads the current dialog Profile picture.') print(' !dp: Downloads the current dialog Profile picture.')
print(' !i: Prints information about this chat..')
print() print()
# And start a while loop to chat # And start a while loop to chat
@ -234,8 +237,7 @@ class InteractiveTelegramClient(TelegramClient):
# And print it to the user # And print it to the user
sprint('[{}:{}] (ID={}) {}: {}'.format( sprint('[{}:{}] (ID={}) {}: {}'.format(
msg.date.hour, msg.date.minute, msg.id, name, msg.date.hour, msg.date.minute, msg.id, name, content))
content))
# Send photo # Send photo
elif msg.startswith('!up '): elif msg.startswith('!up '):
@ -264,12 +266,16 @@ class InteractiveTelegramClient(TelegramClient):
os.makedirs('usermedia', exist_ok=True) os.makedirs('usermedia', exist_ok=True)
output = self.download_profile_photo(entity, 'usermedia') output = self.download_profile_photo(entity, 'usermedia')
if output: if output:
print( print('Profile picture downloaded to', output)
'Profile picture downloaded to {}'.format(output)
)
else: else:
print('No profile picture found for this user!') print('No profile picture found for this user!')
elif msg == '!i':
attributes = list(entity.to_dict().items())
pad = max(len(x) for x, _ in attributes)
for name, val in attributes:
print("{:<{width}} : {}".format(name, val, width=pad))
# Send chat message (if any) # Send chat message (if any)
elif msg: elif msg:
self.send_message(entity, msg, link_preview=False) self.send_message(entity, msg, link_preview=False)
@ -356,6 +362,5 @@ class InteractiveTelegramClient(TelegramClient):
else: else:
who = self.get_entity(update.from_id) who = self.get_entity(update.from_id)
sprint('<< {} @ {} sent "{}"'.format( sprint('<< {} @ {} sent "{}"'.format(
get_display_name(which), get_display_name(who), get_display_name(which), get_display_name(who), update.message
update.message
)) ))