From 1890a4c06df93b162b715ed7eeb8617243699e1b Mon Sep 17 00:00:00 2001 From: endercheif Date: Tue, 7 Dec 2021 19:56:00 -0800 Subject: [PATCH 1/8] :sparkles: Added all user and current user methods --- pincer/client.py | 263 ++++++++++++++++++++++++-------- pincer/objects/guild/channel.py | 3 + 2 files changed, 202 insertions(+), 64 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index 61c2e95e..1d94dbb6 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -16,15 +16,26 @@ from .core import HTTPClient from .core.gateway import Dispatcher from .exceptions import ( - InvalidEventName, TooManySetupArguments, NoValidSetupMethod, - NoCogManagerReturnFound, CogAlreadyExists, CogNotFound + InvalidEventName, + TooManySetupArguments, + NoValidSetupMethod, + NoCogManagerReturnFound, + CogAlreadyExists, + CogNotFound, ) from .middleware import middleware from .objects import ( - Role, Channel, DefaultThrottleHandler, User, Guild, Intents, - GuildTemplate + Role, + Channel, + DefaultThrottleHandler, + User, + Guild, + Intents, + GuildTemplate, + Connection, ) -from .utils.conversion import construct_client_dict +from .objects.guild.channel import GroupDMChannel +from .utils.conversion import construct_client_dict, remove_none from .utils.event_mgr import EventMgr from .utils.extraction import get_index from .utils.insertion import should_pass_cls @@ -39,6 +50,8 @@ from .objects.app.throttling import ThrottleInterface from .objects.guild import Webhook + from collections.abc import AsyncIterator + _log = logging.getLogger(__package__) MiddlewareType = Optional[Union[Coro, Tuple[str, List[Any], Dict[str, Any]]]] @@ -103,7 +116,8 @@ def decorator(func: Coro): if override: _log.warning( "Middleware overriding has been enabled for `%s`." - " This might cause unexpected behavior.", call + " This might cause unexpected behavior.", + call, ) if not override and callable(_events.get(call)): @@ -116,9 +130,7 @@ async def wrapper(cls, payload: GatewayDispatch): _log.debug("`%s` middleware has been invoked", call) return await ( - func(cls, payload) - if should_pass_cls(func) - else func(payload) + func(cls, payload) if should_pass_cls(func) else func(payload) ) _events[call] = wrapper @@ -165,12 +177,13 @@ class Client(Dispatcher): """ def __init__( - self, - token: str, *, - received: str = None, - intents: Intents = None, - throttler: ThrottleInterface = DefaultThrottleHandler, - reconnect: bool = True, + self, + token: str, + *, + received: str = None, + intents: Intents = None, + throttler: ThrottleInterface = DefaultThrottleHandler, + reconnect: bool = True, ): super().__init__( token, @@ -178,7 +191,7 @@ def __init__( # Gets triggered on all events -1: self.payload_event_handler, # Use this event handler for opcode 0. - 0: self.event_handler + 0: self.event_handler, }, intents=intents or Intents.NONE, reconnect=reconnect, @@ -202,10 +215,9 @@ def chat_commands(self) -> List[str]: Get a list of chat command calls which have been registered in the :class:`~pincer.commands.ChatCommandHandler`\\. """ - return list(map( - lambda cmd: cmd.app.name, - ChatCommandHandler.register.values() - )) + return list( + map(lambda cmd: cmd.app.name, ChatCommandHandler.register.values()) + ) @staticmethod def event(coroutine: Coro): @@ -256,8 +268,9 @@ async def on_ready(self): InvalidEventName If the function name is not a valid event (on_x) """ - if not iscoroutinefunction(coroutine) \ - and not isasyncgenfunction(coroutine): + if not iscoroutinefunction(coroutine) and not isasyncgenfunction( + coroutine + ): raise TypeError( "Any event which is registered must be a coroutine function" ) @@ -289,10 +302,17 @@ def get_event_coro(name: str) -> List[Optional[Coro]]: """ calls = _events.get(name.strip().lower()) - return [] if not calls else list(filter( - lambda call: iscoroutinefunction(call) or isasyncgenfunction(call), - calls - )) + return ( + [] + if not calls + else list( + filter( + lambda call: iscoroutinefunction(call) + or isasyncgenfunction(call), + calls, + ) + ) + ) def load_cog(self, path: str, package: Optional[str] = None): """Load a cog from a string path, setup method in COG may @@ -433,7 +453,7 @@ def execute_event(calls: List[Coro], *args, **kwargs): if should_pass_cls(call): call_args = ( ChatCommandHandler.managers[call.__module__], - *(arg for arg in args if arg is not None) + *(arg for arg in args if arg is not None), ) ensure_future(call(*call_args, **kwargs)) @@ -444,15 +464,11 @@ def run(self): def __del__(self): """Ensure close of the http client.""" - if hasattr(self, 'http'): + if hasattr(self, "http"): run(self.http.close()) async def handle_middleware( - self, - payload: GatewayDispatch, - key: str, - *args, - **kwargs + self, payload: GatewayDispatch, key: str, *args, **kwargs ) -> Tuple[Optional[Coro], List[Any], Dict[str, Any]]: """|coro| @@ -504,11 +520,7 @@ async def handle_middleware( ) async def execute_error( - self, - error: Exception, - name: str = "on_error", - *args, - **kwargs + self, error: Exception, name: str = "on_error", *args, **kwargs ): """|coro| @@ -605,7 +617,7 @@ async def create_guild( afk_channel_id: Optional[Snowflake] = None, afk_timeout: Optional[int] = None, system_channel_id: Optional[Snowflake] = None, - system_channel_flags: Optional[int] = None + system_channel_flags: Optional[int] = None, ) -> Guild: """Creates a guild. @@ -646,7 +658,7 @@ async def create_guild( async def create_guild(self, name: str, **kwargs) -> Guild: g = await self.http.post("guilds", data={"name": name, **kwargs}) - return await self.get_guild(g['id']) + return await self.get_guild(g["id"]) async def get_guild_template(self, code: str) -> GuildTemplate: """|coro| @@ -664,16 +676,12 @@ async def get_guild_template(self, code: str) -> GuildTemplate: """ return GuildTemplate.from_dict( construct_client_dict( - self, - await self.http.get(f"guilds/templates/{code}") + self, await self.http.get(f"guilds/templates/{code}") ) ) async def create_guild_from_template( - self, - template: GuildTemplate, - name: str, - icon: Optional[str] = None + self, template: GuildTemplate, name: str, icon: Optional[str] = None ) -> Guild: """|coro| Creates a guild from a template. @@ -697,16 +705,16 @@ async def create_guild_from_template( self, await self.http.post( f"guilds/templates/{template.code}", - data={"name": name, "icon": icon} - ) + data={"name": name, "icon": icon}, + ), ) ) async def wait_for( - self, - event_name: str, - check: CheckFunction = None, - timeout: Optional[float] = None + self, + event_name: str, + check: CheckFunction = None, + timeout: Optional[float] = None, ): """ Parameters @@ -727,11 +735,11 @@ async def wait_for( return await self.event_mgr.wait_for(event_name, check, timeout) def loop_for( - self, - event_name: str, - check: CheckFunction = None, - iteration_timeout: Optional[float] = None, - loop_timeout: Optional[float] = None + self, + event_name: str, + check: CheckFunction = None, + iteration_timeout: Optional[float] = None, + loop_timeout: Optional[float] = None, ): """ Parameters @@ -753,10 +761,7 @@ def loop_for( What the Discord API returns for this event. """ return self.event_mgr.loop_for( - event_name, - check, - iteration_timeout, - loop_timeout + event_name, check, iteration_timeout, loop_timeout ) async def get_guild(self, guild_id: int) -> Guild: @@ -835,9 +840,7 @@ async def get_channel(self, _id: int) -> Channel: return await Channel.from_id(self, _id) async def get_webhook( - self, - id: Snowflake, - token: Optional[str] = None + self, id: Snowflake, token: Optional[str] = None ) -> Webhook: """|coro| Fetch a Webhook from its identifier. @@ -857,4 +860,136 @@ async def get_webhook( """ return await Webhook.from_id(self, id, token) + async def get_current_user(self) -> User: + """|coro| + The user object of the requester's account. + + For OAuth2, this requires the ``identify`` scope, + which will return the object *without* an email, + and optionally the ``email`` scope, + which returns the object *with* an email. + + Returns + ------- + :class:`~pincer.objects.user.user.User` + """ + return User.from_dict( + construct_client_dict(self, self.http.get("users/@me")) + ) + + async def modify_current_user( + self, username: Optional[str] = None, avatar: Optional = None + ) -> User: + """|coro| + Modify the requester's user account settings + + Parameters + ---------- + username : Optional[:class:`str`] + user's username, + if changed may cause the user's discriminator to be randomized. + |default| :data:`None` + avatar : Optional[:class:``] + if passed, modifies the user's avatar |default| :data:`None` + + Returns + ------- + :class:`~pincer.objects.user.user.User` + Current modified user + """ + + user = await self.http.patch( + "users/@me", remove_none({"username": username, "avatar": avatar}) + ) + return User.from_dict(construct_client_dict(self, user)) + + async def get_current_user_guilds( + self, + before: Optional[Snowflake] = None, + after: Optional[Snowflake] = None, + limit: Optional[int] = None, + ) -> AsyncIterator[Guild]: + """|coro| + Returns a list of partial guild objects the current user is a member of. + Requires the ``guilds`` OAuth2 scope. + + Parameters + ---------- + before : Optional[:class:`~pincer.utils.snowflake.Snowflake`] + get guilds before this guild ID + after : Optional[:class:`~pincer.utils.snowflake.Snowflake`] + get guilds after this guild ID + limit : Optional[:class:`int`] + max number of guilds to return (1-200) |default| :data:`200` + + Yields + ------ + :class:`~pincer.objects.guild.guild.Guild` + A Partial Guild that the user is in + + """ + guilds = await self.http.get( + "users/@me/guilds?" + + (f"{before=}&" if before else "") + + (f"{after=}&" if after else "") + + (f"{limit=}&" if limit else "") + ) + + for guild in guilds: + yield Guild.from_dict(construct_client_dict(self, guild)) + + async def leave_guild(self, _id: Snowflake): + """|coro| + Leave a guild. + + Parameters + ---------- + _id : :class:`~pincer.utils.snowflake.Snowflake` + the id of the guild that the bot will leave + """ + self.http.delete(f"users/@me/guilds/{_id}") + + async def create_group_dm( + self, access_tokens: List[str], nicks: Dict[Snowflake, str] + ) -> GroupDMChannel: + """|coro| + Create a new group DM channel with multiple users. + DMs created with this endpoint will not be shown in the Discord client + + Parameters + ---------- + access_tokens : List[:class:`str`] + access tokens of users that have + granted your app the ``gdm.join`` scope + + nicks : Dict[:class:`~pincer.utils.snowflake.Snowflake`, :class:`str`] + a dictionary of user ids to their respective nicknames + + Returns + ------- + :class:`~pincer.objects.guild.channel.GroupDMChannel` + group DM channel created + """ + channel = self.http.post( + "users/@me/channels", + {"access_tokens": access_tokens, "nicks": nicks}, + ) + + return GroupDMChannel.from_dict(construct_client_dict(self, channel)) + + async def get_connections(self) -> AsyncIterator[Connection]: + """|coro| + Returns a list of connection objects. + Requires the ``connections`` OAuth2 scope. + + Yields + ------- + :class:`~pincer.objects.user.connection.Connections` + the connection objects + """ + connections = self.http.get("users/@me/connections") + for conn in connections: + yield Connection.from_dict(conn) + + Bot = Client diff --git a/pincer/objects/guild/channel.py b/pincer/objects/guild/channel.py index c225382e..91d75520 100644 --- a/pincer/objects/guild/channel.py +++ b/pincer/objects/guild/channel.py @@ -451,6 +451,8 @@ async def edit(self, **kwargs): """ return await super().edit(**kwargs) +class GroupDMChannel(Channel): + """A subclass of ``Channel`` for Group DMs""" class CategoryChannel(Channel): """A subclass of ``Channel`` for categories channels @@ -513,6 +515,7 @@ class ChannelMention(APIObject): _channel_type_map: Dict[ChannelType, Channel] = { ChannelType.GUILD_TEXT: TextChannel, ChannelType.GUILD_VOICE: VoiceChannel, + ChannelType.GROUP_DM: GroupDMChannel, ChannelType.GUILD_CATEGORY: CategoryChannel, ChannelType.GUILD_NEWS: NewsChannel } From e158da9994c50258fdca63c7858a89124bac803d Mon Sep 17 00:00:00 2001 From: endercheif Date: Tue, 7 Dec 2021 19:58:15 -0800 Subject: [PATCH 2/8] :sparkles: Added all user and current user methods --- pincer/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pincer/client.py b/pincer/client.py index 1d94dbb6..46838795 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -890,7 +890,9 @@ async def modify_current_user( if changed may cause the user's discriminator to be randomized. |default| :data:`None` avatar : Optional[:class:``] - if passed, modifies the user's avatar |default| :data:`None` + if passed, modifies the user's avatar + a data URI scheme of JPG, GIF or PNG + |default| :data:`None` Returns ------- From ec00fb9c8ce7b988e4703c1c994bf3bc6586d8c7 Mon Sep 17 00:00:00 2001 From: Endercheif <45527309+Endercheif@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:45:42 -0800 Subject: [PATCH 3/8] :sparkles: guild removed from cache Co-authored-by: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> --- pincer/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/client.py b/pincer/client.py index 29946bb9..d9b82730 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -950,7 +950,7 @@ async def leave_guild(self, _id: Snowflake): the id of the guild that the bot will leave """ self.http.delete(f"users/@me/guilds/{_id}") - + self._client.guilds.pop(_id) async def create_group_dm( self, access_tokens: List[str], nicks: Dict[Snowflake, str] ) -> GroupDMChannel: From faa403a7d86d6fd58c10b11c41c1d2af724caa90 Mon Sep 17 00:00:00 2001 From: Endercheif <45527309+Endercheif@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:46:15 -0800 Subject: [PATCH 4/8] :art: Better formatting Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index d9b82730..6db8e38b 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -268,8 +268,9 @@ async def on_ready(self): InvalidEventName If the function name is not a valid event (on_x) """ - if not iscoroutinefunction(coroutine) and not isasyncgenfunction( - coroutine + if ( + not iscoroutinefunction(coroutine) + and not isasyncgenfunction(coroutine) ): raise TypeError( "Any event which is registered must be a coroutine function" From 169225024902b8cfd062954dfc8c03b0f76d8c8f Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Wed, 8 Dec 2021 15:53:32 -0500 Subject: [PATCH 5/8] :ambulance: add default so program won't crash if guild wasn't registered. (Client does not have guild intent) Missed this sorry :( --- pincer/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index 6db8e38b..52e38ec3 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -951,7 +951,8 @@ async def leave_guild(self, _id: Snowflake): the id of the guild that the bot will leave """ self.http.delete(f"users/@me/guilds/{_id}") - self._client.guilds.pop(_id) + self._client.guilds.pop(_id, None) + async def create_group_dm( self, access_tokens: List[str], nicks: Dict[Snowflake, str] ) -> GroupDMChannel: @@ -995,5 +996,4 @@ async def get_connections(self) -> AsyncIterator[Connection]: yield Connection.from_dict(conn) - Bot = Client From b5342719df232e107713abd5fb7e273b900ec02a Mon Sep 17 00:00:00 2001 From: Endercheif <45527309+Endercheif@users.noreply.github.com> Date: Wed, 8 Dec 2021 13:10:55 -0800 Subject: [PATCH 6/8] :art: Removed extra whitespace Co-authored-by: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> --- pincer/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pincer/client.py b/pincer/client.py index 52e38ec3..59103422 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -929,7 +929,6 @@ async def get_current_user_guilds( ------ :class:`~pincer.objects.guild.guild.Guild` A Partial Guild that the user is in - """ guilds = await self.http.get( "users/@me/guilds?" From c328fd9fb19a64dee9588d5188283fdd0b49f6c3 Mon Sep 17 00:00:00 2001 From: Endercheif <45527309+Endercheif@users.noreply.github.com> Date: Thu, 9 Dec 2021 13:08:41 -0800 Subject: [PATCH 7/8] :bug: Added missing awaits --- pincer/client.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index 59103422..6dc0e5d1 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -875,7 +875,10 @@ async def get_current_user(self) -> User: :class:`~pincer.objects.user.user.User` """ return User.from_dict( - construct_client_dict(self, self.http.get("users/@me")) + construct_client_dict( + self, + await self.http.get("users/@me") + ) ) async def modify_current_user( @@ -949,7 +952,7 @@ async def leave_guild(self, _id: Snowflake): _id : :class:`~pincer.utils.snowflake.Snowflake` the id of the guild that the bot will leave """ - self.http.delete(f"users/@me/guilds/{_id}") + await self.http.delete(f"users/@me/guilds/{_id}") self._client.guilds.pop(_id, None) async def create_group_dm( @@ -973,7 +976,7 @@ async def create_group_dm( :class:`~pincer.objects.guild.channel.GroupDMChannel` group DM channel created """ - channel = self.http.post( + channel = await self.http.post( "users/@me/channels", {"access_tokens": access_tokens, "nicks": nicks}, ) @@ -990,7 +993,7 @@ async def get_connections(self) -> AsyncIterator[Connection]: :class:`~pincer.objects.user.connection.Connections` the connection objects """ - connections = self.http.get("users/@me/connections") + connections = await self.http.get("users/@me/connections") for conn in connections: yield Connection.from_dict(conn) From 5a9c44afb2dae079d06f516627078491f0b5ae80 Mon Sep 17 00:00:00 2001 From: endercheif Date: Thu, 16 Dec 2021 21:56:03 -0800 Subject: [PATCH 8/8] :sparkles: File upload with modify_current_user --- pincer/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index d284843a..85a6102a 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -34,7 +34,7 @@ Intents, GuildTemplate, Connection, - StickerPack + StickerPack, File ) from .objects.guild.channel import GroupDMChannel from .utils.conversion import construct_client_dict, remove_none @@ -902,7 +902,7 @@ async def get_current_user(self) -> User: ) async def modify_current_user( - self, username: Optional[str] = None, avatar: Optional = None + self, username: Optional[str] = None, avatar: Optional[File] = None ) -> User: """|coro| Modify the requester's user account settings @@ -913,7 +913,7 @@ async def modify_current_user( user's username, if changed may cause the user's discriminator to be randomized. |default| :data:`None` - avatar : Optional[:class:``] + avatar : Optional[:class:`File`] if passed, modifies the user's avatar a data URI scheme of JPG, GIF or PNG |default| :data:`None` @@ -924,6 +924,8 @@ async def modify_current_user( Current modified user """ + avatar = avatar.uri if avatar else avatar + user = await self.http.patch( "users/@me", remove_none({"username": username, "avatar": avatar}) )