2019-05-10 19:10:56 +03:00
|
|
|
import asyncio
|
2019-09-08 11:56:35 +03:00
|
|
|
import inspect
|
2018-06-09 23:09:02 +03:00
|
|
|
import itertools
|
2019-05-03 22:37:27 +03:00
|
|
|
import typing
|
2022-02-07 11:28:39 +03:00
|
|
|
import dataclasses
|
2018-06-09 23:09:02 +03:00
|
|
|
|
2021-09-26 20:58:42 +03:00
|
|
|
from .. import errors, _tl
|
|
|
|
from .._misc import helpers, utils, requestiter, hints
|
2021-09-12 17:58:06 +03:00
|
|
|
from ..types import _custom
|
2018-06-09 23:09:02 +03:00
|
|
|
|
2019-02-27 15:07:25 +03:00
|
|
|
_MAX_CHUNK_SIZE = 100
|
|
|
|
|
2019-05-03 22:37:27 +03:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
from .telegramclient import TelegramClient
|
|
|
|
|
2018-06-09 23:09:02 +03:00
|
|
|
|
2019-07-23 13:44:19 +03:00
|
|
|
def _dialog_message_key(peer, message_id):
|
|
|
|
"""
|
|
|
|
Get the key to get messages from a dialog.
|
|
|
|
|
|
|
|
We cannot just use the message ID because channels share message IDs,
|
|
|
|
and the peer ID is required to distinguish between them. But it is not
|
|
|
|
necessary in small group chats and private chats.
|
|
|
|
"""
|
2021-09-12 13:16:02 +03:00
|
|
|
return (peer.channel_id if isinstance(peer, _tl.PeerChannel) else None), message_id
|
2019-07-23 13:44:19 +03:00
|
|
|
|
|
|
|
|
2021-09-12 14:27:13 +03:00
|
|
|
class _DialogsIter(requestiter.RequestIter):
|
2019-02-27 12:37:40 +03:00
|
|
|
async def _init(
|
2019-06-11 10:55:13 +03:00
|
|
|
self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder
|
2019-02-27 12:37:40 +03:00
|
|
|
):
|
2021-09-12 13:16:02 +03:00
|
|
|
self.request = _tl.fn.messages.GetDialogs(
|
2019-02-27 12:37:40 +03:00
|
|
|
offset_date=offset_date,
|
|
|
|
offset_id=offset_id,
|
|
|
|
offset_peer=offset_peer,
|
|
|
|
limit=1,
|
2019-05-10 16:45:24 +03:00
|
|
|
hash=0,
|
2019-06-11 10:55:13 +03:00
|
|
|
exclude_pinned=ignore_pinned,
|
2019-05-10 16:45:24 +03:00
|
|
|
folder_id=folder
|
2019-02-27 12:37:40 +03:00
|
|
|
)
|
|
|
|
|
2019-02-27 15:01:04 +03:00
|
|
|
if self.limit <= 0:
|
2019-02-27 12:37:40 +03:00
|
|
|
# Special case, get a single dialog and determine count
|
|
|
|
dialogs = await self.client(self.request)
|
|
|
|
self.total = getattr(dialogs, 'count', len(dialogs.dialogs))
|
|
|
|
raise StopAsyncIteration
|
|
|
|
|
|
|
|
self.seen = set()
|
|
|
|
self.offset_date = offset_date
|
|
|
|
self.ignore_migrated = ignore_migrated
|
|
|
|
|
|
|
|
async def _load_next_chunk(self):
|
2022-02-07 11:28:39 +03:00
|
|
|
self.request = dataclasses.replace(self.request, limit=min(self.left, _MAX_CHUNK_SIZE))
|
2019-02-27 12:37:40 +03:00
|
|
|
r = await self.client(self.request)
|
|
|
|
|
|
|
|
self.total = getattr(r, 'count', len(r.dialogs))
|
|
|
|
|
|
|
|
entities = {utils.get_peer_id(x): x
|
2020-11-13 12:59:25 +03:00
|
|
|
for x in itertools.chain(r.users, r.chats)
|
2021-09-12 13:16:02 +03:00
|
|
|
if not isinstance(x, (_tl.UserEmpty, _tl.ChatEmpty))}
|
2019-02-27 12:37:40 +03:00
|
|
|
|
2021-09-13 21:37:29 +03:00
|
|
|
messages = {
|
|
|
|
_dialog_message_key(m.peer_id, m.id): _custom.Message._new(self.client, m, entities, None)
|
|
|
|
for m in r.messages
|
|
|
|
}
|
2019-02-27 12:37:40 +03:00
|
|
|
|
|
|
|
for d in r.dialogs:
|
|
|
|
# We check the offset date here because Telegram may ignore it
|
2019-07-23 13:44:19 +03:00
|
|
|
message = messages.get(_dialog_message_key(d.peer, d.top_message))
|
2019-02-27 12:37:40 +03:00
|
|
|
if self.offset_date:
|
2019-07-23 13:44:19 +03:00
|
|
|
date = getattr(message, 'date', None)
|
2019-02-27 12:37:40 +03:00
|
|
|
if not date or date.timestamp() > self.offset_date.timestamp():
|
|
|
|
continue
|
|
|
|
|
|
|
|
peer_id = utils.get_peer_id(d.peer)
|
|
|
|
if peer_id not in self.seen:
|
|
|
|
self.seen.add(peer_id)
|
2020-11-13 12:59:25 +03:00
|
|
|
if peer_id not in entities:
|
|
|
|
# > In which case can a UserEmpty appear in the list of banned members?
|
|
|
|
# > In a very rare cases. This is possible but isn't an expected behavior.
|
|
|
|
# Real world example: https://t.me/TelethonChat/271471
|
|
|
|
continue
|
|
|
|
|
2021-09-12 17:58:06 +03:00
|
|
|
cd = _custom.Dialog(self.client, d, entities, message)
|
2019-02-27 12:37:40 +03:00
|
|
|
if cd.dialog.pts:
|
|
|
|
self.client._channel_pts[cd.id] = cd.dialog.pts
|
|
|
|
|
|
|
|
if not self.ignore_migrated or getattr(
|
|
|
|
cd.entity, 'migrated_to', None) is None:
|
2019-02-27 13:24:47 +03:00
|
|
|
self.buffer.append(cd)
|
2019-02-27 12:37:40 +03:00
|
|
|
|
|
|
|
if len(r.dialogs) < self.request.limit\
|
2021-09-12 13:16:02 +03:00
|
|
|
or not isinstance(r, _tl.messages.DialogsSlice):
|
2019-02-27 12:37:40 +03:00
|
|
|
# Less than we requested means we reached the end, or
|
|
|
|
# we didn't get a DialogsSlice which means we got all.
|
2019-02-27 13:24:47 +03:00
|
|
|
return True
|
2019-02-27 12:37:40 +03:00
|
|
|
|
2019-06-11 11:04:36 +03:00
|
|
|
# We can't use `messages[-1]` as the offset ID / date.
|
|
|
|
# Why? Because pinned dialogs will mess with the order
|
|
|
|
# in this list. Instead, we find the last dialog which
|
|
|
|
# has a message, and use it as an offset.
|
2019-07-23 13:44:19 +03:00
|
|
|
last_message = next(filter(None, (
|
|
|
|
messages.get(_dialog_message_key(d.peer, d.top_message))
|
2019-06-11 11:04:36 +03:00
|
|
|
for d in reversed(r.dialogs)
|
2019-07-23 13:44:19 +03:00
|
|
|
)), None)
|
2019-06-11 11:04:36 +03:00
|
|
|
|
2022-02-07 11:28:39 +03:00
|
|
|
self.request = dataclasses.replace(
|
|
|
|
self.request,
|
|
|
|
exclude_pinned=True,
|
|
|
|
offset_id=last_message.id if last_message else 0,
|
|
|
|
offset_date=last_message.date if last_message else None,
|
|
|
|
offset_peer=self.buffer[-1].input_entity,
|
|
|
|
)
|
2019-02-27 12:37:40 +03:00
|
|
|
|
|
|
|
|
2021-09-12 14:27:13 +03:00
|
|
|
class _DraftsIter(requestiter.RequestIter):
|
2019-08-01 21:15:32 +03:00
|
|
|
async def _init(self, entities, **kwargs):
|
|
|
|
if not entities:
|
2021-09-12 13:16:02 +03:00
|
|
|
r = await self.client(_tl.fn.messages.GetAllDrafts())
|
2019-08-01 21:15:32 +03:00
|
|
|
items = r.updates
|
|
|
|
else:
|
|
|
|
peers = []
|
|
|
|
for entity in entities:
|
2021-09-12 13:16:02 +03:00
|
|
|
peers.append(_tl.InputDialogPeer(
|
2019-08-01 21:15:32 +03:00
|
|
|
await self.client.get_input_entity(entity)))
|
|
|
|
|
2021-09-12 13:16:02 +03:00
|
|
|
r = await self.client(_tl.fn.messages.GetPeerDialogs(peers))
|
2019-08-01 21:15:32 +03:00
|
|
|
items = r.dialogs
|
2019-06-28 21:34:30 +03:00
|
|
|
|
|
|
|
# TODO Maybe there should be a helper method for this?
|
|
|
|
entities = {utils.get_peer_id(x): x
|
|
|
|
for x in itertools.chain(r.users, r.chats)}
|
|
|
|
|
2019-08-01 21:15:32 +03:00
|
|
|
self.buffer.extend(
|
2021-09-12 17:58:06 +03:00
|
|
|
_custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft)
|
2019-08-01 21:15:32 +03:00
|
|
|
for d in items
|
|
|
|
)
|
2019-02-27 12:37:40 +03:00
|
|
|
|
|
|
|
async def _load_next_chunk(self):
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
2021-09-17 20:35:10 +03:00
|
|
|
def get_dialogs(
|
2021-09-11 14:33:27 +03:00
|
|
|
self: 'TelegramClient',
|
2021-09-26 19:37:09 +03:00
|
|
|
limit: float = (),
|
2021-09-11 14:33:27 +03:00
|
|
|
*,
|
|
|
|
offset_date: 'hints.DateLike' = None,
|
|
|
|
offset_id: int = 0,
|
2021-09-12 13:16:02 +03:00
|
|
|
offset_peer: 'hints.EntityLike' = _tl.InputPeerEmpty(),
|
2021-09-11 14:33:27 +03:00
|
|
|
ignore_pinned: bool = False,
|
|
|
|
ignore_migrated: bool = False,
|
|
|
|
folder: int = None,
|
|
|
|
archived: bool = None
|
|
|
|
) -> _DialogsIter:
|
|
|
|
if archived is not None:
|
|
|
|
folder = 1 if archived else 0
|
|
|
|
|
|
|
|
return _DialogsIter(
|
|
|
|
self,
|
|
|
|
limit,
|
|
|
|
offset_date=offset_date,
|
|
|
|
offset_id=offset_id,
|
|
|
|
offset_peer=offset_peer,
|
|
|
|
ignore_pinned=ignore_pinned,
|
|
|
|
ignore_migrated=ignore_migrated,
|
|
|
|
folder=folder
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-09-17 20:35:10 +03:00
|
|
|
def get_drafts(
|
2021-09-11 14:33:27 +03:00
|
|
|
self: 'TelegramClient',
|
|
|
|
entity: 'hints.EntitiesLike' = None
|
|
|
|
) -> _DraftsIter:
|
2021-09-17 21:04:57 +03:00
|
|
|
limit = None
|
|
|
|
if entity:
|
|
|
|
if not utils.is_list_like(entity):
|
|
|
|
entity = (entity,)
|
|
|
|
limit = len(entity)
|
2021-09-11 14:33:27 +03:00
|
|
|
|
2021-09-17 21:04:57 +03:00
|
|
|
return _DraftsIter(self, limit, entities=entity)
|
2021-09-11 14:33:27 +03:00
|
|
|
|
|
|
|
|
|
|
|
async def edit_folder(
|
|
|
|
self: 'TelegramClient',
|
|
|
|
entity: 'hints.EntitiesLike' = None,
|
|
|
|
folder: typing.Union[int, typing.Sequence[int]] = None,
|
|
|
|
*,
|
|
|
|
unpack=None
|
2021-09-12 13:16:02 +03:00
|
|
|
) -> _tl.Updates:
|
2021-09-11 14:33:27 +03:00
|
|
|
if (entity is None) == (unpack is None):
|
|
|
|
raise ValueError('You can only set either entities or unpack, not both')
|
|
|
|
|
|
|
|
if unpack is not None:
|
2021-09-12 13:16:02 +03:00
|
|
|
return await self(_tl.fn.folders.DeleteFolder(
|
2021-09-11 14:33:27 +03:00
|
|
|
folder_id=unpack
|
|
|
|
))
|
|
|
|
|
|
|
|
if not utils.is_list_like(entity):
|
|
|
|
entities = [await self.get_input_entity(entity)]
|
|
|
|
else:
|
|
|
|
entities = await asyncio.gather(
|
|
|
|
*(self.get_input_entity(x) for x in entity))
|
|
|
|
|
|
|
|
if folder is None:
|
|
|
|
raise ValueError('You must specify a folder')
|
|
|
|
elif not utils.is_list_like(folder):
|
|
|
|
folder = [folder] * len(entities)
|
|
|
|
elif len(entities) != len(folder):
|
|
|
|
raise ValueError('Number of folders does not match number of entities')
|
|
|
|
|
2021-09-12 13:16:02 +03:00
|
|
|
return await self(_tl.fn.folders.EditPeerFolders([
|
|
|
|
_tl.InputFolderPeer(x, folder_id=y)
|
2021-09-11 14:33:27 +03:00
|
|
|
for x, y in zip(entities, folder)
|
|
|
|
]))
|
|
|
|
|
|
|
|
async def delete_dialog(
|
|
|
|
self: 'TelegramClient',
|
|
|
|
entity: 'hints.EntityLike',
|
|
|
|
*,
|
|
|
|
revoke: bool = False
|
|
|
|
):
|
|
|
|
# If we have enough information (`Dialog.delete` gives it to us),
|
|
|
|
# then we know we don't have to kick ourselves in deactivated chats.
|
2021-09-12 13:16:02 +03:00
|
|
|
if isinstance(entity, _tl.Chat):
|
2021-09-11 14:33:27 +03:00
|
|
|
deactivated = entity.deactivated
|
|
|
|
else:
|
|
|
|
deactivated = False
|
|
|
|
|
|
|
|
entity = await self.get_input_entity(entity)
|
|
|
|
ty = helpers._entity_type(entity)
|
|
|
|
if ty == helpers._EntityType.CHANNEL:
|
2021-09-12 13:16:02 +03:00
|
|
|
return await self(_tl.fn.channels.LeaveChannel(entity))
|
2021-09-11 14:33:27 +03:00
|
|
|
|
|
|
|
if ty == helpers._EntityType.CHAT and not deactivated:
|
|
|
|
try:
|
2021-09-12 13:16:02 +03:00
|
|
|
result = await self(_tl.fn.messages.DeleteChatUser(
|
|
|
|
entity.chat_id, _tl.InputUserSelf(), revoke_history=revoke
|
2019-05-10 19:10:56 +03:00
|
|
|
))
|
2021-09-24 21:07:34 +03:00
|
|
|
except errors.PEER_ID_INVALID:
|
2021-09-11 14:33:27 +03:00
|
|
|
# Happens if we didn't have the deactivated information
|
2019-05-30 14:58:05 +03:00
|
|
|
result = None
|
2021-09-11 14:33:27 +03:00
|
|
|
else:
|
|
|
|
result = None
|
|
|
|
|
|
|
|
if not await self.is_bot():
|
2021-09-12 13:16:02 +03:00
|
|
|
await self(_tl.fn.messages.DeleteHistory(entity, 0, revoke=revoke))
|
2021-09-11 14:33:27 +03:00
|
|
|
|
|
|
|
return result
|