Skip to content

Commit

Permalink
feat: implement voice_channel_effect event (#993)
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftinv authored Nov 13, 2024
1 parent 4c8e943 commit 0961c9d
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 2 deletions.
4 changes: 4 additions & 0 deletions changelog/993.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Support voice channel effect events.
- New events: :func:`on_voice_channel_effect`, :func:`on_raw_voice_channel_effect`.
- New types: :class:`VoiceChannelEffect`, :class:`RawVoiceChannelEffectEvent`.
- New enum: :class:`VoiceChannelEffectAnimationType`.
48 changes: 48 additions & 0 deletions disnake/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
ThreadLayout,
ThreadSortOrder,
VideoQualityMode,
VoiceChannelEffectAnimationType,
try_enum,
try_enum_to_int,
)
Expand All @@ -50,6 +51,7 @@
from .utils import MISSING

__all__ = (
"VoiceChannelEffect",
"TextChannel",
"VoiceChannel",
"StageChannel",
Expand Down Expand Up @@ -90,13 +92,59 @@
)
from .types.snowflake import SnowflakeList
from .types.threads import ThreadArchiveDurationLiteral
from .types.voice import VoiceChannelEffect as VoiceChannelEffectPayload
from .ui.action_row import Components, MessageUIComponent
from .ui.view import View
from .user import BaseUser, ClientUser, User
from .voice_region import VoiceRegion
from .webhook import Webhook


class VoiceChannelEffect:
"""An effect sent by a member in a voice channel.
Different sets of attributes will be present, depending on the type of effect.
.. versionadded:: 2.10
Attributes
----------
emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]
The emoji, for emoji reaction effects.
animation_type: Optional[:class:`VoiceChannelEffectAnimationType`]
The emoji animation type, for emoji reaction effects.
animation_id: Optional[:class:`int`]
The emoji animation ID, for emoji reaction effects.
"""

__slots__ = (
"emoji",
"animation_type",
"animation_id",
)

def __init__(self, *, data: VoiceChannelEffectPayload, state: ConnectionState) -> None:
self.emoji: Optional[Union[Emoji, PartialEmoji]] = None
if emoji_data := data.get("emoji"):
emoji = state._get_emoji_from_data(emoji_data)
if isinstance(emoji, str):
emoji = PartialEmoji(name=emoji)
self.emoji = emoji

self.animation_type = (
try_enum(VoiceChannelEffectAnimationType, value)
if (value := data.get("animation_type")) is not None
else None
)
self.animation_id: Optional[int] = utils._get_as_snowflake(data, "animation_id")

def __repr__(self) -> str:
return (
f"<VoiceChannelEffect emoji={self.emoji!r} animation_type={self.animation_type!r}"
f" animation_id={self.animation_id!r}>"
)


async def _single_delete_strategy(messages: Iterable[Message]) -> None:
for m in messages:
await m.delete()
Expand Down
19 changes: 19 additions & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"SKUType",
"EntitlementType",
"PollLayoutType",
"VoiceChannelEffectAnimationType",
)


Expand Down Expand Up @@ -1160,6 +1161,19 @@ class Event(Enum):
"""Called when a `Member` changes their `VoiceState`.
Represents the :func:`on_voice_state_update` event.
"""
voice_channel_effect = "voice_channel_effect"
"""Called when a `Member` sends an effect in a voice channel the bot is connected to.
Represents the :func:`on_voice_channel_effect` event.
.. versionadded:: 2.10
"""
raw_voice_channel_effect = "raw_voice_channel_effect"
"""Called when a `Member` sends an effect in a voice channel the bot is connected to,
regardless of the member cache.
Represents the :func:`on_raw_voice_channel_effect` event.
.. versionadded:: 2.10
"""
stage_instance_create = "stage_instance_create"
"""Called when a `StageInstance` is created for a `StageChannel`.
Represents the :func:`on_stage_instance_create` event.
Expand Down Expand Up @@ -1385,6 +1399,11 @@ class PollLayoutType(Enum):
default = 1


class VoiceChannelEffectAnimationType(Enum):
premium = 0
basic = 1


T = TypeVar("T")


Expand Down
2 changes: 2 additions & 0 deletions disnake/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,8 @@ def voice_states(self):
This corresponds to the following events:
- :func:`on_voice_state_update`
- :func:`on_voice_channel_effect`
- :func:`on_raw_voice_channel_effect`
This also corresponds to the following attributes and classes in terms of cache:
Expand Down
39 changes: 39 additions & 0 deletions disnake/raw_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .utils import _get_as_snowflake, get_slots

if TYPE_CHECKING:
from .channel import VoiceChannelEffect
from .member import Member
from .message import Message
from .partial_emoji import PartialEmoji
Expand All @@ -29,6 +30,7 @@
PresenceUpdateEvent,
ThreadDeleteEvent,
TypingStartEvent,
VoiceChannelEffectSendEvent,
)
from .user import User

Expand All @@ -48,6 +50,7 @@
"RawGuildMemberRemoveEvent",
"RawPresenceUpdateEvent",
"RawPollVoteActionEvent",
"RawVoiceChannelEffectEvent",
)


Expand Down Expand Up @@ -534,3 +537,39 @@ def __init__(self, data: PresenceUpdateEvent) -> None:
self.user_id: int = int(data["user"]["id"])
self.guild_id: int = int(data["guild_id"])
self.data: PresenceUpdateEvent = data


class RawVoiceChannelEffectEvent(_RawReprMixin):
"""Represents the event payload for an :func:`on_raw_voice_channel_effect` event.
.. versionadded:: 2.10
Attributes
----------
channel_id: :class:`int`
The ID of the channel where the effect was sent.
guild_id: :class:`int`
The ID of the guild where the effect was sent.
user_id: :class:`int`
The ID of the user who sent the effect.
effect: :class:`VoiceChannelEffect`
The effect that was sent.
cached_member: Optional[:class:`Member`]
The member who sent the effect, if they could be found in the internal cache.
"""

__slots__ = (
"channel_id",
"guild_id",
"user_id",
"effect",
"cached_member",
)

def __init__(self, data: VoiceChannelEffectSendEvent, effect: VoiceChannelEffect) -> None:
self.channel_id: int = int(data["channel_id"])
self.guild_id: int = int(data["guild_id"])
self.user_id: int = int(data["user_id"])
self.effect: VoiceChannelEffect = effect

self.cached_member: Optional[Member] = None
22 changes: 21 additions & 1 deletion disnake/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
StageChannel,
TextChannel,
VoiceChannel,
VoiceChannelEffect,
_guild_channel_factory,
_threaded_channel_factory,
)
Expand Down Expand Up @@ -79,6 +80,7 @@
RawThreadDeleteEvent,
RawThreadMemberRemoveEvent,
RawTypingEvent,
RawVoiceChannelEffectEvent,
)
from .role import Role
from .stage_instance import StageInstance
Expand Down Expand Up @@ -1809,7 +1811,6 @@ def parse_voice_state_update(self, data: gateway.VoiceStateUpdateEvent) -> None:
if flags.voice:
if channel_id is None and flags._voice_only and member.id != self_id:
# Only remove from cache if we only have the voice flag enabled
# Member doesn't meet the Snowflake protocol currently
guild._remove_member(member)
elif channel_id is not None:
guild._add_member(member)
Expand All @@ -1831,6 +1832,25 @@ def parse_voice_server_update(self, data: gateway.VoiceServerUpdateEvent) -> Non
logging_coroutine(coro, info="Voice Protocol voice server update handler")
)

def parse_voice_channel_effect_send(self, data: gateway.VoiceChannelEffectSendEvent) -> None:
guild = self._get_guild(int(data["guild_id"]))
if guild is None:
_log.debug(
"VOICE_CHANNEL_EFFECT_SEND referencing an unknown guild ID: %s. Discarding.",
data["guild_id"],
)
return

effect = VoiceChannelEffect(data=data, state=self)
raw = RawVoiceChannelEffectEvent(data, effect)

channel = guild.get_channel(raw.channel_id)
raw.cached_member = member = guild.get_member(raw.user_id)
self.dispatch("raw_voice_channel_effect", raw)

if channel and member:
self.dispatch("voice_channel_effect", channel, member, effect)

# FIXME: this should be refactored. The `GroupChannel` path will never be hit,
# `raw.timestamp` exists so no need to parse it twice, and `.get_user` should be used before falling back
def parse_typing_start(self, data: gateway.TypingStartEvent) -> None:
Expand Down
9 changes: 8 additions & 1 deletion disnake/types/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from .sticker import GuildSticker
from .threads import Thread, ThreadMember, ThreadMemberWithPresence, ThreadType
from .user import AvatarDecorationData, User
from .voice import GuildVoiceState, SupportedModes
from .voice import GuildVoiceState, SupportedModes, VoiceChannelEffect


class SessionStartLimit(TypedDict):
Expand Down Expand Up @@ -612,6 +612,13 @@ class VoiceServerUpdateEvent(TypedDict):
endpoint: Optional[str]


# https://discord.com/developers/docs/topics/gateway-events#voice-channel-effect-send
class VoiceChannelEffectSendEvent(VoiceChannelEffect):
channel_id: Snowflake
guild_id: Snowflake
user_id: Snowflake


# https://discord.com/developers/docs/topics/gateway-events#typing-start
class TypingStartEvent(TypedDict):
channel_id: Snowflake
Expand Down
9 changes: 9 additions & 0 deletions disnake/types/voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing_extensions import NotRequired

from .emoji import PartialEmoji
from .member import MemberWithUser
from .snowflake import Snowflake

Expand All @@ -12,6 +13,8 @@
"aead_xchacha20_poly1305_rtpsize",
]

VoiceChannelEffectAnimationType = Literal[0, 1]


class _VoiceState(TypedDict):
user_id: Snowflake
Expand Down Expand Up @@ -57,3 +60,9 @@ class VoiceReady(TypedDict):
port: int
modes: List[SupportedModes]
heartbeat_interval: int


class VoiceChannelEffect(TypedDict, total=False):
emoji: Optional[PartialEmoji]
animation_type: Optional[VoiceChannelEffectAnimationType]
animation_id: int
30 changes: 30 additions & 0 deletions docs/api/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,36 @@ Voice
:param after: The voice state after the changes.
:type after: :class:`VoiceState`

.. function:: on_voice_channel_effect(channel, member, effect)

Called when a :class:`Member` sends an effect in a voice channel the bot is connected to.

This requires :attr:`Intents.voice_states` and :attr:`Intents.members` to be enabled.

If the member is not found in the internal member cache, then this
event will not be called. Consider using :func:`on_raw_voice_channel_effect` instead.

.. versionadded:: 2.10

:param channel: The voice channel where the effect was sent.
:type channel: :class:`VoiceChannel`
:param member: The member that sent the effect.
:type member: :class:`Member`
:param effect: The effect that was sent.
:type effect: :class:`VoiceChannelEffect`

.. function:: on_raw_voice_channel_effect(payload)

Called when a :class:`Member` sends an effect in a voice channel the bot is connected to.
Unlike :func:`on_voice_channel_effect`, this is called regardless of the member cache.

This requires :attr:`Intents.voice_states` to be enabled.

.. versionadded:: 2.10

:param payload: The raw event payload data.
:type payload: :class:`RawVoiceChannelEffectEvent`

Interactions
~~~~~~~~~~~~

Expand Down
35 changes: 35 additions & 0 deletions docs/api/voice.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,43 @@ VoiceRegion
.. autoclass:: VoiceRegion()
:members:

VoiceChannelEffect
~~~~~~~~~~~~~~~~~~

.. attributetable:: VoiceChannelEffect

.. autoclass:: VoiceChannelEffect()
:members:

RawVoiceChannelEffectEvent
~~~~~~~~~~~~~~~~~~~~~~~~~~

.. attributetable:: RawVoiceChannelEffectEvent

.. autoclass:: RawVoiceChannelEffectEvent()
:members:


Enumerations
------------

VoiceChannelEffectAnimationType
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. class:: VoiceChannelEffectAnimationType

The type of an emoji reaction effect animation in a voice channel.

.. versionadded:: 2.10

.. attribute:: premium

A fun animation, sent by a Nitro subscriber.

.. attribute:: basic

A standard animation.

PartyType
~~~~~~~~~

Expand Down Expand Up @@ -168,3 +201,5 @@ Events
------

- :func:`on_voice_state_update(member, before, after) <disnake.on_voice_state_update>`
- :func:`on_voice_channel_effect(channel, member, effect) <disnake.on_voice_channel_effect>`
- :func:`on_raw_voice_channel_effect(payload) <disnake.on_raw_voice_channel_effect>`

0 comments on commit 0961c9d

Please sign in to comment.