From f507f508a2aa56305ac90bd222f49af9cf47c49b Mon Sep 17 00:00:00 2001 From: NCPlayz Date: Thu, 14 Mar 2019 12:38:02 +0000 Subject: [PATCH] Expose Metadata Added access to: * `/users/@me/guilds` * `/guilds/{guild_id}` * `/guilds/{guild_id}/members/{member_id}` BREAKING CHANGE: * `get_user_info` -> `fetch_user_info` to match naming scheme. Remove useless note Remove `reverse` and corresponding documentation Update documentation to reflect #1988 Rename `get_` HTTP functions to `fetch_` Breaking Changes: * `get_message` -> `fetch_message` * `get_invite` -> `fetch_invite` * `get_user_profile` -> `fetch_user_profile` * `get_webhook_info` -> `fetch_webhook` * `get_ban` -> `fetch_ban` Fix InviteConverter, update migrating.rst Rename get_message to fetch_message --- discord/abc.py | 2 +- discord/client.py | 81 +++++++++++++++++- discord/ext/commands/converter.py | 4 +- discord/guild.py | 27 +++++- discord/http.py | 18 ++++ discord/iterators.py | 132 +++++++++++++++++++++++++++++- docs/migrating.rst | 6 +- 7 files changed, 260 insertions(+), 10 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 3ac9a82e0..97383573c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -816,7 +816,7 @@ class Messageable(metaclass=abc.ABCMeta): """ return Typing(self) - async def get_message(self, id): + async def fetch_message(self, id): """|coro| Retrieves a single :class:`.Message` from the destination. diff --git a/discord/client.py b/discord/client.py index a5cec8318..14b3e3384 100644 --- a/discord/client.py +++ b/discord/client.py @@ -39,6 +39,7 @@ from .user import User, Profile from .invite import Invite from .object import Object from .guild import Guild +from .member import Member from .errors import * from .enums import Status, VoiceRegion from .gateway import * @@ -49,6 +50,7 @@ from .state import ConnectionState from . import utils from .backoff import ExponentialBackoff from .webhook import Webhook +from .iterators import GuildIterator log = logging.getLogger(__name__) @@ -841,6 +843,77 @@ class Client: # Guild stuff + def fetch_guilds(self, *, limit=100, before=None, after=None): + """|coro| + + Retreives an :class:`AsyncIterator` that enables receiving your guilds. + + All parameters are optional. + + Parameters + ----------- + limit: Optional[:class:`int`] + The number of guilds to retrieve. + If ``None``, it retrieves every guild you have access to. Note, however, + that this would make it a slow operation. + Defaults to 100. + before: :class:`Snowflake` or `datetime` + Retrieves guilds before this date or object. + If a date is provided it must be a timezone-naive datetime representing UTC time. + after: :class:`Snowflake` or `datetime` + Retrieve guilds after this date or object. + If a date is provided it must be a timezone-naive datetime representing UTC time. + + Raises + ------ + HTTPException + Getting the guilds failed. + + Yields + -------- + :class:`Guild` + The guild with the guild data parsed. + + Examples + --------- + + Usage :: + + async for guild in client.fetch_guilds(limit=150): + print(guild.name) + + Flattening into a list :: + + guilds = await client.fetch_guilds(limit=150).flatten() + # guilds is now a list of Guild... + """ + return GuildIterator(self, limit=limit, before=before, after=after) + + async def fetch_guild(self, guild_id): + """|coro| + + Retreives a :class:`Guild` from an ID. + + Parameters + ----------- + guild_id: :class:`int` + The guild's ID to fetch from. + + Raises + ------ + Forbidden + You do not have access to the guild. + HTTPException + Getting the guild failed. + + Returns + -------- + :class:`Guild` + The guild from the ID. + """ + data = await self.http.get_guild(guild_id) + return Guild(data=data, state=self._connection) + async def create_guild(self, name, region=None, icon=None): """|coro| @@ -885,7 +958,7 @@ class Client: # Invite management - async def get_invite(self, url, *, with_counts=True): + async def fetch_invite(self, url, *, with_counts=True): """|coro| Gets an :class:`Invite` from a discord.gg URL or ID. @@ -974,7 +1047,7 @@ class Client: bot_require_code_grant=data['bot_require_code_grant'], owner=User(state=self._connection, data=data['owner'])) - async def get_user_info(self, user_id): + async def fetch_user(self, user_id): """|coro| Retrieves a :class:`User` based on their ID. This can only @@ -1002,7 +1075,7 @@ class Client: data = await self.http.get_user_info(user_id) return User(state=self._connection, data=data) - async def get_user_profile(self, user_id): + async def fetch_user_profile(self, user_id): """|coro| Gets an arbitrary user's profile. This can only be used by non-bot accounts. @@ -1040,7 +1113,7 @@ class Client: user=User(data=user, state=state), connected_accounts=data['connected_accounts']) - async def get_webhook_info(self, webhook_id): + async def fetch_webhook(self, webhook_id): """|coro| Retrieves a :class:`Webhook` with the specified ID. diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 9fc6cf6d0..81d3a00ed 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -336,11 +336,11 @@ class GameConverter(Converter): class InviteConverter(Converter): """Converts to a :class:`Invite`. - This is done via an HTTP request using :meth:`.Bot.get_invite`. + This is done via an HTTP request using :meth:`.Bot.fetch_invite`. """ async def convert(self, ctx, argument): try: - invite = await ctx.bot.get_invite(argument) + invite = await ctx.bot.fetch_invite(argument) return invite except Exception as exc: raise BadArgument('Invite is invalid or expired') from exc diff --git a/discord/guild.py b/discord/guild.py index fd83acf67..e2d704b3c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -944,7 +944,32 @@ class Guild(Hashable): fields['explicit_content_filter'] = explicit_content_filter.value await http.edit_guild(self.id, reason=reason, **fields) - async def get_ban(self, user): + async def fetch_member(self, member_id): + """|coro| + + Retreives a :class:`Member` from a guild ID, and a member ID. + + Parameters + ----------- + member_id: :class:`int` + The member's ID to fetch from. + + Raises + ------- + Forbidden + You do not have access to the guild. + HTTPException + Getting the guild failed. + + Returns + -------- + :class:`Member` + The member from the member ID. + """ + data = await self._state.http.get_member(self.id, member_id) + return Member(data=data, state=self._state, guild=self) + + async def fetch_ban(self, user): """|coro| Retrieves the :class:`BanEntry` for a user, which is a namedtuple diff --git a/discord/http.py b/discord/http.py index 7e34951f6..72e9f8ec2 100644 --- a/discord/http.py +++ b/discord/http.py @@ -553,9 +553,24 @@ class HTTPClient: # Guild management + def get_guilds(self, limit, before=None, after=None): + params = { + 'limit': limit + } + + if before: + params['before'] = before + if after: + params['after'] = after + + return self.request(Route('GET', '/users/@me/guilds'), params=params) + def leave_guild(self, guild_id): return self.request(Route('DELETE', '/users/@me/guilds/{guild_id}', guild_id=guild_id)) + def get_guild(self, guild_id): + return self.request(Route('GET', '/guilds/{guild_id}', guild_id=guild_id)) + def delete_guild(self, guild_id): return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) @@ -593,6 +608,9 @@ class HTTPClient: payload = {'code': code} return self.request(Route('PATCH', '/guilds/{guild_id}/vanity-url', guild_id=guild_id), json=payload, reason=reason) + def get_member(self, guild_id, member_id): + return self.request(Route('GET', '/guilds/{guild_id}/members/{member_id}', guild_id=guild_id, member_id=member_id)) + def prune_members(self, guild_id, days, *, reason=None): params = { 'days': days diff --git a/discord/iterators.py b/discord/iterators.py index d77592a52..015620bca 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -237,7 +237,7 @@ class HistoryIterator(_AsyncIterator): elif self.limit == 101: self.limit = 100 # Thanks discord elif self.limit == 1: - raise ValueError("Use get_message.") + raise ValueError("Use fetch_message.") self._retrieve_messages = self._retrieve_messages_around_strategy if self.before and self.after: @@ -459,3 +459,133 @@ class AuditLogIterator(_AsyncIterator): continue await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild)) + + +class GuildIterator(_AsyncIterator): + """Iterator for receiving the client's guilds. + + The guilds endpoint has the same two behaviours as described + in :class:`HistoryIterator`: + If `before` is specified, the guilds endpoint returns the `limit` + newest guilds before `before`, sorted with newest first. For filling over + 100 guilds, update the `before` parameter to the oldest guild received. + Guilds will be returned in order by time. + If `after` is specified, it returns the `limit` oldest guilds after `after`, + sorted with newest first. For filling over 100 guilds, update the `after` + parameter to the newest guild received, If guilds are not reversed, they + will be out of order (99-0, 199-100, so on) + + Not that if both before and after are specified, before is ignored by the + guilds endpoint. + + Parameters + ----------- + bot: :class:`discord.Client` + The client to retrieve the guilds from. + limit: :class:`int` + Maximum number of guilds to retrieve. + before: :class:`Snowflake` + Object before which all guilds must be. + after: :class:`Snowflake` + Object after which all guilds must be. + """ + def __init__(self, bot, limit, before=None, after=None): + + if isinstance(before, datetime.datetime): + before = Object(id=time_snowflake(before, high=False)) + if isinstance(after, datetime.datetime): + after = Object(id=time_snowflake(after, high=True)) + + self.bot = bot + self.limit = limit + self.before = before + self.after = after + + self._filter = None + + self.state = self.bot._connection + self.get_guilds = self.bot.http.get_guilds + self.guilds = asyncio.Queue(loop=self.state.loop) + + if self.before and self.after: + self._retrieve_guilds = self._retrieve_guilds_before_strategy + self._filter = lambda m: int(m['id']) > self.after.id + elif self.after: + self._retrieve_guilds = self._retrieve_guilds_after_strategy + else: + self._retrieve_guilds = self._retrieve_guilds_before_strategy + + async def next(self): + if self.guilds.empty(): + await self.fill_guilds() + + try: + return self.guilds.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + def _get_retrieve(self): + l = self.limit + if l is None: + r = 100 + elif l <= 100: + r = l + else: + r = 100 + + self.retrieve = r + return r > 0 + + def create_guild(self, data): + from .guild import Guild + return Guild(state=self.state, data=data) + + async def flatten(self): + result = [] + while self._get_retrieve(): + data = await self._retrieve_guilds(self.retrieve) + if len(data) < 100: + self.limit = 0 + + if self._filter: + data = filter(self._filter, data) + + for element in data: + result.append(self.create_guild(element)) + return result + + async def fill_guilds(self): + if self._get_retrieve(): + data = await self._retrieve_guilds(self.retrieve) + if self.limit is None or len(data) < 100: + self.limit = 0 + + if self._filter: + data = filter(self._filter, data) + + for element in data: + await self.guilds.put(self.create_guild(element)) + + async def _retrieve_guilds(self, retrieve): + """Retrieve guilds and update next parameters.""" + pass + + async def _retrieve_guilds_before_strategy(self, retrieve): + """Retrieve guilds using before parameter.""" + before = self.before.id if self.before else None + data = await self.get_guilds(retrieve, before=before) + if len(data): + if self.limit is not None: + self.limit -= retrieve + self.before = Object(id=int(data[-1]['id'])) + return data + + async def _retrieve_guilds_after_strategy(self, retrieve): + """Retrieve guilds using after parameter.""" + after = self.after.id if self.after else None + data = await self.get_guilds(retrieve, after=after) + if len(data): + if self.limit is not None: + self.limit -= retrieve + self.after = Object(id=int(data[0]['id'])) + return data diff --git a/docs/migrating.rst b/docs/migrating.rst index f03b08b09..9ea0e5f0a 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -148,10 +148,14 @@ A list of these changes is enumerated below. +---------------------------------------+------------------------------------------------------------------------------+ | ``Client.get_bans`` | :meth:`Guild.bans` | +---------------------------------------+------------------------------------------------------------------------------+ -| ``Client.get_message`` | :meth:`abc.Messageable.get_message` | +| ``Client.get_invite`` | :meth:`Client.fetch_invite` | ++---------------------------------------+------------------------------------------------------------------------------+ +| ``Client.get_message`` | :meth:`abc.Messageable.fetch_message` | +---------------------------------------+------------------------------------------------------------------------------+ | ``Client.get_reaction_users`` | :meth:`Reaction.users` | +---------------------------------------+------------------------------------------------------------------------------+ +| ``Client.get_user_info`` | :meth:`Client.fetch_user` | ++---------------------------------------+------------------------------------------------------------------------------+ | ``Client.invites_from`` | :meth:`abc.GuildChannel.invites` or :meth:`Guild.invites` | +---------------------------------------+------------------------------------------------------------------------------+ | ``Client.join_voice_channel`` | :meth:`VoiceChannel.connect` (see :ref:`migrating_1_0_voice`) |