mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-04 12:10:21 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
c2fba26ad9
3
optional-requirements.txt
Normal file
3
optional-requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
cryptg
|
||||||
|
pysocks
|
||||||
|
hachoir3
|
|
@ -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
|
||||||
*******************
|
*******************
|
||||||
|
|
|
@ -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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pyaes
|
||||||
|
rsa
|
7
setup.py
7
setup.py
|
@ -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']
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 ''):
|
||||||
if self.chats is not None:
|
|
||||||
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
|
return
|
||||||
|
|
||||||
# Tests passed so return the event
|
return self._filter_event(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:
|
||||||
|
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 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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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,6 +424,7 @@ class Session:
|
||||||
(phone,))
|
(phone,))
|
||||||
else:
|
else:
|
||||||
username, _ = utils.parse_username(key)
|
username, _ = utils.parse_username(key)
|
||||||
|
if username:
|
||||||
c.execute('select id, hash from entities where username=?',
|
c.execute('select id, hash from entities where username=?',
|
||||||
(username,))
|
(username,))
|
||||||
|
|
||||||
|
@ -431,6 +432,11 @@ class Session:
|
||||||
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
|
||||||
|
|
|
@ -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 (
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
||||||
|
entity = await self.get_input_entity(entity)
|
||||||
|
if isinstance(message, Message):
|
||||||
|
if (message.media
|
||||||
|
and not isinstance(message.media, MessageMediaWebPage)):
|
||||||
|
return await 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 = await self._parse_message_text(message, parse_mode)
|
||||||
request = SendMessageRequest(
|
request = SendMessageRequest(
|
||||||
peer=entity,
|
peer=entity,
|
||||||
message=message,
|
message=message,
|
||||||
entities=msg_entities,
|
entities=msg_ent,
|
||||||
no_webpage=not link_preview,
|
no_webpage=not link_preview,
|
||||||
reply_to_msg_id=self._get_message_id(reply_to)
|
reply_to_msg_id=self._get_message_id(reply_to)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = 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,6 +1231,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'is_voice_note' in kwargs:
|
if 'is_voice_note' in kwargs:
|
||||||
|
if DocumentAttributeAudio in attr_dict:
|
||||||
|
attr_dict[DocumentAttributeAudio].voice = True
|
||||||
|
else:
|
||||||
attr_dict[DocumentAttributeAudio] = \
|
attr_dict[DocumentAttributeAudio] = \
|
||||||
DocumentAttributeAudio(0, voice=True)
|
DocumentAttributeAudio(0, voice=True)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
kind = 'audio'
|
||||||
|
if attr.performer and attr.title:
|
||||||
possible_names.append('{} - {}'.format(
|
possible_names.append('{} - {}'.format(
|
||||||
attr.performer, attr.title
|
attr.performer, attr.title
|
||||||
))
|
))
|
||||||
|
elif attr.performer:
|
||||||
|
possible_names.append(attr.performer)
|
||||||
|
elif attr.title:
|
||||||
|
possible_names.append(attr.title)
|
||||||
|
elif attr.voice:
|
||||||
|
kind = 'voice'
|
||||||
|
|
||||||
file = self._get_proper_filename(
|
file = self._get_proper_filename(
|
||||||
file, 'document', utils.get_extension(document),
|
file, kind, utils.get_extension(document),
|
||||||
date=date, possible_names=possible_names
|
date=date, possible_names=possible_names
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
return username, True
|
||||||
else:
|
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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import os
|
import os
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
|
|
||||||
from telethon import TelegramClient, ConnectionMode
|
from telethon.utils import get_display_name
|
||||||
|
|
||||||
|
from telethon import ConnectionMode, TelegramClient
|
||||||
from telethon.errors import SessionPasswordNeededError
|
from telethon.errors import SessionPasswordNeededError
|
||||||
from telethon.tl.types import (
|
from telethon.tl.types import (
|
||||||
UpdateShortChatMessage, UpdateShortMessage, PeerChat
|
PeerChat, UpdateShortChatMessage, UpdateShortMessage
|
||||||
)
|
)
|
||||||
from telethon.utils import get_display_name
|
|
||||||
|
|
||||||
|
|
||||||
def sprint(string, *args, **kwargs):
|
def sprint(string, *args, **kwargs):
|
||||||
|
@ -47,6 +48,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
Telegram through Telethon, such as listing dialogs (open chats),
|
Telegram through Telethon, such as listing dialogs (open chats),
|
||||||
talking to people, downloading media, and receiving updates.
|
talking to people, downloading media, and receiving updates.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session_user_id, user_phone, api_id, api_hash,
|
def __init__(self, session_user_id, user_phone, api_id, api_hash,
|
||||||
proxy=None):
|
proxy=None):
|
||||||
"""
|
"""
|
||||||
|
@ -190,6 +192,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
print(' !d <msg-id>: Deletes a message by its id')
|
print(' !d <msg-id>: Deletes a message by its id')
|
||||||
print(' !dm <msg-id>: Downloads the given message Media (if any).')
|
print(' !dm <msg-id>: Downloads the given message Media (if any).')
|
||||||
print(' !dp: Downloads the current dialog Profile picture.')
|
print(' !dp: Downloads the current dialog Profile picture.')
|
||||||
|
print(' !i: Prints information about this chat..')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# And start a while loop to chat
|
# And start a while loop to chat
|
||||||
|
@ -234,8 +237,7 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
|
|
||||||
# And print it to the user
|
# And print it to the user
|
||||||
sprint('[{}:{}] (ID={}) {}: {}'.format(
|
sprint('[{}:{}] (ID={}) {}: {}'.format(
|
||||||
msg.date.hour, msg.date.minute, msg.id, name,
|
msg.date.hour, msg.date.minute, msg.id, name, content))
|
||||||
content))
|
|
||||||
|
|
||||||
# Send photo
|
# Send photo
|
||||||
elif msg.startswith('!up '):
|
elif msg.startswith('!up '):
|
||||||
|
@ -264,12 +266,16 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
os.makedirs('usermedia', exist_ok=True)
|
os.makedirs('usermedia', exist_ok=True)
|
||||||
output = self.download_profile_photo(entity, 'usermedia')
|
output = self.download_profile_photo(entity, 'usermedia')
|
||||||
if output:
|
if output:
|
||||||
print(
|
print('Profile picture downloaded to', output)
|
||||||
'Profile picture downloaded to {}'.format(output)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print('No profile picture found for this user!')
|
print('No profile picture found for this user!')
|
||||||
|
|
||||||
|
elif msg == '!i':
|
||||||
|
attributes = list(entity.to_dict().items())
|
||||||
|
pad = max(len(x) for x, _ in attributes)
|
||||||
|
for name, val in attributes:
|
||||||
|
print("{:<{width}} : {}".format(name, val, width=pad))
|
||||||
|
|
||||||
# Send chat message (if any)
|
# Send chat message (if any)
|
||||||
elif msg:
|
elif msg:
|
||||||
self.send_message(entity, msg, link_preview=False)
|
self.send_message(entity, msg, link_preview=False)
|
||||||
|
@ -356,6 +362,5 @@ class InteractiveTelegramClient(TelegramClient):
|
||||||
else:
|
else:
|
||||||
who = self.get_entity(update.from_id)
|
who = self.get_entity(update.from_id)
|
||||||
sprint('<< {} @ {} sent "{}"'.format(
|
sprint('<< {} @ {} sent "{}"'.format(
|
||||||
get_display_name(which), get_display_name(who),
|
get_display_name(which), get_display_name(who), update.message
|
||||||
update.message
|
|
||||||
))
|
))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
|
||||||
words = re.split('\W+', msg)
|
|
||||||
for trigger, response in REACTS.items():
|
|
||||||
if len(recent_reacts[update.user_id]) > 3:
|
|
||||||
# Silently ignore triggers if we've recently sent 3 reactions
|
|
||||||
break
|
|
||||||
|
|
||||||
if trigger in words:
|
|
||||||
# Send a reaction
|
|
||||||
client.send_message(update.user_id, response, reply_to=update.id)
|
|
||||||
# Add this reaction to the list of recent reactions
|
|
||||||
recent_reacts[update.user_id].append(datetime.now())
|
|
||||||
|
|
||||||
# Automatically send relevant media when we say certain things
|
# Automatically send relevant media when we say certain things
|
||||||
# When invoking requests, get_input_entity needs to be called manually
|
# When invoking requests, get_input_entity needs to be called manually
|
||||||
if isinstance(update, UpdateNewChannelMessage) and msg.out:
|
if event.out:
|
||||||
if msg.message.lower() == 'x files theme':
|
if event.raw_text.lower() == 'x files theme':
|
||||||
client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id)
|
client.send_voice_note(event.message.to_id, 'xfiles.m4a',
|
||||||
if msg.message.lower() == 'anytime':
|
reply_to=event.message.id)
|
||||||
client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id)
|
if event.raw_text.lower() == 'anytime':
|
||||||
if '.shrug' in msg.message:
|
client.send_file(event.message.to_id, 'anytime.png',
|
||||||
client(EditMessageRequest(
|
reply_to=event.message.id)
|
||||||
client.get_input_entity(msg.to_id), msg.id,
|
if '.shrug' in event.text:
|
||||||
message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯')
|
event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯'))
|
||||||
))
|
|
||||||
|
|
||||||
if isinstance(update, UpdateShortMessage) and update.out:
|
if 'TG_PHONE' in environ:
|
||||||
if msg.lower() == 'x files theme':
|
client.start(phone=environ['TG_PHONE'])
|
||||||
client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id)
|
else:
|
||||||
if msg.lower() == 'anytime':
|
client.start()
|
||||||
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'¯\_(ツ)_/¯')
|
|
||||||
))
|
|
||||||
|
|
||||||
|
print('(Press Ctrl+C to stop this)')
|
||||||
if __name__ == '__main__':
|
client.idle()
|
||||||
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()
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user