diff --git a/pincer/client.py b/pincer/client.py index e2aa672e..d07678e0 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -8,7 +8,9 @@ from collections import defaultdict from importlib import import_module from inspect import isasyncgenfunction -from typing import Any, Dict, List, Optional, Tuple, Union, overload +from typing import ( + Any, Dict, List, Optional, Tuple, Union, overload, AsyncIterator +) from typing import TYPE_CHECKING from . import __package__ @@ -22,7 +24,7 @@ from .middleware import middleware from .objects import ( Role, Channel, DefaultThrottleHandler, User, Guild, Intents, - GuildTemplate + GuildTemplate, StickerPack ) from .utils.conversion import construct_client_dict from .utils.event_mgr import EventMgr @@ -857,5 +859,18 @@ async def get_webhook( """ return await Webhook.from_id(self, id, token) + async def sticker_packs(self) -> AsyncIterator[StickerPack]: + """|coro| + Yields sticker packs available to Nitro subscribers. + + Yields + ------ + :class:`~pincer.objects.message.sticker.StickerPack` + a sticker pack + """ + packs = await self.http.get("sticker-packs") + for pack in packs: + yield StickerPack.from_dict(pack) + Bot = Client diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index d680ec97..8e11d6bb 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -3,10 +3,13 @@ from __future__ import annotations +from collections import AsyncIterator from dataclasses import dataclass, field from enum import IntEnum from typing import AsyncGenerator, overload, TYPE_CHECKING +from aiohttp import FormData + from .invite import Invite from .channel import Channel from ..message.emoji import Emoji @@ -34,7 +37,8 @@ from ..user.integration import Integration from ..voice.region import VoiceRegion from ..events.presence import PresenceUpdateEvent - from ..message.sticker import Sticker + from ..message.emoji import Emoji + from ..message.sticker import Sticker, StickerPack from ..user.voice_state import VoiceState from ...client import Client from ...utils.timestamp import Timestamp @@ -349,7 +353,9 @@ class Guild(APIObject): system_channel_flags: APINullable[SystemChannelFlags] = MISSING explicit_content_filter: APINullable[ExplicitContentFilterLevel] = MISSING premium_tier: APINullable[PremiumTier] = MISSING - default_message_notifications: APINullable[DefaultMessageNotificationLevel] = MISSING + default_message_notifications: APINullable[ + DefaultMessageNotificationLevel + ] = MISSING mfa_level: APINullable[MFALevel] = MISSING owner_id: APINullable[Snowflake] = MISSING afk_timeout: APINullable[int] = MISSING @@ -491,7 +497,7 @@ async def ban( self, member_id: int, reason: str = None, - delete_message_days: int = None + delete_message_days: int = None, ): """ Parameters @@ -514,9 +520,7 @@ async def ban( data["delete_message_days"] = delete_message_days await self._http.put( - f"/guilds/{self.id}/bans/{member_id}", - data=data, - headers=headers + f"/guilds/{self.id}/bans/{member_id}", data=data, headers=headers ) async def kick(self, member_id: int, reason: Optional[str] = None): @@ -536,8 +540,7 @@ async def kick(self, member_id: int, reason: Optional[str] = None): headers["X-Audit-Log-Reason"] = reason await self._http.delete( - f"/guilds/{self.id}/members/{member_id}", - header=headers + f"/guilds/{self.id}/members/{member_id}", headers=headers ) async def get_roles(self) -> AsyncGenerator[Role, None]: @@ -601,18 +604,14 @@ async def create_role( """ ... - async def create_role( - self, - reason: Optional[str] = None, - **kwargs - ) -> Role: + async def create_role(self, reason: Optional[str] = None, **kwargs) -> Role: return Role.from_dict( construct_client_dict( self._client, await self._http.post( f"guilds/{self.id}/roles", data=kwargs, - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ), ) ) @@ -621,7 +620,7 @@ async def edit_role_position( self, id: Snowflake, reason: Optional[str] = None, - position: Optional[int] = None + position: Optional[int] = None, ) -> AsyncGenerator[Role, None]: """|coro| Edits the position of a role. @@ -643,7 +642,7 @@ async def edit_role_position( data = await self._http.patch( f"guilds/{self.id}/roles", data={"id": id, "position": position}, - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ) for role_data in data: yield Role.from_dict(construct_client_dict(self._client, role_data)) @@ -699,10 +698,7 @@ async def edit_role( ... async def edit_role( - self, - id: Snowflake, - reason: Optional[str] = None, - **kwargs + self, id: Snowflake, reason: Optional[str] = None, **kwargs ) -> Role: return Role.from_dict( construct_client_dict( @@ -710,7 +706,7 @@ async def edit_role( await self._http.patch( f"guilds/{self.id}/roles/{id}", data=kwargs, - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ), ) ) @@ -729,7 +725,7 @@ async def delete_role(self, id: Snowflake, reason: Optional[str] = None): """ await self._http.delete( f"guilds/{self.id}/roles/{id}", - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ) async def get_bans(self) -> AsyncGenerator[Ban, None]: @@ -779,7 +775,7 @@ async def unban(self, id: Snowflake, reason: Optional[str] = None): """ await self._http.delete( f"guilds/{self.id}/bans/{id}", - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ) @overload @@ -889,9 +885,7 @@ async def delete(self): await self._http.delete(f"guilds/{self.id}") async def prune_count( - self, - days: Optional[int] = 7, - include_roles: Optional[str] = None + self, days: Optional[int] = 7, include_roles: Optional[str] = None ) -> int: """|coro| Returns the number of members that @@ -920,7 +914,7 @@ async def prune( days: Optional[int] = 7, compute_prune_days: Optional[bool] = True, include_roles: Optional[List[Snowflake]] = None, - reason: Optional[str] = None + reason: Optional[str] = None, ) -> int: """|coro| Prunes members from the guild. Requires the ``KICK_MEMBERS`` permission. @@ -949,9 +943,9 @@ async def prune( data={ "days": days, "compute_prune_days": compute_prune_days, - "include_roles": include_roles + "include_roles": include_roles, }, - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), )["pruned"] async def get_voice_regions(self) -> AsyncGenerator[VoiceRegion, None]: @@ -965,7 +959,9 @@ async def get_voice_regions(self) -> AsyncGenerator[VoiceRegion, None]: """ data = await self._http.get(f"guilds/{self.id}/regions") for voice_region_data in data: - yield VoiceRegion.from_dict(construct_client_dict(self._client, voice_region_data)) + yield VoiceRegion.from_dict( + construct_client_dict(self._client, voice_region_data) + ) async def get_invites(self) -> AsyncGenerator[Invite, None]: """|coro| @@ -979,9 +975,11 @@ async def get_invites(self) -> AsyncGenerator[Invite, None]: """ data = await self._http.get(f"guilds/{self.id}/invites") for invite_data in data: - yield Invite.from_dict(construct_client_dict(self._client, invite_data)) + yield Invite.from_dict( + construct_client_dict(self._client, invite_data) + ) - async def get_integrations(self) -> AsyncGenerator[Integration, None]: + async def get_integrations(self) -> AsyncIterator[Integration]: """|coro| Returns an async generator of integrations for the guild. Requires the ``MANAGE_GUILD`` permission. @@ -993,12 +991,12 @@ async def get_integrations(self) -> AsyncGenerator[Integration, None]: """ data = await self._http.get(f"guilds/{self.id}/integrations") for integration_data in data: - yield Integration.from_dict(construct_client_dict(self._client, integration_data)) + yield Integration.from_dict( + construct_client_dict(self._client, integration_data) + ) async def delete_integration( - self, - integration: Integration, - reason: Optional[str] = None + self, integration: Integration, reason: Optional[str] = None ): """|coro| Deletes an integration. @@ -1013,7 +1011,7 @@ async def delete_integration( """ await self._http.delete( f"guilds/{self.id}/integrations/{integration.id}", - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ) async def get_widget_settings(self) -> GuildWidget: @@ -1028,15 +1026,12 @@ async def get_widget_settings(self) -> GuildWidget: """ return GuildWidget.from_dict( construct_client_dict( - self._client, - await self._http.get(f"guilds/{self.id}/widget") + self._client, await self._http.get(f"guilds/{self.id}/widget") ) ) async def modify_widget( - self, - reason: Optional[str] = None, - **kwargs + self, reason: Optional[str] = None, **kwargs ) -> GuildWidget: """|coro| Modifies the guild widget for the guild. @@ -1057,7 +1052,7 @@ async def modify_widget( data = await self._http.patch( f"guilds/{self.id}/widget", data=kwargs, - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ) return GuildWidget.from_dict(construct_client_dict(self._client, data)) @@ -1082,7 +1077,9 @@ async def vanity_url(self) -> Invite: data = await self._http.get(f"guilds/{self.id}/vanity-url") return Invite.from_dict(construct_client_dict(self._client, data)) - async def get_widget_image(self, style: Optional[str] = "shield") -> str: # TODO Replace str with ImageURL object + async def get_widget_image( + self, style: Optional[str] = "shield" + ) -> str: # TODO Replace str with ImageURL object """|coro| Returns a PNG image widget for the guild. Requires no permissions or authentication. @@ -1127,14 +1124,16 @@ async def get_welcome_screen(self) -> WelcomeScreen: The welcome screen for the guild. """ data = await self._http.get(f"guilds/{self.id}/welcome-screen") - return WelcomeScreen.from_dict(construct_client_dict(self._client, data)) + return WelcomeScreen.from_dict( + construct_client_dict(self._client, data) + ) async def modify_welcome_screen( self, enabled: Optional[bool] = None, welcome_channels: Optional[List[WelcomeScreenChannel]] = None, description: Optional[str] = None, - reason: Optional[str] = None + reason: Optional[str] = None, ) -> WelcomeScreen: """|coro| Modifies the guild's Welcome Screen. @@ -1163,17 +1162,19 @@ async def modify_welcome_screen( data={ "enabled": enabled, "welcome_channels": welcome_channels, - "description": description + "description": description, }, - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), + ) + return WelcomeScreen.from_dict( + construct_client_dict(self._client, data) ) - return WelcomeScreen.from_dict(construct_client_dict(self._client, data)) async def modify_current_user_voice_state( self, channel_id: Snowflake, suppress: Optional[bool] = None, - request_to_speak_timestamp: Optional[Timestamp] = None + request_to_speak_timestamp: Optional[Timestamp] = None, ): """|coro| Updates the current user's voice state. @@ -1202,15 +1203,12 @@ async def modify_current_user_voice_state( data={ "channel_id": channel_id, "suppress": suppress, - "request_to_speak_timestamp": request_to_speak_timestamp - } + "request_to_speak_timestamp": request_to_speak_timestamp, + }, ) async def modify_user_voice_state( - self, - user: User, - channel_id: Snowflake, - suppress: Optional[bool] = None + self, user: User, channel_id: Snowflake, suppress: Optional[bool] = None ): """|coro| Updates another user's voice state. @@ -1237,10 +1235,7 @@ async def modify_user_voice_state( """ await self._http.patch( f"guilds/{self.id}/voice-states/{user.id}", - data={ - "channel_id": channel_id, - "suppress": suppress - } + data={"channel_id": channel_id, "suppress": suppress}, ) async def get_audit_log(self) -> AuditLog: @@ -1256,7 +1251,7 @@ async def get_audit_log(self) -> AuditLog: return AuditLog.from_dict( construct_client_dict( self._client, - await self._http.get(f"guilds/{self.id}/audit-logs") + await self._http.get(f"guilds/{self.id}/audit-logs"), ) ) @@ -1292,7 +1287,7 @@ async def get_emoji(self, id: Snowflake) -> Emoji: return Emoji.from_dict( construct_client_dict( self._client, - await self._http.get(f"guilds/{self.id}/emojis/{id}") + await self._http.get(f"guilds/{self.id}/emojis/{id}"), ) ) @@ -1302,7 +1297,7 @@ async def create_emoji( name: str, image: File, roles: List[Snowflake] = [], - reason: Optional[str] = None + reason: Optional[str] = None, ) -> Emoji: """|coro| Creates a new emoji for the guild. @@ -1329,16 +1324,10 @@ async def create_emoji( """ data = await self._http.post( f"guilds/{self.id}/emojis", - data={ - "name": name, - "image": image.uri, - "roles": roles - }, - headers=remove_none({"X-Audit-Log-Reason": reason}) - ) - return Emoji.from_dict( - construct_client_dict(self._client, data) + data={"name": name, "image": image.uri, "roles": roles}, + headers=remove_none({"X-Audit-Log-Reason": reason}), ) + return Emoji.from_dict(construct_client_dict(self._client, data)) async def edit_emoji( self, @@ -1346,7 +1335,7 @@ async def edit_emoji( *, name: Optional[str] = None, roles: Optional[List[Snowflake]] = None, - reason: Optional[str] = None + reason: Optional[str] = None, ) -> Emoji: """|coro| Modifies the given emoji. @@ -1370,21 +1359,13 @@ async def edit_emoji( """ data = await self._http.patch( f"guilds/{self.id}/emojis/{id}", - data={ - "name": name, - "roles": roles - }, - headers=remove_none({"X-Audit-Log-Reason": reason}) - ) - return Emoji.from_dict( - construct_client_dict(self._client, data) + data={"name": name, "roles": roles}, + headers=remove_none({"X-Audit-Log-Reason": reason}), ) + return Emoji.from_dict(construct_client_dict(self._client, data)) async def delete_emoji( - self, - id: Snowflake, - *, - reason: Optional[str] = None + self, id: Snowflake, *, reason: Optional[str] = None ): """|coro| Deletes the given emoji. @@ -1399,10 +1380,10 @@ async def delete_emoji( """ await self._http.delete( f"guilds/{self.id}/emojis/{id}", - headers=remove_none({"X-Audit-Log-Reason": reason}) + headers=remove_none({"X-Audit-Log-Reason": reason}), ) - async def get_templates(self) -> AsyncGenerator[GuildTemplate, None]: + async def get_templates(self) -> AsyncIterator[GuildTemplate]: """|coro| Returns an async generator of the guild templates. @@ -1418,9 +1399,7 @@ async def get_templates(self) -> AsyncGenerator[GuildTemplate, None]: ) async def create_template( - self, - name: str, - description: Optional[str] = None + self, name: str, description: Optional[str] = None ) -> GuildTemplate: """|coro| Creates a new template for the guild. @@ -1440,19 +1419,13 @@ async def create_template( """ data = await self._http.post( f"guilds/{self.id}/templates", - data={ - "name": name, - "description": description - } + data={"name": name, "description": description}, ) return GuildTemplate.from_dict( construct_client_dict(self._client, data) ) - async def sync_template( - self, - template: GuildTemplate - ) -> GuildTemplate: + async def sync_template(self, template: GuildTemplate) -> GuildTemplate: """|coro| Syncs the given template. Requires the ``MANAGE_GUILD`` permission. @@ -1479,7 +1452,7 @@ async def edit_template( template: GuildTemplate, *, name: Optional[str] = None, - description: Optional[str] = None + description: Optional[str] = None, ) -> GuildTemplate: """|coro| Modifies the template's metadata. @@ -1503,19 +1476,13 @@ async def edit_template( """ data = await self._http.patch( f"guilds/{self.id}/templates/{template.code}", - data={ - "name": name, - "description": description - } + data={"name": name, "description": description}, ) return GuildTemplate.from_dict( construct_client_dict(self._client, data) ) - async def delete_template( - self, - template: GuildTemplate - ) -> GuildTemplate: + async def delete_template(self, template: GuildTemplate) -> GuildTemplate: """|coro| Deletes the given template. Requires the ``MANAGE_GUILD`` permission. @@ -1524,7 +1491,7 @@ async def delete_template( ---------- template : :class:`~pincer.objects.guild.template.GuildTemplate` The template to delete - + Returns ------- :class:`~pincer.objects.guild.template.GuildTemplate` @@ -1537,6 +1504,104 @@ async def delete_template( construct_client_dict(self._client, data) ) + async def list_stickers(self) -> AsyncIterator[Sticker]: + """|coro| + Yields sticker objects for the current guild. + Includes ``user`` fields if the bot has the + ``MANAGE_EMOJIS_AND_STICKERS`` permission. + + Yields + ------ + :class:`~pincer.objects.message.sticker.Sticker` + a sticker for the current guild + """ + + for sticker in await self._http.get(f"guild/{self.id}/stickers"): + yield Sticker.from_dict(sticker) + + async def get_sticker(self, _id: Snowflake) -> Sticker: + """|coro| + Returns a sticker object for the current guild and sticker IDs. + Includes the ``user`` field if the bot has the + ``MANAGE_EMOJIS_AND_STICKERS`` permission. + + Parameters + ---------- + _id : int + id of the sticker + + Returns + ------- + :class:`~pincer.objects.message.sticker.Sticker` + the sticker requested + """ + sticker = await self._http.get(f"guilds/{self.id}/stickers/{_id}") + return Sticker.from_dict(sticker) + + async def create_sticker( + self, + name: str, + tags: str, + description: str, + file: File, + reason: Optional[str] = None, + ) -> Sticker: + """|coro| + Create a new sticker for the guild. + Requires the ``MANAGE_EMOJIS_AND_STICKERS permission``. + + Parameters + ---------- + name : str + name of the sticker (2-30 characters) + tags : str + autocomplete/suggestion tags for the sticker (max 200 characters) + file : :class:`~pincer.objects.message.file.File` + the sticker file to upload, must be a PNG, APNG, or Lottie JSON file, max 500 KB + description : str + description of the sticker (empty or 2-100 characters) |default| :data:`""` + reason : Optional[:class:`str`] |default| :data:`None` + reason for creating the sticker + + Returns + ------- + :class:`~pincer.objects.message.sticker.Sticker` + the newly created sticker + """ # noqa: E501 + + form = FormData() + form.add_field("name", name) + form.add_field("tags", tags) + form.add_field("description", description) + form.add_field( + "file", + file.content, + content_type=file.content_type + ) + + payload = form() + + sticker = await self._http.post( + f"guilds/{self.id}/stickers", + data=payload, + headers=remove_none({"X-Audit-Log-Reason": reason}), + content_type=payload.content_type + ) + + return Sticker.from_dict(sticker) + + async def delete_sticker(self, _id: Snowflake): + """|coro| + Delete the given sticker. + Requires the ``MANAGE_EMOJIS_AND_STICKERS`` permission. + + Parameters + ---------- + _id: Snowflake + id of the sticker + """ + await self._http.delete(f"guilds/{self.id}/stickers/{_id}") + async def get_webhooks(self) -> AsyncGenerator[Webhook, None]: """|coro| Returns an async generator of the guild webhooks. @@ -1564,6 +1629,7 @@ def from_dict(cls, data) -> Guild: :class:`~pincer.objects.guild.guild.Guild` The new guild object. Raises + ------ :class:`~pincer.exceptions.UnavailableGuildError` The guild is unavailable due to a discord outage. """ diff --git a/pincer/objects/message/file.py b/pincer/objects/message/file.py index cdc7e511..28d7cab4 100644 --- a/pincer/objects/message/file.py +++ b/pincer/objects/message/file.py @@ -201,3 +201,7 @@ def uri(self) -> str: encoded_bytes = b64encode(self.content).decode('ascii') return f"data:image/{self.image_format};base64,{encoded_bytes}" + + @property + def content_type(self): + return f"image/{self.image_format}" diff --git a/pincer/objects/message/sticker.py b/pincer/objects/message/sticker.py index 1b178cbf..56a3cf73 100644 --- a/pincer/objects/message/sticker.py +++ b/pincer/objects/message/sticker.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from ...utils.api_object import APIObject +from ...utils.conversion import remove_none from ...utils.types import MISSING if TYPE_CHECKING: @@ -27,6 +28,7 @@ class StickerType(IntEnum): GUILD: Sticker is a custom sticker from a discord server. """ + STANDARD = 1 GUILD = 2 @@ -43,6 +45,7 @@ class StickerFormatType(IntEnum): LOTTIE: Sticker is animated with LOTTIE format. (vector based) """ + PNG = 1 APNG = 2 LOTTIE = 3 @@ -94,6 +97,61 @@ class Sticker(APIObject): sort_value: APINullable[int] = MISSING user: APINullable[User] = MISSING + @classmethod + async def from_id(cls, _id: Snowflake) -> Sticker: + """|coro| + Returns a sticker object for the given sticker ID. + + Parameters + ---------- + _id : Snowflake + id of the sticker + + Returns + ------- + :class:`~pincer.objects.message.sticker.Sticker` + sticker object of the given ID + """ + sticker = await cls._http.get(f"stickers/{_id}") + return cls.from_dict(sticker) + + async def modify( + self, + name: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[str] = None, + reason: Optional[str] = None, + ) -> Sticker: + """|coro| + Modify the given sticker. + Requires the ``MANAGE_EMOJIS_AND_STICKERS permission.`` + + Parameters + ---------- + name : Optional[:class:`str`] |default| :data:`None` + name of the sticker (2-30 characters) + description : Optional[:class:`str`] |default| :data:`None` + description of the sticker (2-100 characters) + tags : Optional[:class:`str`] |default| :data:`None` + autocomplete/suggestion tags for the sticker (max 200 characters) + reason : Optional[:class:`str`] + reason for modifying the sticker + + Returns + ------- + :class:`~pincer.objects.message.sticker.Sticker` + the modified sticker + """ + sticker = await self._http.patch( + f"guilds/{self.guild_id}/stickers/{self.id}", + data=remove_none( + {"name": name, "description": description, "tags": tags} + ), + headers=remove_none({"X-Audit-Log-Reason": reason}), + ) + + return Sticker.from_dict(sticker) + @dataclass(repr=False) class StickerItem(APIObject):