Skip to content
This repository has been archived by the owner on Dec 26, 2022. It is now read-only.

✨ ⚡ Added Mentionable type and improved Interaction performance #253

Merged
merged 11 commits into from
Nov 28, 2021
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class Bot(Client):
# pincer.objects.User - User
# pincer.objects.Channel - Channel
# pincer.objects.Role - Role
# Mentionable is not implemented
# pincer.objects.Mentionable - Mentionable
async def say(self, message: str):
return message

Expand Down
7 changes: 7 additions & 0 deletions docs/pincer.objects.app.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,10 @@ DefaultThrottleHandler
.. attributetable:: DefaultThrottleHandler

.. autoclass:: DefaultThrottleHandler()

Mentionable
~~~~~~~~~~~~~~~~~~~~~~

.. attributetable:: Mentionable

.. autoclass:: Mentionable()
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Available types are as follows:
- pincer.objects.User - User
- pincer.objects.Channel - Channel
- pincer.objects.Role - Role
- Mentionable is not implemented
- pincer.objects.Mentionable - Mentionable

.. code-block:: python

Expand Down
4 changes: 3 additions & 1 deletion pincer/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
User,
Channel,
Guild,
Mentionable,
MessageContext,
)
from ..objects.app import (
Expand Down Expand Up @@ -68,6 +69,7 @@
User: AppCommandOptionType.USER,
Channel: AppCommandOptionType.CHANNEL,
Role: AppCommandOptionType.ROLE,
Mentionable: AppCommandOptionType.MENTIONABLE
}

if TYPE_CHECKING:
Expand Down Expand Up @@ -95,7 +97,7 @@ def command(
pincer.objects.User - User
pincer.objects.Channel - Channel
pincer.objects.Role - Role
Mentionable is not implemented
pincer.objects.Mentionable - Mentionable

.. code-block:: python3

Expand Down
1 change: 0 additions & 1 deletion pincer/middleware/interaction_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ async def interaction_create_middleware(
interaction: Interaction = Interaction.from_dict(
construct_client_dict(self, payload.data)
)
await interaction.build()
command = ChatCommandHandler.register.get(interaction.data.name)

if command:
Expand Down
3 changes: 2 additions & 1 deletion pincer/objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .app.interactions import (
ResolvedData, InteractionData, Interaction
)
from .app.mentionable import Mentionable
from .app.select_menu import SelectOption, SelectMenu
from .app.session_start_limit import SessionStartLimit
from .app.throttle_scope import ThrottleScope
Expand Down Expand Up @@ -147,5 +148,5 @@
"User", "UserMessage", "VerificationLevel", "VisibilityType",
"VoiceChannel", "VoiceRegion", "VoiceServerUpdateEvent", "VoiceState",
"Webhook", "WebhookType", "WebhooksUpdateEvent", "WelcomeScreen",
"WelcomeScreenChannel"
"WelcomeScreenChannel", "Mentionable"
)
3 changes: 2 additions & 1 deletion pincer/objects/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .interaction_base import CallbackType, InteractionType, MessageInteraction
from .interaction_flags import InteractionFlags
from .interactions import ResolvedData, InteractionData, Interaction
from .mentionable import Mentionable
from .select_menu import SelectOption, SelectMenu
from .session_start_limit import SessionStartLimit
from .throttle_scope import ThrottleScope
Expand All @@ -25,5 +26,5 @@
"DefaultThrottleHandler", "Intents", "Interaction", "InteractionData",
"InteractionFlags", "InteractionType", "MessageInteraction",
"ResolvedData", "SelectMenu", "SelectOption", "SessionStartLimit",
"ThrottleInterface", "ThrottleScope"
"ThrottleInterface", "ThrottleScope", "Mentionable"
)
113 changes: 59 additions & 54 deletions pincer/objects/app/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

from __future__ import annotations

from asyncio import gather, iscoroutine, sleep, ensure_future
from asyncio import sleep, ensure_future
from dataclasses import dataclass
from typing import Dict, TYPE_CHECKING, Union, Optional, List
from typing import Any, Dict, TYPE_CHECKING, Union, Optional, List

from .command_types import AppCommandOptionType
from .interaction_base import InteractionType, CallbackType
from .mentionable import Mentionable
from ..app.select_menu import SelectOption
from ..guild.member import GuildMember
from ..message.context import MessageContext
Expand All @@ -17,7 +18,7 @@
from ..user import User
from ...exceptions import InteractionDoesNotExist, UseFollowup, \
InteractionAlreadyAcknowledged, NotFoundError, InteractionTimedOut
from ...utils import APIObject, convert
from ...utils import APIObject
from ...utils.convert_message import convert_message
from ...utils.snowflake import Snowflake
from ...utils.types import MISSING
Expand Down Expand Up @@ -142,61 +143,65 @@ class Interaction(APIObject):
def __post_init__(self):
super().__post_init__()

self._convert_functions = {
AppCommandOptionType.SUB_COMMAND: None,
AppCommandOptionType.SUB_COMMAND_GROUP: None,

AppCommandOptionType.STRING: str,
AppCommandOptionType.INTEGER: int,
AppCommandOptionType.BOOLEAN: bool,
AppCommandOptionType.NUMBER: float,

AppCommandOptionType.USER: lambda value:
self._client.get_user(
convert(value, Snowflake.from_string)
),
AppCommandOptionType.CHANNEL: lambda value:
self._client.get_channel(
convert(value, Snowflake.from_string)
),
AppCommandOptionType.ROLE: lambda value:
self._client.get_role(
convert(self.guild_id, Snowflake.from_string),
convert(value, Snowflake.from_string)
),
AppCommandOptionType.MENTIONABLE: None
}

async def build(self):
"""|coro|

Sets the parameters in the interaction that need information
from the discord API.
for option in self.data.options:

if option.type is AppCommandOptionType.STRING:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this big elif chain is actually any faster?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea. It's there now since Mentionable runs two different functions so a dictionary doesn't work.
The optimazatiom comes from me removing unnecessary request to the discord API.

option.value = str(option.value)
elif option.type is AppCommandOptionType.INTEGER:
option.value = int(option.value)
elif option.type is AppCommandOptionType.BOOLEAN:
option.value = bool(option.value)
elif option.type is AppCommandOptionType.NUMBER:
option.value = float(option.value)

elif option.type is AppCommandOptionType.USER:
user = self.return_type(option, self.data.resolved.members)
user.set_user_data(
self.return_type(option, self.data.resolved.users)
)
option.value = user

elif option.type is AppCommandOptionType.CHANNEL:
option.value = self.return_type(
option, self.data.resolved.channels
)

elif option.type is AppCommandOptionType.ROLE:
option.value = self.return_type(
option, self.data.resolved.roles
)

elif option.type is AppCommandOptionType.MENTIONABLE:
user = self.return_type(option, self.data.resolved.members)
if user:
user.set_user_data(self.return_type(
option, self.data.resolved.users)
)

option.value = Mentionable(
user,
self.return_type(
option, self.data.resolved.roles
)
)

@staticmethod
def return_type(
option: Snowflake,
data: Dict[Snowflake, Any]
) -> Optional[APIObject]:
"""
if not self.data.options:
return

await gather(
*map(self.convert, self.data.options)
)
Returns a value from the option or None if it doesn't exist.

async def convert(self, option: AppCommandInteractionDataOption):
"""|coro|

Sets an ``AppCommandInteractionDataOption`` value parameter to
the payload type
option : :class:`~pincer.utils.types.Snowflake`
Snowflake to search ``data`` for.
data : Dict[:class:`~pincer.utils.types.Snowflake`, Any]
Resolved data to search through.
"""
converter = self._convert_functions.get(option.type)

if not converter:
raise NotImplementedError(
f"Handling for AppCommandOptionType {option.type} is not "
"implemented"
)

res = converter(option.value)
if data:
return data[option.value]

option.value = (await res) if iscoroutine(res) else res
return None

def convert_to_message_context(self, command):
return MessageContext(
Expand Down
33 changes: 33 additions & 0 deletions pincer/objects/app/mentionable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright Pincer 2021-Present
# Full MIT License can be found in `LICENSE` at the project root.

from dataclasses import dataclass
from typing import Optional


from ...objects.guild.role import Role
from ...objects.user.user import User


@dataclass
class Mentionable:
"""
Represents the Mentionable type

user : Optional[:class:`~pincer.objects.user.user.User`]
User object returned from a discord interaction
role: Optional[:class:`~pincer.objects.guild.role.Role`]
Role object returned from a discord interaction
"""
user: Optional[User] = None
role: Optional[Role] = None

@property
def is_user(self):
"""Returns true if the Mentionable object has a User"""
return self.user is not None

@property
def is_role(self):
"""Returns true if the Mentionable object has a Role"""
return self.role is not None
28 changes: 25 additions & 3 deletions pincer/objects/guild/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class BaseMember(APIObject):
hoisted_role: APINullable[:class:`~pincer.utils.snowflake.Snowflake`]
The user's top role in the guild.
"""
joined_at: Timestamp
roles: List[Snowflake]
joined_at: APINullable[Timestamp] = MISSING
roles: APINullable[List[Snowflake]] = MISSING
deaf: bool = MISSING
mute: bool = MISSING

Expand Down Expand Up @@ -76,7 +76,7 @@ class PartialGuildMember(APIObject):


@dataclass
class GuildMember(BaseMember, APIObject):
class GuildMember(BaseMember, User, APIObject):
"""Represents a member which resides in a guild/server.

Attributes
Expand Down Expand Up @@ -107,6 +107,28 @@ class GuildMember(BaseMember, APIObject):
user: APINullable[User] = MISSING
avatar: APINullable[str] = MISSING

def __post_init__(self):
super().__post_init__()

if self.user is not MISSING:
self.set_user_data(self.user)

def set_user_data(self, user: User):
"""
Used to set the user parameters of a GuildMember instance

user: APINullable[:class:`~pincer.objects.user.user.User`]
A user class to copy the fields from
"""

# Inspired from this thread
# https://stackoverflow.com/questions/57962873/easiest-way-to-copy-all-fields-from-one-dataclass-instance-to-another

for key, value in user.__dict__.items():
setattr(self, key, value)

self.user = MISSING

@classmethod
async def from_id(
cls,
Expand Down
2 changes: 1 addition & 1 deletion pincer/objects/user/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class User(APIObject):
Whether the email on this account has been verified
"""

id: Snowflake
id: APINullable[Snowflake] = MISSING
username: APINullable[str] = MISSING
discriminator: APINullable[str] = MISSING

Expand Down