diff --git a/telethon/client/chats.py b/telethon/client/chats.py new file mode 100644 index 00000000..d0b0d2e9 --- /dev/null +++ b/telethon/client/chats.py @@ -0,0 +1,184 @@ +from collections import UserList + +from .users import UserMethods +from .. import utils +from ..tl import types, functions + + +class ChatMethods(UserMethods): + + # region Public methods + + async def iter_participants( + self, entity, limit=None, search='', + filter=None, aggressive=False, _total=None): + """ + Iterator over the participants belonging to the specified chat. + + Args: + entity (`entity`): + The entity from which to retrieve the participants list. + + limit (`int`): + Limits amount of participants fetched. + + search (`str`, optional): + Look for participants with this string in name/username. + + filter (:tl:`ChannelParticipantsFilter`, optional): + The filter to be used, if you want e.g. only admins + Note that you might not have permissions for some filter. + This has no effect for normal chats or users. + + aggressive (`bool`, optional): + Aggressively looks for all participants in the chat in + order to get more than 10,000 members (a hard limit + imposed by Telegram). Note that this might take a long + time (over 5 minutes), but is able to return over 90,000 + participants on groups with 100,000 members. + + This has no effect for groups or channels with less than + 10,000 members, or if a ``filter`` is given. + + _total (`list`, optional): + A single-item list to pass the total parameter by reference. + + Yields: + The :tl:`User` objects returned by :tl:`GetParticipantsRequest` + with an additional ``.participant`` attribute which is the + matched :tl:`ChannelParticipant` type for channels/megagroups + or :tl:`ChatParticipants` for normal chats. + """ + if isinstance(filter, type): + if filter in (types.ChannelParticipantsBanned, + types.ChannelParticipantsKicked, + types.ChannelParticipantsSearch): + # These require a `q` parameter (support types for convenience) + filter = filter('') + else: + filter = filter() + + entity = await self.get_input_entity(entity) + if search and (filter + or not isinstance(entity, types.InputPeerChannel)): + # We need to 'search' ourselves unless we have a PeerChannel + search = search.lower() + + def filter_entity(ent): + return search in utils.get_display_name(ent).lower() or\ + search in (getattr(ent, 'username', '') or None).lower() + else: + def filter_entity(ent): + return True + + limit = float('inf') if limit is None else int(limit) + if isinstance(entity, types.InputPeerChannel): + if _total or (aggressive and not filter): + total = (await self(functions.channels.GetFullChannelRequest( + entity + ))).full_chat.participants_count + if _total: + _total[0] = total + else: + total = 0 + + if limit == 0: + return + + seen = set() + if total > 10000 and aggressive and not filter: + requests = [functions.channels.GetParticipantsRequest( + channel=entity, + filter=types.ChannelParticipantsSearch(search + chr(x)), + offset=0, + limit=200, + hash=0 + ) for x in range(ord('a'), ord('z') + 1)] + else: + requests = [functions.channels.GetParticipantsRequest( + channel=entity, + filter=filter or types.ChannelParticipantsSearch(search), + offset=0, + limit=200, + hash=0 + )] + + while requests: + # Only care about the limit for the first request + # (small amount of people, won't be aggressive). + # + # Most people won't care about getting exactly 12,345 + # members so it doesn't really matter not to be 100% + # precise with being out of the offset/limit here. + requests[0].limit = min(limit - requests[0].offset, 200) + if requests[0].offset > limit: + break + + results = await self(requests) + for i in reversed(range(len(requests))): + participants = results[i] + if not participants.users: + requests.pop(i) + else: + requests[i].offset += len(participants.participants) + users = {user.id: user for user in participants.users} + for participant in participants.participants: + user = users[participant.user_id] + if not filter_entity(user) or user.id in seen: + continue + + seen.add(participant.user_id) + user = users[participant.user_id] + user.participant = participant + yield user + if len(seen) >= limit: + return + + elif isinstance(entity, types.InputPeerChat): + # TODO We *could* apply the `filter` here ourselves + full = await self( + functions.messages.GetFullChatRequest(entity.chat_id)) + if not isinstance( + full.full_chat.participants, types.ChatParticipants): + # ChatParticipantsForbidden won't have ``.participants`` + _total[0] = 0 + return + + if _total: + _total[0] = len(full.full_chat.participants.participants) + + have = 0 + users = {user.id: user for user in full.users} + for participant in full.full_chat.participants.participants: + user = users[participant.user_id] + if not filter_entity(user): + continue + have += 1 + if have > limit: + break + else: + user = users[participant.user_id] + user.participant = participant + yield user + else: + if _total: + _total[0] = 1 + if limit != 0: + user = await self.get_entity(entity) + if filter_entity(user): + user.participant = None + yield user + + async def get_participants(self, *args, **kwargs): + """ + Same as :meth:`iter_participants`, but returns a list instead + with an additional ``.total`` attribute on the list. + """ + total = [0] + kwargs['_total'] = total + participants = UserList(x async for x in + self.iter_participants(*args, **kwargs)) + participants.total = total[0] + return participants + + # endregion diff --git a/telethon/client/telegramclient.py b/telethon/client/telegramclient.py index c0aaf372..5c90a8e4 100644 --- a/telethon/client/telegramclient.py +++ b/telethon/client/telegramclient.py @@ -1,22 +1,15 @@ import getpass import hashlib import io -import itertools import logging -import re import sys -import time import warnings -from collections import UserList -from io import BytesIO -from mimetypes import guess_type from ..crypto import CdnDecrypter -from ..tl.custom import InputSizedFile from ..tl.functions.help import AcceptTermsOfServiceRequest from ..tl.functions.updates import GetDifferenceRequest from ..tl.functions.upload import ( - SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest + GetFileRequest ) from ..tl.types.updates import ( DifferenceSlice, DifferenceEmpty, Difference, DifferenceTooLong @@ -43,7 +36,6 @@ from ..errors import ( SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, PhoneNumberOccupiedError ) -from ..tl.custom import Draft, Dialog from ..tl.functions.account import ( GetPasswordRequest, UpdatePasswordSettingsRequest ) @@ -51,41 +43,19 @@ from ..tl.functions.auth import ( CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest ) -from ..tl.functions.messages import ( - GetDialogsRequest, GetHistoryRequest, SendMediaRequest, - SendMessageRequest, GetAllDraftsRequest, - ReadMentionsRequest, SendMultiMediaRequest, - UploadMediaRequest, EditMessageRequest, GetFullChatRequest, - ForwardMessagesRequest, SearchRequest -) - -from ..tl.functions import channels -from ..tl.functions import messages from ..tl.functions.channels import ( - GetFullChannelRequest, GetParticipantsRequest + GetFullChannelRequest ) from ..tl.types import ( DocumentAttributeAudio, DocumentAttributeFilename, - InputMediaUploadedDocument, InputMediaUploadedPhoto, InputPeerEmpty, Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, - UserProfilePhoto, ChatPhoto, UpdateMessageID, - UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, - PeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - Photo, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, - InputDocument, InputMediaDocument, Document, MessageEntityTextUrl, - InputMessageEntityMentionName, DocumentAttributeVideo, - UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, - MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, - PhotoSizeEmpty, MessageService, ChatParticipants, WebPage, - ChannelParticipantsBanned, ChannelParticipantsKicked, - InputMessagesFilterEmpty, UpdatesCombined + UserProfilePhoto, ChatPhoto, UpdateNewMessage, InputPeerChannel, Photo, + Document, Updates, + MessageMediaWebPage, PhotoSize, PhotoCachedSize, + PhotoSizeEmpty, WebPage ) -from ..tl.types.messages import DialogsSlice, MessagesNotModified from ..tl.types.account import PasswordInputSettings, NoPassword -from ..tl import custom -from ..utils import Default -from ..extensions import markdown, html __log__ = logging.getLogger(__name__) import os @@ -429,180 +399,6 @@ class TelegramClient(TelegramBaseClient): # endregion - # region Dialogs ("chats") requests - - def iter_participants(self, entity, limit=None, search='', - filter=None, aggressive=False, _total=None): - """ - Iterator over the participants belonging to the specified chat. - - Args: - entity (`entity`): - The entity from which to retrieve the participants list. - - limit (`int`): - Limits amount of participants fetched. - - search (`str`, optional): - Look for participants with this string in name/username. - - filter (:tl:`ChannelParticipantsFilter`, optional): - The filter to be used, if you want e.g. only admins - Note that you might not have permissions for some filter. - This has no effect for normal chats or users. - - aggressive (`bool`, optional): - Aggressively looks for all participants in the chat in - order to get more than 10,000 members (a hard limit - imposed by Telegram). Note that this might take a long - time (over 5 minutes), but is able to return over 90,000 - participants on groups with 100,000 members. - - This has no effect for groups or channels with less than - 10,000 members, or if a ``filter`` is given. - - _total (`list`, optional): - A single-item list to pass the total parameter by reference. - - Yields: - The :tl:`User` objects returned by :tl:`GetParticipantsRequest` - with an additional ``.participant`` attribute which is the - matched :tl:`ChannelParticipant` type for channels/megagroups - or :tl:`ChatParticipants` for normal chats. - """ - if isinstance(filter, type): - if filter in (ChannelParticipantsBanned, ChannelParticipantsKicked, - ChannelParticipantsSearch): - # These require a `q` parameter (support types for convenience) - filter = filter('') - else: - filter = filter() - - entity = self.get_input_entity(entity) - if search and (filter or not isinstance(entity, InputPeerChannel)): - # We need to 'search' ourselves unless we have a PeerChannel - search = search.lower() - - def filter_entity(ent): - return search in utils.get_display_name(ent).lower() or\ - search in (getattr(ent, 'username', '') or None).lower() - else: - def filter_entity(ent): - return True - - limit = float('inf') if limit is None else int(limit) - if isinstance(entity, InputPeerChannel): - if _total or (aggressive and not filter): - total = self(GetFullChannelRequest( - entity - )).full_chat.participants_count - if _total: - _total[0] = total - else: - total = 0 - - if limit == 0: - return - - seen = set() - if total > 10000 and aggressive and not filter: - requests = [GetParticipantsRequest( - channel=entity, - filter=ChannelParticipantsSearch(search + chr(x)), - offset=0, - limit=200, - hash=0 - ) for x in range(ord('a'), ord('z') + 1)] - else: - requests = [GetParticipantsRequest( - channel=entity, - filter=filter or ChannelParticipantsSearch(search), - offset=0, - limit=200, - hash=0 - )] - - while requests: - # Only care about the limit for the first request - # (small amount of people, won't be aggressive). - # - # Most people won't care about getting exactly 12,345 - # members so it doesn't really matter not to be 100% - # precise with being out of the offset/limit here. - requests[0].limit = min(limit - requests[0].offset, 200) - if requests[0].offset > limit: - break - - results = self(requests) - for i in reversed(range(len(requests))): - participants = results[i] - if not participants.users: - requests.pop(i) - else: - requests[i].offset += len(participants.participants) - users = {user.id: user for user in participants.users} - for participant in participants.participants: - user = users[participant.user_id] - if not filter_entity(user) or user.id in seen: - continue - - seen.add(participant.user_id) - user = users[participant.user_id] - user.participant = participant - yield user - if len(seen) >= limit: - return - - elif isinstance(entity, InputPeerChat): - # TODO We *could* apply the `filter` here ourselves - full = self(GetFullChatRequest(entity.chat_id)) - if not isinstance(full.full_chat.participants, ChatParticipants): - # ChatParticipantsForbidden won't have ``.participants`` - _total[0] = 0 - return - - if _total: - _total[0] = len(full.full_chat.participants.participants) - - have = 0 - users = {user.id: user for user in full.users} - for participant in full.full_chat.participants.participants: - user = users[participant.user_id] - if not filter_entity(user): - continue - have += 1 - if have > limit: - break - else: - user = users[participant.user_id] - user.participant = participant - yield user - else: - if _total: - _total[0] = 1 - if limit != 0: - user = self.get_entity(entity) - if filter_entity(user): - user.participant = None - yield user - - def get_participants(self, *args, **kwargs): - """ - Same as :meth:`iter_participants`, but returns a list instead - with an additional ``.total`` attribute on the list. - """ - total = [0] - kwargs['_total'] = total - participants = UserList(self.iter_participants(*args, **kwargs)) - participants.total = total[0] - return participants - - # endregion - - # region Uploading files - - # endregion - # region Downloading media requests def download_profile_photo(self, entity, file=None, download_big=True):