Skip to content
Merged

5.10.0 #1545

Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,9 @@
to_optional_snowflake,
to_snowflake,
to_snowflake_list,
TYPE_ALL_ACTION,
TYPE_ALL_CHANNEL,
TYPE_ALL_TRIGGER,
TYPE_CHANNEL_MAPPING,
TYPE_COMPONENT_MAPPING,
TYPE_DM_CHANNEL,
Expand Down Expand Up @@ -659,7 +661,9 @@
"to_optional_snowflake",
"to_snowflake",
"to_snowflake_list",
"TYPE_ALL_ACTION",
"TYPE_ALL_CHANNEL",
"TYPE_ALL_TRIGGER",
"TYPE_CHANNEL_MAPPING",
"TYPE_COMPONENT_MAPPING",
"TYPE_DM_CHANNEL",
Expand Down
44 changes: 25 additions & 19 deletions interactions/api/events/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ async def an_event_handler(event: ChannelCreate):

"""

from typing import TYPE_CHECKING, List, Sequence, Union, Optional
from typing import TYPE_CHECKING, List, Optional, Sequence, Union

import attrs

import interactions.models
from interactions.api.events.base import GuildEvent, BaseEvent
from interactions.api.events.base import BaseEvent, GuildEvent
from interactions.client.const import Absent
from interactions.client.utils.attr_utils import docs
from interactions.models.discord.snowflake import to_snowflake
Expand Down Expand Up @@ -99,35 +99,41 @@ async def an_event_handler(event: ChannelCreate):


if TYPE_CHECKING:
from interactions.models.discord.guild import Guild, GuildIntegration
from interactions.models.discord.channel import BaseChannel, TYPE_THREAD_CHANNEL, VoiceChannel
from interactions.models.discord.message import Message
from interactions.models.discord.timestamp import Timestamp
from interactions.models.discord.user import Member, User, BaseUser
from interactions.models.discord.snowflake import Snowflake_Type
from interactions.models.discord.activity import Activity
from interactions.models.discord.app_perms import ApplicationCommandPermission
from interactions.models.discord.auto_mod import AutoModerationAction, AutoModRule
from interactions.models.discord.channel import (
TYPE_ALL_CHANNEL,
TYPE_THREAD_CHANNEL,
VoiceChannel,
)
from interactions.models.discord.emoji import CustomEmoji, PartialEmoji
from interactions.models.discord.guild import Guild, GuildIntegration
from interactions.models.discord.message import Message
from interactions.models.discord.reaction import Reaction
from interactions.models.discord.role import Role
from interactions.models.discord.scheduled_event import ScheduledEvent
from interactions.models.discord.snowflake import Snowflake_Type
from interactions.models.discord.stage_instance import StageInstance
from interactions.models.discord.sticker import Sticker
from interactions.models.discord.timestamp import Timestamp
from interactions.models.discord.user import BaseUser, Member, User
from interactions.models.discord.voice_state import VoiceState
from interactions.models.discord.stage_instance import StageInstance
from interactions.models.discord.auto_mod import AutoModerationAction, AutoModRule
from interactions.models.discord.reaction import Reaction
from interactions.models.discord.app_perms import ApplicationCommandPermission
from interactions.models.discord.scheduled_event import ScheduledEvent


@attrs.define(eq=False, order=False, hash=False, kw_only=False)
class AutoModExec(BaseEvent):
"""Dispatched when an auto modation action is executed"""

execution: "AutoModerationAction" = attrs.field(repr=False, metadata=docs("The executed auto mod action"))
channel: "BaseChannel" = attrs.field(repr=False, metadata=docs("The channel the action was executed in"))
channel: "TYPE_ALL_CHANNEL" = attrs.field(repr=False, metadata=docs("The channel the action was executed in"))
guild: "Guild" = attrs.field(repr=False, metadata=docs("The guild the action was executed in"))


@attrs.define(eq=False, order=False, hash=False, kw_only=False)
class AutoModCreated(BaseEvent):
"""Dispatched when an auto mod rule is created"""

guild: "Guild" = attrs.field(repr=False, metadata=docs("The guild the rule was modified in"))
rule: "AutoModRule" = attrs.field(repr=False, metadata=docs("The rule that was modified"))

Expand Down Expand Up @@ -164,18 +170,18 @@ class ApplicationCommandPermissionsUpdate(BaseEvent):
class ChannelCreate(BaseEvent):
"""Dispatched when a channel is created."""

channel: "BaseChannel" = attrs.field(repr=False, metadata=docs("The channel this event is dispatched from"))
channel: "TYPE_ALL_CHANNEL" = attrs.field(repr=False, metadata=docs("The channel this event is dispatched from"))


@attrs.define(eq=False, order=False, hash=False, kw_only=False)
class ChannelUpdate(BaseEvent):
"""Dispatched when a channel is updated."""

before: "BaseChannel" = attrs.field(
before: "TYPE_ALL_CHANNEL" = attrs.field(
repr=False,
)
"""Channel before this event. MISSING if it was not cached before"""
after: "BaseChannel" = attrs.field(
after: "TYPE_ALL_CHANNEL" = attrs.field(
repr=False,
)
"""Channel after this event"""
Expand Down Expand Up @@ -226,7 +232,7 @@ class ThreadListSync(BaseEvent):
repr=False,
)
"""The parent channel ids whose threads are being synced. If omitted, then threads were synced for the entire guild. This array may contain channel_ids that have no active threads as well, so you know to clear that data."""
threads: List["BaseChannel"] = attrs.field(
threads: List["TYPE_ALL_CHANNEL"] = attrs.field(
repr=False,
)
"""all active threads in the given channels that the current user can access"""
Expand Down Expand Up @@ -618,7 +624,7 @@ class TypingStart(BaseEvent):
repr=False,
)
"""The user who started typing"""
channel: "BaseChannel" = attrs.field(
channel: "TYPE_ALL_CHANNEL" = attrs.field(
repr=False,
)
"""The channel typing is in"""
Expand Down
7 changes: 4 additions & 3 deletions interactions/api/events/processors/auto_mod.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import TYPE_CHECKING

from interactions.models.discord.auto_mod import AutoModerationAction, AutoModRule
from ._template import EventMixinTemplate, Processor

from ... import events
from ._template import EventMixinTemplate, Processor

if TYPE_CHECKING:
from interactions.api.events import RawGatewayEvent
Expand All @@ -25,13 +26,13 @@ async def raw_auto_moderation_rule_create(self, event: "RawGatewayEvent") -> Non
self.dispatch(events.AutoModCreated(guild, rule))

@Processor.define()
async def raw_auto_moderation_rule_delete(self, event: "RawGatewayEvent") -> None:
async def raw_auto_moderation_rule_update(self, event: "RawGatewayEvent") -> None:
rule = AutoModRule.from_dict(event.data, self)
guild = self.get_guild(event.data["guild_id"])
self.dispatch(events.AutoModUpdated(guild, rule))

@Processor.define()
async def raw_auto_moderation_rule_update(self, event: "RawGatewayEvent") -> None:
async def raw_auto_moderation_rule_delete(self, event: "RawGatewayEvent") -> None:
rule = AutoModRule.from_dict(event.data, self)
guild = self.get_guild(event.data["guild_id"])
self.dispatch(events.AutoModDeleted(guild, rule))
12 changes: 8 additions & 4 deletions interactions/api/events/processors/integrations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import TYPE_CHECKING

from interactions.models.discord.app_perms import ApplicationCommandPermission, CommandPermissions
from interactions.models.discord.app_perms import (
ApplicationCommandPermission,
CommandPermissions,
)
from interactions.models.discord.snowflake import to_snowflake
from ._template import EventMixinTemplate, Processor

from ... import events
from ._template import EventMixinTemplate, Processor

if TYPE_CHECKING:
from interactions.api.events import RawGatewayEvent
Expand All @@ -17,6 +21,7 @@ async def _raw_application_command_permissions_update(self, event: "RawGatewayEv
perms = [ApplicationCommandPermission.from_dict(perm, self) for perm in event.data["permissions"]]
guild_id = to_snowflake(event.data["guild_id"])
command_id = to_snowflake(event.data["id"])
application_id = to_snowflake(event.data["application_id"])

if guild := self.get_guild(guild_id):
if guild.permissions:
Expand All @@ -27,5 +32,4 @@ async def _raw_application_command_permissions_update(self, event: "RawGatewayEv

command_permissions = guild.command_permissions[command_id]
command_permissions.update_permissions(*perms)

self.dispatch(events.ApplicationCommandPermissionsUpdate(guild, perms))
self.dispatch(events.ApplicationCommandPermissionsUpdate(command_id, guild_id, application_id, perms))
12 changes: 6 additions & 6 deletions interactions/api/events/processors/role_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ async def _on_raw_guild_role_create(self, event: "RawGatewayEvent") -> None:
g_id = int(event.data.get("guild_id"))
r_id = int(event.data["role"]["id"])

guild = self.cache.get_guild(g_id)
guild._role_ids.add(r_id)
if guild := self.cache.get_guild(g_id):
guild._role_ids.add(r_id)

role = self.cache.place_role_data(g_id, [event.data.get("role")])[r_id]
self.dispatch(events.RoleCreate(g_id, role))
Expand All @@ -39,13 +39,13 @@ async def _on_raw_guild_role_delete(self, event: "RawGatewayEvent") -> None:
g_id = int(event.data.get("guild_id"))
r_id = int(event.data.get("role_id"))

guild = self.cache.get_guild(g_id)
role = self.cache.get_role(r_id)

self.cache.delete_role(r_id)

role_members = (member for member in guild.members if member.has_role(r_id))
for member in role_members:
member._role_ids.remove(r_id)
if guild := self.cache.get_guild(g_id):
role_members = (member for member in guild.members if member.has_role(r_id))
for member in role_members:
member._role_ids.remove(r_id)

self.dispatch(events.RoleDelete(g_id, r_id, role))
18 changes: 11 additions & 7 deletions interactions/api/events/processors/voice_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
class VoiceEvents(EventMixinTemplate):
@Processor.define()
async def _on_raw_voice_state_update(self, event: "RawGatewayEvent") -> None:
before = copy.copy(self.cache.get_voice_state(event.data["user_id"])) or None
after = await self.cache.place_voice_state_data(event.data)

self.dispatch(events.VoiceStateUpdate(before, after))

if before and before.user_id == self.user.id:
if vc := self.cache.get_bot_voice_state(event.data["guild_id"]):
if str(event.data["user_id"]) == str(self.user.id):
# User is the bot itself
before = copy.copy(self.cache.get_bot_voice_state(event.data["guild_id"])) or None
after = await self.cache.place_voice_state_data(event.data, update_cache=False)
if vc := before:
# noinspection PyProtectedMember
await vc._voice_state_update(before, after, event.data)
else:
# User is not the bot
before = copy.copy(self.cache.get_voice_state(event.data["user_id"])) or None
after = await self.cache.place_voice_state_data(event.data)

self.dispatch(events.VoiceStateUpdate(before, after))

if before and after:
if (before.mute != after.mute) or (before.self_mute != after.self_mute):
Expand Down
38 changes: 22 additions & 16 deletions interactions/api/gateway/gateway.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Outlines the interaction between interactions and Discord's Gateway API."""
import asyncio
import logging
import sys
import time
import zlib
Expand Down Expand Up @@ -171,31 +172,32 @@ async def run(self) -> None:
async def dispatch_opcode(self, data, op: OPCODE) -> None:
match op:
case OPCODE.HEARTBEAT:
self.logger.debug("Received heartbeat request from gateway")
self.state.wrapped_logger(logging.DEBUG, "❤ Received heartbeat request from gateway")
return await self.send_heartbeat()

case OPCODE.HEARTBEAT_ACK:
self._latency.append(time.perf_counter() - self._last_heartbeat)

if self._last_heartbeat != 0 and self._latency[-1] >= 15:
self.logger.warning(
f"High Latency! shard ID {self.shard[0]} heartbeat took {self._latency[-1]:.1f}s to be acknowledged!"
self.state.wrapped_logger(
logging.WARNING,
f"❤ High Latency! shard ID {self.shard[0]} heartbeat took {self._latency[-1]:.1f}s to be acknowledged!",
)
else:
self.logger.debug(f"❤ Heartbeat acknowledged after {self._latency[-1]:.5f} seconds")
self.state.wrapped_logger(logging.DEBUG, "❤ Received heartbeat acknowledgement from gateway")

return self._acknowledged.set()

case OPCODE.RECONNECT:
self.logger.debug("Gateway requested reconnect. Reconnecting...")
self.state.wrapped_logger(logging.DEBUG, "Gateway requested reconnect. Reconnecting...")
return await self.reconnect(resume=True, url=self.ws_resume_url)

case OPCODE.INVALIDATE_SESSION:
self.logger.warning("Gateway has invalidated session! Reconnecting...")
self.state.wrapped_logger(logging.WARNING, "Gateway invalidated session. Reconnecting...")
return await self.reconnect()

case _:
return self.logger.debug(f"Unhandled OPCODE: {op} = {OPCODE(op).name}")
return self.state.wrapped_logger(logging.DEBUG, f"Unhandled OPCODE: {op} = {OPCODE(op).name}")

async def dispatch_event(self, data, seq, event) -> None:
match event:
Expand All @@ -207,12 +209,14 @@ async def dispatch_event(self, data, seq, event) -> None:
self.ws_resume_url = (
f"{data['resume_gateway_url']}?encoding=json&v={__api_version__}&compress=zlib-stream"
)
self.logger.info(f"Shard {self.shard[0]} has connected to gateway!")
self.logger.debug(f"Session ID: {self.session_id} Trace: {self._trace}")
self.state.wrapped_logger(logging.INFO, "Gateway connection established")
self.state.wrapped_logger(logging.DEBUG, f"Session ID: {self.session_id} Trace: {self._trace}")
return self.state.client.dispatch(events.WebsocketReady(data))

case "RESUMED":
self.logger.info(f"Successfully resumed connection! Session_ID: {self.session_id}")
self.state.wrapped_logger(
logging.INFO, f"Successfully resumed connection! Session_ID: {self.session_id}"
)
self.state.client.dispatch(events.Resume())
return None

Expand All @@ -228,9 +232,11 @@ async def dispatch_event(self, data, seq, event) -> None:
processor(events.RawGatewayEvent(data.copy(), override_name=event_name))
)
except Exception as ex:
self.logger.error(f"Failed to run event processor for {event_name}: {ex}")
self.state.wrapped_logger(
logging.ERROR, f"Failed to run event processor for {event_name}: {ex}"
)
else:
self.logger.debug(f"No processor for `{event_name}`")
self.state.wrapped_logger(logging.DEBUG, f"No processor for `{event_name}`")

self.state.client.dispatch(events.RawGatewayEvent(data.copy(), override_name="raw_gateway_event"))
self.state.client.dispatch(events.RawGatewayEvent(data.copy(), override_name=f"raw_{event.lower()}"))
Expand Down Expand Up @@ -263,8 +269,8 @@ async def _identify(self) -> None:
serialized = FastJson.dumps(payload)
await self.ws.send_str(serialized)

self.logger.debug(
f"Shard ID {self.shard[0]} has identified itself to Gateway, requesting intents: {self.state.intents}!"
self.state.wrapped_logger(
logging.DEBUG, f"Identification payload sent to gateway, requesting intents: {self.state.intents}"
)

async def reconnect(self, *, resume: bool = False, code: int = 1012, url: str | None = None) -> None:
Expand All @@ -289,11 +295,11 @@ async def _resume_connection(self) -> None:
serialized = FastJson.dumps(payload)
await self.ws.send_str(serialized)

self.logger.debug(f"{self.shard[0]} is attempting to resume a connection")
self.state.wrapped_logger(logging.DEBUG, f"Resume payload sent to gateway, session ID: {self.session_id}")

async def send_heartbeat(self) -> None:
await self.send_json({"op": OPCODE.HEARTBEAT, "d": self.sequence}, bypass=True)
self.logger.debug(f"❤ Shard {self.shard[0]} is sending a Heartbeat")
self.state.wrapped_logger(logging.DEBUG, "❤ Gateway is sending a Heartbeat")

async def change_presence(self, activity=None, status: Status = Status.ONLINE, since=None) -> None:
"""Update the bot's presence status."""
Expand Down
Loading