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
pysocks
hachoir3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '0.17.3'
__version__ = '0.17.4'

View File

@ -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 <path>: Uploads and sends the Photo from path.')
print(' !uf <path>: Uploads and sends the File from path.')
print(' !d <msg-id>: Deletes a message by its id')
print(' !dm <msg-id>: 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 <path>: Uploads and sends the Photo from path.')
print(' !uf <path>: Uploads and sends the File from path.')
print(' !d <msg-id>: Deletes a message by its id')
print(' !dm <msg-id>: 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
))