mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 04:30:22 +03:00
Merge branch 'master' of https://github.com/LonamiWebs/Telethon into stop-propagation-of-updates
This commit is contained in:
commit
ebcfd1ed99
|
@ -1,2 +1,3 @@
|
|||
cryptg
|
||||
pysocks
|
||||
hachoir3
|
||||
|
|
|
@ -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)
|
||||
=========================================
|
||||
|
||||
|
|
2
setup.py
2
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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '0.17.3'
|
||||
__version__ = '0.17.4'
|
||||
|
|
|
@ -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
|
||||
))
|
||||
|
|
Loading…
Reference in New Issue
Block a user