Merge branch 'master' into asyncio

This commit is contained in:
Tulir Asokan 2018-02-25 01:27:34 +02:00
commit c2fba26ad9
19 changed files with 573 additions and 370 deletions

View File

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

View File

@ -33,6 +33,13 @@ If you don't have root access, simply pass the ``--user`` flag to the pip
command. If you want to install a specific branch, append ``@branch`` to command. If you want to install a specific branch, append ``@branch`` to
the end of the first install command. the end of the first install command.
By default the library will use a pure Python implementation for encryption,
which can be really slow when uploading or downloading files. If you don't
mind using a C extension, install `cryptg <https://github.com/Lonami/cryptg>`__
via ``pip`` or as an extra:
``pip3 install telethon[cryptg]``
Manual Installation Manual Installation
******************* *******************

View File

@ -14,6 +14,76 @@ 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)
=========================================
*Published at 2018/02/18*
More bug fixes and a few others addition to make events easier to use.
Additions
~~~~~~~~~
- Use ``hachoir`` to extract video and audio metadata before upload.
- New ``.add_event_handler``, ``.add_update_handler`` now deprecated.
Bug fixes
~~~~~~~~~
- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password``
(now it will ask you for it if you don't provide it, as docstring hinted).
- ``.edit_message()`` was ignoring the formatting (e.g. markdown).
- Added missing case to the ``NewMessage`` event for normal groups.
- Accessing the ``.text`` of the ``NewMessage`` event was failing due
to a bug with the markdown unparser.
Internal changes
~~~~~~~~~~~~~~~~
- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead,
which you can find on https://github.com/Lonami/cryptg.
New small convenience functions (v0.17.2) New small convenience functions (v0.17.2)
========================================= =========================================

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pyaes
rsa

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',
@ -149,7 +149,10 @@ def main():
'telethon_generator', 'telethon_tests', 'run_tests.py', 'telethon_generator', 'telethon_tests', 'run_tests.py',
'try_telethon.py' 'try_telethon.py'
]), ]),
install_requires=['pyaes', 'rsa'] install_requires=['pyaes', 'rsa'],
extras_require={
'cryptg': ['cryptg']
}
) )

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
@ -8,14 +9,62 @@ from ..extensions import markdown
from ..tl import types, functions from ..tl import types, functions
async def _into_id_set(client, chats):
"""Helper util to turn the input chat or chats into a set of IDs."""
if chats is None:
return None
if not hasattr(chats, '__iter__') or isinstance(chats, str):
chats = (chats,)
result = set()
for chat in chats:
chat = await client.get_input_entity(chat)
if isinstance(chat, types.InputPeerSelf):
chat = await client.get_me(input_peer=True)
result.add(utils.get_peer_id(chat))
return result
class _EventBuilder(abc.ABC): class _EventBuilder(abc.ABC):
"""
The common event builder, with builtin support to filter per chat.
Args:
chats (:obj:`entity`, optional):
May be one or more entities (username/peer/etc.). By default,
only matching chats will be handled.
blacklist_chats (:obj:`bool`, optional):
Whether to treat the the list of chats as a blacklist (if
it matches it will NOT be handled) or a whitelist (default).
"""
def __init__(self, chats=None, blacklist_chats=False):
self.chats = chats
self.blacklist_chats = blacklist_chats
self._self_id = None
@abc.abstractmethod @abc.abstractmethod
def build(self, update): def build(self, update):
"""Builds an event for the given update if possible, or returns None""" """Builds an event for the given update if possible, or returns None"""
@abc.abstractmethod
async def resolve(self, client): async 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 = await _into_id_set(client, self.chats)
self._self_id = (await client.get_me(input_peer=True)).user_id
def _filter_event(self, event):
"""
If the ID of ``event._chat_peer`` isn't in the chats set (or it is
but the set is a blacklist) returns ``None``, otherwise the event.
"""
if self.chats is not None:
inside = utils.get_peer_id(event._chat_peer) in self.chats
if inside == self.blacklist_chats:
# If this chat matches but it's a blacklist ignore.
# If it doesn't match but it's a whitelist ignore.
return None
return event
class _EventCommon(abc.ABC): class _EventCommon(abc.ABC):
@ -98,7 +147,7 @@ class _EventCommon(abc.ABC):
there is no caching besides local caching yet. there is no caching besides local caching yet.
""" """
if self._chat is None and await self.input_chat: if self._chat is None and await self.input_chat:
self._chat = await self._client.get_entity(self._input_chat) self._chat = await self._client.get_entity(await self._input_chat)
return self._chat return self._chat
@ -106,8 +155,6 @@ class Raw(_EventBuilder):
""" """
Represents a raw event. The event is the update itself. Represents a raw event. The event is the update itself.
""" """
async def resolve(self, client):
pass
def build(self, update): def build(self, update):
return update return update
@ -129,36 +176,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).
chats (:obj:`entity`, optional): pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional):
May be one or more entities (username/peer/etc.). By default, If set, only messages matching this pattern will be handled.
only matching chats will be handled. You can specify a regex-like string which will be matched
against the message, a callable function that returns ``True``
blacklist_chats (:obj:`bool`, optional): if a message is acceptable, or a compiled regex pattern.
Whether to treat the the list of chats as a blacklist (if
it matches it will NOT be handled) or a whitelist (default).
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).
""" """
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)
self.incoming = incoming self.incoming = incoming
self.outgoing = outgoing self.outgoing = outgoing
self.chats = chats if isinstance(pattern, str):
self.blacklist_chats = blacklist_chats self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
async def resolve(self, client): self.pattern = pattern
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): elif hasattr(pattern, 'match') and callable(pattern.match):
self.chats = set(utils.get_peer_id(await client.get_input_entity(x)) self.pattern = pattern.match
for x in self.chats) else:
elif self.chats is not None: raise TypeError('Invalid pattern type given')
self.chats = {utils.get_peer_id(
await client.get_input_entity(self.chats))}
def build(self, update): def build(self, update):
if isinstance(update, if isinstance(update,
@ -174,7 +213,23 @@ 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,
date=update.date,
fwd_from=update.fwd_from,
via_bot_id=update.via_bot_id,
reply_to_msg_id=update.reply_to_msg_id,
entities=update.entities
))
elif isinstance(update, types.UpdateShortChatMessage):
event = NewMessage.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
from_id=update.from_id,
to_id=types.PeerChat(update.chat_id),
message=update.message, message=update.message,
date=update.date, date=update.date,
fwd_from=update.fwd_from, fwd_from=update.fwd_from,
@ -186,23 +241,18 @@ 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
if self.chats is not None: return self._filter_event(event)
inside = utils.get_peer_id(event.message.to_id) in self.chats
if inside == self.blacklist_chats:
# If this chat matches but it's a blacklist ignore.
# If it doesn't match but it's a whitelist ignore.
return
# Tests passed so return the event
return event
class Event(_EventCommon): class Event(_EventCommon):
""" """
@ -264,9 +314,13 @@ class NewMessage(_EventBuilder):
or the edited message otherwise. or the edited message otherwise.
""" """
if not self.message.out: if not self.message.out:
return None if not isinstance(self.message.to_id, types.PeerUser):
return None
me = await self._client.get_me(input_peer=True)
if self.message.to_id.user_id != me.user_id:
return None
return await self._client.edit_message(self.input_chat, return await self._client.edit_message(await self.input_chat,
self.message, self.message,
*args, **kwargs) *args, **kwargs)
@ -277,7 +331,7 @@ class NewMessage(_EventBuilder):
This is a shorthand for This is a shorthand for
``client.delete_messages(event.chat, event.message, ...)``. ``client.delete_messages(event.chat, event.message, ...)``.
""" """
return await self._client.delete_messages(self.input_chat, return await self._client.delete_messages(await self.input_chat,
[self.message], [self.message],
*args, **kwargs) *args, **kwargs)
@ -413,30 +467,7 @@ class NewMessage(_EventBuilder):
class ChatAction(_EventBuilder): class ChatAction(_EventBuilder):
""" """
Represents an action in a chat (such as user joined, left, or new pin). Represents an action in a chat (such as user joined, left, or new pin).
Args:
chats (:obj:`entity`, optional):
May be one or more entities (username/peer/etc.). By default,
only matching chats will be handled.
blacklist_chats (:obj:`bool`, optional):
Whether to treat the the list of chats as a blacklist (if
it matches it will NOT be handled) or a whitelist (default).
""" """
def __init__(self, chats=None, blacklist_chats=False):
# TODO This can probably be reused in all builders
self.chats = chats
self.blacklist_chats = blacklist_chats
async def resolve(self, client):
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
self.chats = set(utils.get_peer_id(await client.get_input_entity(x))
for x in self.chats)
elif self.chats is not None:
self.chats = {utils.get_peer_id(
await client.get_input_entity(self.chats))}
def build(self, update): def build(self, update):
if isinstance(update, types.UpdateChannelPinnedMessage): if isinstance(update, types.UpdateChannelPinnedMessage):
# Telegram sends UpdateChannelPinnedMessage and then # Telegram sends UpdateChannelPinnedMessage and then
@ -494,16 +525,7 @@ class ChatAction(_EventBuilder):
else: else:
return return
if self.chats is None: return self._filter_event(event)
return event
else:
inside = utils.get_peer_id(event._chat_peer) in self.chats
if inside == self.blacklist_chats:
# If this chat matches but it's a blacklist ignore.
# If it doesn't match but it's a whitelist ignore.
return
return event
class Event(_EventCommon): class Event(_EventCommon):
""" """
@ -649,7 +671,6 @@ class UserUpdate(_EventBuilder):
""" """
Represents an user update (gone online, offline, joined Telegram). Represents an user update (gone online, offline, joined Telegram).
""" """
def build(self, update): def build(self, update):
if isinstance(update, types.UpdateUserStatus): if isinstance(update, types.UpdateUserStatus):
event = UserUpdate.Event(update.user_id, event = UserUpdate.Event(update.user_id,
@ -657,10 +678,7 @@ class UserUpdate(_EventBuilder):
else: else:
return return
return event return self._filter_event(event)
async def resolve(self, client):
pass
class Event(_EventCommon): class Event(_EventCommon):
""" """
@ -800,13 +818,16 @@ class MessageChanged(_EventBuilder):
""" """
Represents a message changed (edited or deleted). Represents a message changed (edited or deleted).
""" """
def build(self, update): def build(self, update):
if isinstance(update, (types.UpdateEditMessage, if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)): types.UpdateEditChannelMessage)):
event = MessageChanged.Event(edit_msg=update.message) event = MessageChanged.Event(edit_msg=update.message)
elif isinstance(update, (types.UpdateDeleteMessages, elif isinstance(update, types.UpdateDeleteMessages):
types.UpdateDeleteChannelMessages)): event = MessageChanged.Event(
deleted_ids=update.messages,
peer=None
)
elif isinstance(update, types.UpdateDeleteChannelMessages):
event = MessageChanged.Event( event = MessageChanged.Event(
deleted_ids=update.messages, deleted_ids=update.messages,
peer=types.PeerChannel(update.channel_id) peer=types.PeerChannel(update.channel_id)
@ -814,91 +835,32 @@ class MessageChanged(_EventBuilder):
else: else:
return return
return event return self._filter_event(event)
async def resolve(self, client): class Event(NewMessage.Event):
pass
class Event(_EventCommon):
""" """
Represents the event of an user status update (last seen, joined). Represents the event of an user status update (last seen, joined).
Please note that the ``message`` member will be ``None`` if the
action was a deletion and not an edit.
Members: Members:
edited (:obj:`bool`): edited (:obj:`bool`):
``True`` if the message was edited. ``True`` if the message was edited.
message (:obj:`Message`, optional):
The new edited message, if any.
deleted (:obj:`bool`): deleted (:obj:`bool`):
``True`` if the message IDs were deleted. ``True`` if the message IDs were deleted.
deleted_ids (:obj:`List[int]`): deleted_ids (:obj:`List[int]`):
A list containing the IDs of the messages that were deleted. A list containing the IDs of the messages that were deleted.
input_sender (:obj:`InputPeer`):
This is the input version of the user who edited the message.
Similarly to ``input_chat``, this doesn't have things like
username or similar, but still useful in some cases.
Note that this might not be available if the library can't
find the input chat.
sender (:obj:`User`):
This property will make an API call the first time to get the
most up to date version of the sender, so use with care as
there is no caching besides local caching yet.
``input_sender`` needs to be available (often the case).
""" """
def __init__(self, edit_msg=None, deleted_ids=None, peer=None): def __init__(self, edit_msg=None, deleted_ids=None, peer=None):
super().__init__(peer if not edit_msg else edit_msg.to_id) if edit_msg is None:
msg = types.Message((deleted_ids or [0])[0], peer, None, '')
else:
msg = edit_msg
super().__init__(msg)
self.edited = bool(edit_msg) self.edited = bool(edit_msg)
self.message = edit_msg
self.deleted = bool(deleted_ids) self.deleted = bool(deleted_ids)
self.deleted_ids = deleted_ids or [] self.deleted_ids = deleted_ids or []
self._input_sender = None
self._sender = None
@property
async def input_sender(self):
"""
This (:obj:`InputPeer`) is the input version of the user who
sent the message. Similarly to ``input_chat``, this doesn't have
things like username or similar, but still useful in some cases.
Note that this might not be available if the library can't
find the input chat, or if the message a broadcast on a channel.
"""
# TODO Code duplication
if self._input_sender is None:
if self.is_channel and not self.is_group:
return None
try:
self._input_sender = await self._client.get_input_entity(
self.message.from_id
)
except (ValueError, TypeError):
# We can rely on self.input_chat for this
self._input_sender = await self._get_input_entity(
self.message.id,
self.message.from_id,
chat=await self.input_chat
)
return self._input_sender
@property
async def sender(self):
"""
This (:obj:`User`) will make an API call the first time to get
the most up to date version of the sender, so use with care as
there is no caching besides local caching yet.
``input_sender`` needs to be available (often the case).
"""
if self._sender is None and await self.input_sender:
self._sender = await self._client.get_entity(self._input_sender)
return self._sender

View File

@ -169,6 +169,7 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True)) entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True))
text = _add_surrogate(text) text = _add_surrogate(text)
delimiters = {v: k for k, v in delimiters.items()}
for entity in entities: for entity in entities:
s = entity.offset s = entity.offset
e = entity.offset + entity.length e = entity.offset + entity.length

View File

@ -15,6 +15,11 @@ CONN_RESET_ERRNOS = {
errno.EINVAL, errno.ENOTCONN errno.EINVAL, errno.ENOTCONN
} }
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,
@ -81,6 +86,9 @@ class TcpClient:
await asyncio.sleep(timeout) await asyncio.sleep(timeout)
timeout = min(timeout * 2, MAX_TIMEOUT) timeout = min(timeout * 2, MAX_TIMEOUT)
except OSError as e: except OSError as 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,

View File

@ -17,7 +17,7 @@ from ..errors import SecurityError
from ..extensions import BinaryReader from ..extensions import BinaryReader
from ..network import MtProtoPlainSender from ..network import MtProtoPlainSender
from ..tl.functions import ( from ..tl.functions import (
ReqPqRequest, ReqDHParamsRequest, SetClientDHParamsRequest ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest
) )
@ -53,7 +53,7 @@ async def _do_authentication(connection):
sender = MtProtoPlainSender(connection) sender = MtProtoPlainSender(connection)
# Step 1 sending: PQ Request, endianness doesn't matter since it's random # Step 1 sending: PQ Request, endianness doesn't matter since it's random
req_pq_request = ReqPqRequest( req_pq_request = ReqPqMultiRequest(
nonce=int.from_bytes(os.urandom(16), 'big', signed=True) nonce=int.from_bytes(os.urandom(16), 'big', signed=True)
) )
await sender.send(bytes(req_pq_request)) await sender.send(bytes(req_pq_request))

View File

@ -221,7 +221,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
@ -424,13 +424,19 @@ class Session:
(phone,)) (phone,))
else: else:
username, _ = utils.parse_username(key) username, _ = utils.parse_username(key)
c.execute('select id, hash from entities where username=?', if username:
(username,)) c.execute('select id, hash from entities where username=?',
(username,))
if isinstance(key, int): if isinstance(key, int):
c.execute('select id, hash from entities where id=?', (key,)) c.execute('select id, hash from entities where id=?', (key,))
result = c.fetchone() result = c.fetchone()
if not result and isinstance(key, str):
# Try exact match by name if phone/username failed
c.execute('select id, hash from entities where name=?', (key,))
result = c.fetchone()
c.close() c.close()
if result: if result:
i, h = result # unpack resulting tuple i, h = result # unpack resulting tuple

View File

@ -3,7 +3,6 @@ import logging
import os import os
from asyncio import Lock from asyncio import Lock
from datetime import timedelta from datetime import timedelta
from . import version, utils from . import version, utils
from .crypto import rsa from .crypto import rsa
from .errors import ( from .errors import (
@ -78,7 +77,7 @@ class TelegramBareClient:
"Refer to telethon.rtfd.io for more information.") "Refer to telethon.rtfd.io for more information.")
self._use_ipv6 = use_ipv6 self._use_ipv6 = use_ipv6
# Determine what session object we have # Determine what session object we have
if isinstance(session, str) or session is None: if isinstance(session, str) or session is None:
session = Session(session) session = Session(session)
@ -554,17 +553,6 @@ class TelegramBareClient:
""" """
self.updates.process(await self(GetStateRequest())) self.updates.process(await self(GetStateRequest()))
def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates"""
self.updates.handlers.append(handler)
def remove_update_handler(self, handler):
self.updates.handlers.remove(handler)
def list_update_handlers(self):
return self.updates.handlers[:]
# endregion # endregion
# Constant read # Constant read

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
import getpass
import hashlib import hashlib
import io import io
import itertools import itertools
@ -6,6 +7,7 @@ import logging
import os import os
import re import re
import sys import sys
import warnings
from collections import OrderedDict, UserList from collections import OrderedDict, UserList
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
@ -23,8 +25,15 @@ try:
except ImportError: except ImportError:
socks = None socks = None
try:
import hachoir
import hachoir.metadata
import hachoir.parser
except ImportError:
hachoir = None
from . import TelegramBareClient from . import TelegramBareClient
from . import helpers, utils from . import helpers, utils, events
from .errors import ( from .errors import (
RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError,
PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError,
@ -47,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
@ -57,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,
@ -71,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
@ -168,6 +178,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
@ -207,8 +220,9 @@ class TelegramClient(TelegramBareClient):
async def start(self, async def start(self,
phone=lambda: input('Please enter your phone: '), phone=lambda: input('Please enter your phone: '),
password=None, bot_token=None, password=lambda: getpass.getpass(
force_sms=False, code_callback=None, 'Please enter your password: '),
bot_token=None, force_sms=False, code_callback=None,
first_name='New User', last_name=''): first_name='New User', last_name=''):
""" """
Convenience method to interactively connect and sign in if required, Convenience method to interactively connect and sign in if required,
@ -265,7 +279,7 @@ class TelegramClient(TelegramBareClient):
if not phone and not bot_token: if not phone and not bot_token:
raise ValueError('No phone number or bot token provided.') raise ValueError('No phone number or bot token provided.')
if phone and bot_token: if phone and bot_token and not callable(phone):
raise ValueError('Both a phone and a bot token provided, ' raise ValueError('Both a phone and a bot token provided, '
'must only provide one of either') 'must only provide one of either')
@ -322,6 +336,9 @@ class TelegramClient(TelegramBareClient):
"Two-step verification is enabled for this account. " "Two-step verification is enabled for this account. "
"Please provide the 'password' argument to 'start()'." "Please provide the 'password' argument to 'start()'."
) )
# TODO If callable given make it retry on invalid
if callable(password):
password = password()
me = await self.sign_in(phone=phone, password=password) me = await self.sign_in(phone=phone, password=password)
# We won't reach here if any step failed (exit by exception) # We won't reach here if any step failed (exit by exception)
@ -393,6 +410,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
@ -422,6 +442,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
@ -441,16 +464,31 @@ class TelegramClient(TelegramBareClient):
self.session.delete() self.session.delete()
return True return True
async def get_me(self): async 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 (await self(GetUsersRequest([InputUserSelf()])))[0] me = (await self(GetUsersRequest([InputUserSelf()])))[0]
if not self._self_input_peer:
self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
if input_peer:
return self._self_input_peer
return me
except UnauthorizedError: except UnauthorizedError:
return None return None
@ -627,8 +665,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,
@ -646,16 +684,37 @@ class TelegramClient(TelegramBareClient):
Returns: Returns:
the sent message the sent message
""" """
entity = await self.get_input_entity(entity)
message, msg_entities = await self._parse_message_text(message, parse_mode)
request = SendMessageRequest( entity = await self.get_input_entity(entity)
peer=entity, if isinstance(message, Message):
message=message, if (message.media
entities=msg_entities, and not isinstance(message.media, MessageMediaWebPage)):
no_webpage=not link_preview, return await self.send_file(entity, message.media)
reply_to_msg_id=self._get_message_id(reply_to)
) 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 = await 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)
)
result = await self(request) result = await self(request)
if isinstance(result, UpdateShortSentMessage): if isinstance(result, UpdateShortSentMessage):
@ -956,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
async def send_file(self, entity, file, caption='', async 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,
@ -1019,6 +1131,10 @@ class TelegramClient(TelegramBareClient):
If "is_voice_note" in kwargs, despite its value, and the file is If "is_voice_note" in kwargs, despite its value, and the file is
sent as a document, it will be sent as a voice note. sent as a document, it will be sent as a voice note.
Notes:
If the ``hachoir3`` package (``hachoir`` module) is installed,
it will be used to determine metadata from audio and video files.
Returns: Returns:
The message (or messages) containing the sent file. The message (or messages) containing the sent file.
""" """
@ -1068,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):
@ -1082,12 +1198,32 @@ class TelegramClient(TelegramBareClient):
attr_dict = { attr_dict = {
DocumentAttributeFilename: DocumentAttributeFilename:
DocumentAttributeFilename(os.path.basename(file)) DocumentAttributeFilename(os.path.basename(file))
# TODO If the input file is an audio, find out:
# Performer and song title and add DocumentAttributeAudio
} }
if utils.is_audio(file) and hachoir:
m = hachoir.metadata.extractMetadata(
hachoir.parser.createParser(file)
)
attr_dict[DocumentAttributeAudio] = DocumentAttributeAudio(
title=m.get('title') if m.has('title') else None,
performer=m.get('author') if m.has('author') else None,
duration=int(m.get('duration').seconds
if m.has('duration') else 0)
)
if not force_document and utils.is_video(file): if not force_document and utils.is_video(file):
attr_dict[DocumentAttributeVideo] = \ if hachoir:
DocumentAttributeVideo(0, 0, 0) m = hachoir.metadata.extractMetadata(
hachoir.parser.createParser(file)
)
doc = DocumentAttributeVideo(
w=m.get('width') if m.has('width') else 0,
h=m.get('height') if m.has('height') else 0,
duration=int(m.get('duration').seconds
if m.has('duration') else 0)
)
else:
doc = DocumentAttributeVideo(0, 0, 0)
attr_dict[DocumentAttributeVideo] = doc
else: else:
attr_dict = { attr_dict = {
DocumentAttributeFilename: DocumentAttributeFilename:
@ -1095,8 +1231,11 @@ class TelegramClient(TelegramBareClient):
} }
if 'is_voice_note' in kwargs: if 'is_voice_note' in kwargs:
attr_dict[DocumentAttributeAudio] = \ if DocumentAttributeAudio in attr_dict:
DocumentAttributeAudio(0, voice=True) attr_dict[DocumentAttributeAudio].voice = True
else:
attr_dict[DocumentAttributeAudio] = \
DocumentAttributeAudio(0, voice=True)
# Now override the attributes if any. As we have a dict of # Now override the attributes if any. As we have a dict of
# {cls: instance}, we can override any class with the list # {cls: instance}, we can override any class with the list
@ -1119,7 +1258,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
) )
@ -1139,7 +1278,7 @@ class TelegramClient(TelegramBareClient):
return msg return msg
async def send_voice_note(self, entity, file, caption='', async def send_voice_note(self, entity, file, caption=None,
progress_callback=None, reply_to=None): progress_callback=None, reply_to=None):
"""Wrapper method around .send_file() with is_voice_note=()""" """Wrapper method around .send_file() with is_voice_note=()"""
return await self.send_file(entity, file, caption, return await self.send_file(entity, file, caption,
@ -1147,7 +1286,7 @@ class TelegramClient(TelegramBareClient):
reply_to=reply_to, reply_to=reply_to,
is_voice_note=()) # empty tuple is enough is_voice_note=()) # empty tuple is enough
async def _send_album(self, entity, files, caption='', async 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
@ -1156,6 +1295,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 = await self.get_input_entity(entity) entity = await 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
@ -1479,18 +1619,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):
possible_names.append('{} - {}'.format( kind = 'audio'
attr.performer, attr.title 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 = 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
) )
@ -1711,28 +1860,19 @@ class TelegramClient(TelegramBareClient):
# region Event handling # region Event handling
async def on(self, event): def on(self, event):
""" """
Decorator helper method around add_event_handler().
Turns the given entity into a valid Telegram user or chat.
Args: Args:
event (:obj:`_EventBuilder` | :obj:`type`): event (:obj:`_EventBuilder` | :obj:`type`):
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 isinstance(event, type): async def decorator(f):
event = event() await self.add_event_handler(f, event)
await event.resolve(self)
def decorator(f):
self._event_builders.append((event, f))
return f return f
if self._on_handler not in self.updates.handlers:
self.add_update_handler(self._on_handler)
return decorator return decorator
async def _on_handler(self, update): async def _on_handler(self, update):
@ -1742,6 +1882,48 @@ class TelegramClient(TelegramBareClient):
event._client = self event._client = self
await callback(event) await callback(event)
async def add_event_handler(self, callback, event=None):
"""
Registers the given callback to be called on the specified event.
Args:
callback (:obj:`callable`):
The callable function accepting one parameter to be used.
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.
"""
self.updates.handler = self._on_handler
if isinstance(event, type):
event = event()
elif not event:
event = events.Raw()
await event.resolve(self)
self._event_builders.append((event, callback))
def add_update_handler(self, handler):
"""Adds an update handler (a function which takes a TLObject,
an update, as its parameter) and listens for updates"""
warnings.warn(
'add_update_handler is deprecated, use the @client.on syntax '
'or add_event_handler(callback, events.Raw) instead (see '
'https://telethon.rtfd.io/en/latest/extra/basic/working-'
'with-updates.html)'
)
self.add_event_handler(handler, events.Raw)
def remove_update_handler(self, handler):
pass
def list_update_handlers(self):
return []
# endregion # endregion
# region Small utilities to make users' life easier # region Small utilities to make users' life easier
@ -1831,9 +2013,9 @@ class TelegramClient(TelegramBareClient):
if user.phone == phone: if user.phone == phone:
return user return user
else: else:
string, is_join_chat = utils.parse_username(string) username, is_join_chat = utils.parse_username(string)
if is_join_chat: if is_join_chat:
invite = await self(CheckChatInviteRequest(string)) invite = await self(CheckChatInviteRequest(username))
if isinstance(invite, ChatInvite): if isinstance(invite, ChatInvite):
raise ValueError( raise ValueError(
'Cannot get entity from a channel ' 'Cannot get entity from a channel '
@ -1841,13 +2023,19 @@ class TelegramClient(TelegramBareClient):
) )
elif isinstance(invite, ChatInviteAlready): elif isinstance(invite, ChatInviteAlready):
return invite.chat return invite.chat
else: elif username:
if string in ('me', 'self'): if username in ('me', 'self'):
return await self.get_me() return await self.get_me()
result = await self(ResolveUsernameRequest(string)) result = await self(ResolveUsernameRequest(username))
for entity in itertools.chain(result.users, result.chats): for entity in itertools.chain(result.users, result.chats):
if entity.username.lower() == string: if entity.username.lower() == username:
return entity return entity
try:
# Nobody with this username, maybe it's an exact name/title
return await self.get_entity(
self.session.get_input_entity(string))
except ValueError:
pass
raise TypeError( raise TypeError(
'Cannot turn "{}" into any entity (user or chat)'.format(string) 'Cannot turn "{}" into any entity (user or chat)'.format(string)

View File

@ -16,15 +16,15 @@ class UpdateState:
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers
def __init__(self, loop=None): def __init__(self, loop=None):
self.handlers = [] self.handler = None
self._loop = loop if loop else asyncio.get_event_loop() self._loop = loop if loop else asyncio.get_event_loop()
# https://core.telegram.org/api/updates # https://core.telegram.org/api/updates
self._state = tl.updates.State(0, 0, datetime.now(), 0, 0) self._state = tl.updates.State(0, 0, datetime.now(), 0, 0)
def handle_update(self, update): def handle_update(self, update):
for handler in self.handlers: if self.handler:
asyncio.ensure_future(handler(update), loop=self._loop) asyncio.ensure_future(self.handler(update), loop=self._loop)
def process(self, update): def process(self, update):
"""Processes an update object. This method is normally called by """Processes an update object. This method is normally called by

View File

@ -27,9 +27,11 @@ 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]$')
def get_display_name(entity): def get_display_name(entity):
"""Gets the input peer for the given "entity" (user, chat or channel) """Gets the input peer for the given "entity" (user, chat or channel)
@ -323,12 +325,20 @@ def get_input_media(media, user_caption=None, is_photo=False):
def is_image(file): def is_image(file):
"""Returns True if the file extension looks like an image file""" """Returns True if the file extension looks like an image file"""
return (mimetypes.guess_type(file)[0] or '').startswith('image/') return (isinstance(file, str) and
(mimetypes.guess_type(file)[0] or '').startswith('image/'))
def is_audio(file):
"""Returns True if the file extension looks like an audio file"""
return (isinstance(file, str) and
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
def is_video(file): def is_video(file):
"""Returns True if the file extension looks like a video file""" """Returns True if the file extension looks like a video file"""
return (mimetypes.guess_type(file)[0] or '').startswith('video/') return (isinstance(file, str) and
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
def parse_phone(phone): def parse_phone(phone):
@ -346,15 +356,23 @@ def parse_username(username):
a string, username or URL. Returns a tuple consisting of a string, username or URL. Returns a tuple consisting of
both the stripped, lowercase username and whether it is both the stripped, lowercase username and whether it is
a joinchat/ hash (in which case is not lowercase'd). a joinchat/ hash (in which case is not lowercase'd).
Returns None if the username is not valid.
""" """
username = username.strip() username = username.strip()
m = USERNAME_RE.match(username) m = USERNAME_RE.match(username)
if m: if m:
result = username[m.end():] username = username[m.end():]
is_invite = bool(m.group(1)) is_invite = bool(m.group(1))
return result if is_invite else result.lower(), is_invite if is_invite:
else: return username, True
else:
username = username.rstrip('/')
if VALID_USERNAME_RE.match(username):
return username.lower(), False return username.lower(), False
else:
return None, False
def get_peer_id(peer): def get_peer_id(peer):

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.2' __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):
""" """
@ -182,14 +184,15 @@ class InteractiveTelegramClient(TelegramClient):
# Show some information # Show some information
print_title('Chat with "{}"'.format(get_display_name(entity))) print_title('Chat with "{}"'.format(get_display_name(entity)))
print('Available commands:') print('Available commands:')
print(' !q: Quits the current chat.') print(' !q: Quits the current chat.')
print(' !Q: Quits the current chat and exits.') print(' !Q: Quits the current chat and exits.')
print(' !h: prints the latest messages (message History).') print(' !h: prints the latest messages (message History).')
print(' !up <path>: Uploads and sends the Photo from path.') print(' !up <path>: Uploads and sends the Photo from path.')
print(' !uf <path>: Uploads and sends the File from path.') print(' !uf <path>: Uploads and sends the File from path.')
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
)) ))

View File

@ -1,46 +1,36 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# A simple script to print all updates received # A simple script to print all updates received
from getpass import getpass
from os import environ from os import environ
# environ is used to get API information from environment variables # environ is used to get API information from environment variables
# You could also use a config file, pass them as arguments, # You could also use a config file, pass them as arguments,
# or even hardcode them (not recommended) # or even hardcode them (not recommended)
from telethon import TelegramClient from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
def main(): def main():
session_name = environ.get('TG_SESSION', 'session') session_name = environ.get('TG_SESSION', 'session')
user_phone = environ['TG_PHONE']
client = TelegramClient(session_name, client = TelegramClient(session_name,
int(environ['TG_API_ID']), int(environ['TG_API_ID']),
environ['TG_API_HASH'], environ['TG_API_HASH'],
proxy=None, proxy=None,
update_workers=4) update_workers=4,
spawn_read_thread=False)
print('INFO: Connecting to Telegram Servers...', end='', flush=True) if 'TG_PHONE' in environ:
client.connect() client.start(phone=environ['TG_PHONE'])
print('Done!') else:
client.start()
if not client.is_user_authorized():
print('INFO: Unauthorized user')
client.send_code_request(user_phone)
code_ok = False
while not code_ok:
code = input('Enter the auth code: ')
try:
code_ok = client.sign_in(user_phone, code)
except SessionPasswordNeededError:
password = getpass('Two step verification enabled. Please enter your password: ')
code_ok = client.sign_in(password=password)
print('INFO: Client initialized succesfully!')
client.add_update_handler(update_handler) client.add_update_handler(update_handler)
input('Press Enter to stop this!\n') print('(Press Ctrl+C to stop this)')
client.idle()
def update_handler(update): def update_handler(update):
print(update) print(update)
print('Press Enter to stop this!')
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -9,17 +9,12 @@ file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION.
This script assumes that you have certain files on the working directory, This script assumes that you have certain files on the working directory,
such as "xfiles.m4a" or "anytime.png" for some of the automated replies. such as "xfiles.m4a" or "anytime.png" for some of the automated replies.
""" """
from getpass import getpass import re
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from os import environ from os import environ
import re from telethon import TelegramClient, events, utils
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService
from telethon.tl.functions.messages import EditMessageRequest
"""Uncomment this for debugging """Uncomment this for debugging
import logging import logging
@ -35,103 +30,57 @@ REACTS = {'emacs': 'Needs more vim',
recent_reacts = defaultdict(list) recent_reacts = defaultdict(list)
def update_handler(update): if __name__ == '__main__':
global recent_reacts # TG_API_ID and TG_API_HASH *must* exist or this won't run!
try: session_name = environ.get('TG_SESSION', 'session')
msg = update.message client = TelegramClient(
except AttributeError: session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'],
# print(update, 'did not have update.message') spawn_read_thread=False, proxy=None, update_workers=4
return )
if isinstance(msg, MessageService):
print(msg, 'was service msg')
return
# React to messages in supergroups and PMs @client.on(events.NewMessage)
if isinstance(update, UpdateNewChannelMessage): def my_handler(event: events.NewMessage.Event):
words = re.split('\W+', msg.message) global recent_reacts
# This utils function gets the unique identifier from peers (to_id)
to_id = utils.get_peer_id(event.message.to_id)
# Through event.raw_text we access the text of messages without format
words = re.split('\W+', event.raw_text)
# Try to match some reaction
for trigger, response in REACTS.items(): for trigger, response in REACTS.items():
if len(recent_reacts[msg.to_id.channel_id]) > 3: if len(recent_reacts[to_id]) > 3:
# Silently ignore triggers if we've recently sent 3 reactions # Silently ignore triggers if we've recently sent 3 reactions
break break
if trigger in words: if trigger in words:
# Remove recent replies older than 10 minutes # Remove recent replies older than 10 minutes
recent_reacts[msg.to_id.channel_id] = [ recent_reacts[to_id] = [
a for a in recent_reacts[msg.to_id.channel_id] if a for a in recent_reacts[to_id] if
datetime.now() - a < timedelta(minutes=10) datetime.now() - a < timedelta(minutes=10)
] ]
# Send a reaction # Send a reaction as a reply (otherwise, event.respond())
client.send_message(msg.to_id, response, reply_to=msg.id) event.reply(response)
# Add this reaction to the list of recent actions # Add this reaction to the list of recent actions
recent_reacts[msg.to_id.channel_id].append(datetime.now()) recent_reacts[to_id].append(datetime.now())
if isinstance(update, UpdateShortMessage): # Automatically send relevant media when we say certain things
words = re.split('\W+', msg) # When invoking requests, get_input_entity needs to be called manually
for trigger, response in REACTS.items(): if event.out:
if len(recent_reacts[update.user_id]) > 3: if event.raw_text.lower() == 'x files theme':
# Silently ignore triggers if we've recently sent 3 reactions client.send_voice_note(event.message.to_id, 'xfiles.m4a',
break reply_to=event.message.id)
if event.raw_text.lower() == 'anytime':
client.send_file(event.message.to_id, 'anytime.png',
reply_to=event.message.id)
if '.shrug' in event.text:
event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯'))
if trigger in words: if 'TG_PHONE' in environ:
# Send a reaction client.start(phone=environ['TG_PHONE'])
client.send_message(update.user_id, response, reply_to=update.id) else:
# Add this reaction to the list of recent reactions client.start()
recent_reacts[update.user_id].append(datetime.now())
# Automatically send relevant media when we say certain things print('(Press Ctrl+C to stop this)')
# When invoking requests, get_input_entity needs to be called manually client.idle()
if isinstance(update, UpdateNewChannelMessage) and msg.out:
if msg.message.lower() == 'x files theme':
client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id)
if msg.message.lower() == 'anytime':
client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id)
if '.shrug' in msg.message:
client(EditMessageRequest(
client.get_input_entity(msg.to_id), msg.id,
message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯')
))
if isinstance(update, UpdateShortMessage) and update.out:
if msg.lower() == 'x files theme':
client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id)
if msg.lower() == 'anytime':
client.send_file(update.user_id, 'anytime.png', reply_to=update.id)
if '.shrug' in msg:
client(EditMessageRequest(
client.get_input_entity(update.user_id), update.id,
message=msg.replace('.shrug', r'¯\_(ツ)_/¯')
))
if __name__ == '__main__':
session_name = environ.get('TG_SESSION', 'session')
user_phone = environ['TG_PHONE']
client = TelegramClient(
session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'],
proxy=None, update_workers=4
)
try:
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
client.connect()
print('Done!')
if not client.is_user_authorized():
print('INFO: Unauthorized user')
client.send_code_request(user_phone)
code_ok = False
while not code_ok:
code = input('Enter the auth code: ')
try:
code_ok = client.sign_in(user_phone, code)
except SessionPasswordNeededError:
password = getpass('Two step verification enabled. '
'Please enter your password: ')
code_ok = client.sign_in(password=password)
print('INFO: Client initialized successfully!')
client.add_update_handler(update_handler)
input('Press Enter to stop this!\n')
except KeyboardInterrupt:
pass
finally:
client.disconnect()

View File

@ -53,7 +53,10 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
---functions--- ---functions---
// Deprecated since somewhere around February of 2018
// See https://core.telegram.org/mtproto/auth_key
req_pq#60469778 nonce:int128 = ResPQ; req_pq#60469778 nonce:int128 = ResPQ;
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params; req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params;