diff --git a/optional-requirements.txt b/optional-requirements.txt index c973d402..55bfc014 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,2 +1,3 @@ cryptg +pysocks hachoir3 diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 2fe0352f..ad027361 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,45 @@ it can take advantage of new goodies! .. 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) ========================================= diff --git a/setup.py b/setup.py index 85e77a74..00dd7446 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def main(): long_description = f.read() 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) setup( name='Telethon', diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 9274813e..48b26004 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,6 +1,7 @@ import abc import datetime import itertools +import re from .. import utils from ..errors import RPCError @@ -13,15 +14,14 @@ def _into_id_set(client, chats): if chats is None: return None - if not hasattr(chats, '__iter__') or isinstance(chats, str): + if not utils.is_list_like(chats): chats = (chats,) result = set() for chat in chats: chat = client.get_input_entity(chat) if isinstance(chat, types.InputPeerSelf): - chat = getattr(_into_id_set, 'me', None) or client.get_me() - _into_id_set.me = chat + chat = client.get_me(input_peer=True) result.add(utils.get_peer_id(chat)) return result @@ -42,6 +42,7 @@ class _EventBuilder(abc.ABC): def __init__(self, chats=None, blacklist_chats=False): self.chats = chats self.blacklist_chats = blacklist_chats + self._self_id = None @abc.abstractmethod def build(self, update): @@ -50,6 +51,7 @@ class _EventBuilder(abc.ABC): def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" self.chats = _into_id_set(client, self.chats) + self._self_id = client.get_me(input_peer=True).user_id def _filter_event(self, event): """ @@ -153,6 +155,9 @@ class Raw(_EventBuilder): """ Represents a raw event. The event is the update itself. """ + def resolve(self, client): + pass + def build(self, update): return update @@ -173,19 +178,28 @@ class NewMessage(_EventBuilder): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). - Notes: - The ``message.from_id`` might not only be an integer or ``None``, - but also ``InputPeerSelf()`` for short private messages (the API - would not return such thing, this is a custom modification). + pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + 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, - chats=None, blacklist_chats=False): + chats=None, blacklist_chats=False, pattern=None): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') super().__init__(chats=chats, blacklist_chats=blacklist_chats) self.incoming = incoming 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): if isinstance(update, @@ -201,7 +215,7 @@ class NewMessage(_EventBuilder): silent=update.silent, id=update.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, date=update.date, fwd_from=update.fwd_from, @@ -229,13 +243,16 @@ class NewMessage(_EventBuilder): return # 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 if self.incoming and event.message.out: return if self.outgoing and not event.message.out: return + if self.pattern and not self.pattern(event.message.message or ''): + return return self._filter_event(event) @@ -260,7 +277,14 @@ class NewMessage(_EventBuilder): Whether the message is a reply to some other or not. """ 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)) self.message = message @@ -299,7 +323,11 @@ class NewMessage(_EventBuilder): or the edited message otherwise. """ if not self.message.out: - return None + 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 self._client.edit_message(self.input_chat, self.message, diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index a306302a..d9cea2f0 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -2,18 +2,26 @@ This module holds a rough implementation of the C# TCP client. """ import errno +import logging import socket import time from datetime import timedelta from io import BytesIO, BufferedWriter from threading import Lock +try: + import socks +except ImportError: + socks = None + MAX_TIMEOUT = 15 # in seconds CONN_RESET_ERRNOS = { errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, errno.EINVAL, errno.ENOTCONN } +__log__ = logging.getLogger(__name__) + class TcpClient: """A simple TCP client to ease the work with sockets and proxies.""" @@ -70,6 +78,10 @@ class TcpClient: self._socket.connect(address) break # Successful connection, stop retrying to connect 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 # the loop will allow us to retry if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, @@ -112,19 +124,22 @@ class TcpClient: :param data: the data to send. """ 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: # The socket timeout is now the maximum total duration to send all data. try: self._socket.sendall(data) except socket.timeout as e: + __log__.debug('socket.timeout "%s" while writing data', e) raise TimeoutError() from e - except ConnectionError: - self._raise_connection_reset() + except ConnectionError as e: + __log__.info('ConnectionError "%s" while writing data', e) + self._raise_connection_reset(e) except OSError as e: + __log__.info('OSError "%s" while writing data', e) if e.errno in CONN_RESET_ERRNOS: - self._raise_connection_reset() + self._raise_connection_reset(e) else: raise @@ -136,7 +151,7 @@ class TcpClient: :return: the read data with len(data) == size. """ 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 with BufferedWriter(BytesIO(), buffer_size=size) as buffer: @@ -145,17 +160,22 @@ class TcpClient: try: partial = self._socket.recv(bytes_left) 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 - except ConnectionError: - self._raise_connection_reset() + except ConnectionError as e: + __log__.info('ConnectionError "%s" while reading data', e) + self._raise_connection_reset(e) except OSError as e: + __log__.info('OSError "%s" while reading data', e) if e.errno in CONN_RESET_ERRNOS: - self._raise_connection_reset() + self._raise_connection_reset(e) else: raise if len(partial) == 0: - self._raise_connection_reset() + self._raise_connection_reset(None) buffer.write(partial) bytes_left -= len(partial) @@ -164,7 +184,8 @@ class TcpClient: buffer.flush() return buffer.raw.getvalue() - def _raise_connection_reset(self): + def _raise_connection_reset(self, original): """Disconnects the client and raises ConnectionResetError.""" 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 diff --git a/telethon/session.py b/telethon/session.py index 16232b14..faa1516f 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -227,7 +227,7 @@ class Session: c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() - if tuple_: + if tuple_ and tuple_[0]: self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None @@ -355,14 +355,14 @@ class Session: if not self.save_entities: 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 entities = tlo else: 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) - if hasattr(tlo, 'users') and hasattr(tlo.users, '__iter__'): + if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): entities.extend(tlo.users) if not entities: return diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 320d9ff7..1e1d80e2 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -56,7 +56,7 @@ from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest + UploadMediaRequest, EditMessageRequest, GetFullChatRequest ) from .tl.functions import channels @@ -66,7 +66,7 @@ from .tl.functions.users import ( GetUsersRequest ) from .tl.functions.channels import ( - GetChannelsRequest, GetFullChannelRequest + GetChannelsRequest, GetFullChannelRequest, GetParticipantsRequest ) from .tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, @@ -80,7 +80,8 @@ from .tl.types import ( InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, InputMessageEntityMentionName, DocumentAttributeVideo, - UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates + UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, + MessageMediaWebPage, ChannelParticipantsSearch ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -179,6 +180,9 @@ class TelegramClient(TelegramBareClient): self._phone_code_hash = {} self._phone = None + # Sometimes we need to know who we are, cache the self peer + self._self_input_peer = None + # endregion # region Telegram requests functions @@ -407,6 +411,9 @@ class TelegramClient(TelegramBareClient): '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() return result.user @@ -436,6 +443,9 @@ class TelegramClient(TelegramBareClient): last_name=last_name )) + self._self_input_peer = utils.get_input_peer( + result.user, allow_self=False + ) self._set_connected_and_authorized() return result.user @@ -455,16 +465,31 @@ class TelegramClient(TelegramBareClient): self.session.delete() return True - def get_me(self): + def get_me(self, input_peer=False): """ Gets "me" (the self user) which is currently 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: :obj:`User`: Your own user. """ + if input_peer and self._self_input_peer: + return self._self_input_peer + 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: return None @@ -641,8 +666,8 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): To who will it be sent. - message (:obj:`str`): - The message to be sent. + message (:obj:`str` | :obj:`Message`): + The message to be sent, or another message object to resend. reply_to (:obj:`int` | :obj:`Message`, optional): Whether to reply to a message or not. If an integer is provided, @@ -661,15 +686,35 @@ class TelegramClient(TelegramBareClient): the sent message """ 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( + peer=entity, + message=message, + entities=msg_ent, + no_webpage=not link_preview, + reply_to_msg_id=self._get_message_id(reply_to) + ) - request = SendMessageRequest( - peer=entity, - message=message, - entities=msg_entities, - no_webpage=not link_preview, - reply_to_msg_id=self._get_message_id(reply_to) - ) result = self(request) if isinstance(result, UpdateShortSentMessage): return Message( @@ -930,7 +975,7 @@ class TelegramClient(TelegramBareClient): """ if max_id is None: if message: - if hasattr(message, '__iter__'): + if utils.is_list_like(message): max_id = max(msg.id for msg in message) else: max_id = message.id @@ -970,11 +1015,64 @@ class TelegramClient(TelegramBareClient): 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 # region Uploading files - def send_file(self, entity, file, caption='', + def send_file(self, entity, file, caption=None, force_document=False, progress_callback=None, reply_to=None, attributes=None, @@ -1042,7 +1140,7 @@ class TelegramClient(TelegramBareClient): """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. - if hasattr(file, '__iter__') and not isinstance(file, (str, bytes)): + if utils.is_list_like(file): # Convert to tuple so we can iterate several times file = tuple(x for x in file) if all(utils.is_image(x) for x in file): @@ -1086,11 +1184,11 @@ class TelegramClient(TelegramBareClient): if isinstance(file_handle, use_cache): # File was cached, so an instance of use_cache was returned if as_image: - media = InputMediaPhoto(file_handle, caption) + media = InputMediaPhoto(file_handle, caption or '') else: - media = InputMediaDocument(file_handle, caption) + media = InputMediaDocument(file_handle, caption or '') elif as_image: - media = InputMediaUploadedPhoto(file_handle, caption) + media = InputMediaUploadedPhoto(file_handle, caption or '') else: mime_type = None if isinstance(file, str): @@ -1128,8 +1226,9 @@ class TelegramClient(TelegramBareClient): attr_dict[DocumentAttributeVideo] = doc else: attr_dict = { - DocumentAttributeFilename: - DocumentAttributeFilename('unnamed') + DocumentAttributeFilename: DocumentAttributeFilename( + os.path.basename( + getattr(file, 'name', None) or 'unnamed')) } if 'is_voice_note' in kwargs: @@ -1160,7 +1259,7 @@ class TelegramClient(TelegramBareClient): file=file_handle, mime_type=mime_type, attributes=list(attr_dict.values()), - caption=caption, + caption=caption or '', **input_kw ) @@ -1180,15 +1279,11 @@ class TelegramClient(TelegramBareClient): return msg - def send_voice_note(self, entity, file, caption='', progress_callback=None, - reply_to=None): - """Wrapper method around .send_file() with is_voice_note=()""" - return self.send_file(entity, file, caption, - progress_callback=progress_callback, - reply_to=reply_to, - is_voice_note=()) # empty tuple is enough + def send_voice_note(self, *args, **kwargs): + """Wrapper method around .send_file() with is_voice_note=True""" + return self.send_file(*args, **kwargs, is_voice_note=True) - def _send_album(self, entity, files, caption='', + def _send_album(self, entity, files, caption=None, progress_callback=None, reply_to=None): """Specialized version of .send_file for albums""" # 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 # want the attributes used on them to change. Caption's ignored. entity = self.get_input_entity(entity) + caption = caption or '' reply_to = self._get_message_id(reply_to) # 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 + kind = 'document' possible_names = [] for attr in document.attributes: if isinstance(attr, DocumentAttributeFilename): possible_names.insert(0, attr.file_name) elif isinstance(attr, DocumentAttributeAudio): - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) + kind = 'audio' + if attr.performer and attr.title: + possible_names.append('{} - {}'.format( + 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, 'document', utils.get_extension(document), + file, kind, utils.get_extension(document), date=date, possible_names=possible_names ) @@ -1787,7 +1892,7 @@ class TelegramClient(TelegramBareClient): callback.__name__, type(update).__name__)) 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. @@ -1795,9 +1900,12 @@ class TelegramClient(TelegramBareClient): callback (:obj:`callable`): 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, 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: warnings.warn( @@ -1809,6 +1917,8 @@ class TelegramClient(TelegramBareClient): self.updates.handler = self._on_handler if isinstance(event, type): event = event() + elif not event: + event = events.Raw() event.resolve(self) self._event_builders.append((event, callback)) @@ -1857,7 +1967,7 @@ class TelegramClient(TelegramBareClient): ``User``, ``Chat`` or ``Channel`` corresponding to the input entity. """ - if hasattr(entity, '__iter__') and not isinstance(entity, str): + if utils.is_list_like(entity): single = False else: single = True @@ -1940,8 +2050,8 @@ class TelegramClient(TelegramBareClient): return entity try: # Nobody with this username, maybe it's an exact name/title - return self.get_entity(self.get_input_entity(string)) - except (ValueError, TypeError): + return self.get_entity(self.session.get_input_entity(string)) + except ValueError: pass raise TypeError( diff --git a/telethon/utils.py b/telethon/utils.py index 607897e4..8f38563a 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -5,6 +5,7 @@ to convert between an entity like an User, Chat, etc. into its Input version) import math import mimetypes import re +import types from mimetypes import add_type, guess_extension from .tl.types import ( @@ -27,7 +28,7 @@ from .tl.types import ( from .tl.types.contacts import ResolvedPeer 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]$') @@ -341,6 +342,17 @@ def is_video(file): (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): """Parses the given phone, or returns None if it's invalid""" if isinstance(phone, int): @@ -366,6 +378,8 @@ def parse_username(username): is_invite = bool(m.group(1)) if is_invite: return username, True + else: + username = username.rstrip('/') if VALID_USERNAME_RE.match(username): return username.lower(), False diff --git a/telethon/version.py b/telethon/version.py index eaeb252f..8cc14b33 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.17.3' +__version__ = '0.17.4' diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index d45d2ff1..f6986370 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -1,12 +1,13 @@ import os 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.tl.types import ( - UpdateShortChatMessage, UpdateShortMessage, PeerChat + PeerChat, UpdateShortChatMessage, UpdateShortMessage ) -from telethon.utils import get_display_name def sprint(string, *args, **kwargs): @@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient): Telegram through Telethon, such as listing dialogs (open chats), talking to people, downloading media, and receiving updates. """ + def __init__(self, session_user_id, user_phone, api_id, api_hash, proxy=None): """ @@ -182,14 +184,15 @@ class InteractiveTelegramClient(TelegramClient): # Show some information print_title('Chat with "{}"'.format(get_display_name(entity))) print('Available commands:') - print(' !q: Quits the current chat.') - print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History).') - print(' !up : Uploads and sends the Photo from path.') - print(' !uf : Uploads and sends the File from path.') - print(' !d : Deletes a message by its id') - print(' !dm : Downloads the given message Media (if any).') + print(' !q: Quits the current chat.') + print(' !Q: Quits the current chat and exits.') + print(' !h: prints the latest messages (message History).') + print(' !up : Uploads and sends the Photo from path.') + print(' !uf : Uploads and sends the File from path.') + print(' !d : Deletes a message by its id') + print(' !dm : Downloads the given message Media (if any).') print(' !dp: Downloads the current dialog Profile picture.') + print(' !i: Prints information about this chat..') print() # And start a while loop to chat @@ -234,8 +237,7 @@ class InteractiveTelegramClient(TelegramClient): # And print it to the user sprint('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, - content)) + msg.date.hour, msg.date.minute, msg.id, name, content)) # Send photo elif msg.startswith('!up '): @@ -264,12 +266,16 @@ class InteractiveTelegramClient(TelegramClient): os.makedirs('usermedia', exist_ok=True) output = self.download_profile_photo(entity, 'usermedia') if output: - print( - 'Profile picture downloaded to {}'.format(output) - ) + print('Profile picture downloaded to', output) else: 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) elif msg: self.send_message(entity, msg, link_preview=False) @@ -356,6 +362,5 @@ class InteractiveTelegramClient(TelegramClient): else: who = self.get_entity(update.from_id) sprint('<< {} @ {} sent "{}"'.format( - get_display_name(which), get_display_name(who), - update.message + get_display_name(which), get_display_name(who), update.message ))