From e13e29a796eafbfdd2cd6b4747f9fb98000d25c6 Mon Sep 17 00:00:00 2001 From: nulldomain Date: Sat, 3 Aug 2024 21:02:22 +0100 Subject: [PATCH] Implement Stage Instances (#1725) Signed-off-by: nulldomain Signed-off-by: thomm.o <43570299+tandemdude@users.noreply.github.com> Co-authored-by: Ashwin Vinod Co-authored-by: davfsa Co-authored-by: tandemdude <43570299+tandemdude@users.noreply.github.com> --- changes/1725.feature.md | 1 + hikari/__init__.py | 1 + hikari/__init__.pyi | 1 + hikari/api/entity_factory.py | 20 +++ hikari/api/event_factory.py | 62 +++++++++ hikari/api/rest.py | 170 +++++++++++++++++++++++ hikari/audit_logs.py | 3 + hikari/events/__init__.py | 1 + hikari/events/__init__.pyi | 1 + hikari/events/stage_events.py | 96 +++++++++++++ hikari/impl/entity_factory.py | 16 +++ hikari/impl/event_factory.py | 26 ++++ hikari/impl/event_manager.py | 19 +++ hikari/impl/rest.py | 56 ++++++++ hikari/internal/routes.py | 6 + hikari/stage_instances.py | 80 +++++++++++ tests/hikari/events/test_stage_events.py | 58 ++++++++ tests/hikari/impl/test_entity_factory.py | 28 ++++ tests/hikari/impl/test_event_factory.py | 53 +++++++ tests/hikari/impl/test_event_manager.py | 69 +++++++++ tests/hikari/impl/test_rest.py | 70 ++++++++++ tests/hikari/test_stage_instances.py | 71 ++++++++++ 22 files changed, 908 insertions(+) create mode 100644 changes/1725.feature.md create mode 100644 hikari/events/stage_events.py create mode 100644 hikari/stage_instances.py create mode 100644 tests/hikari/events/test_stage_events.py create mode 100644 tests/hikari/test_stage_instances.py diff --git a/changes/1725.feature.md b/changes/1725.feature.md new file mode 100644 index 0000000000..c9c02fd0cb --- /dev/null +++ b/changes/1725.feature.md @@ -0,0 +1 @@ +Implement stage instances diff --git a/hikari/__init__.py b/hikari/__init__.py index 33a19959ad..3e17ff1444 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -126,6 +126,7 @@ from hikari.snowflakes import SnowflakeishOr from hikari.snowflakes import SnowflakeishSequence from hikari.snowflakes import Unique +from hikari.stage_instances import * from hikari.stickers import * from hikari.templates import * from hikari.traits import * diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi index 97c59acd0c..4ea13a1bd2 100644 --- a/hikari/__init__.pyi +++ b/hikari/__init__.pyi @@ -100,6 +100,7 @@ from hikari.snowflakes import Snowflakeish as Snowflakeish from hikari.snowflakes import SnowflakeishOr as SnowflakeishOr from hikari.snowflakes import SnowflakeishSequence as SnowflakeishSequence from hikari.snowflakes import Unique as Unique +from hikari.stage_instances import * from hikari.stickers import * from hikari.templates import * from hikari.traits import * diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 5eb97f5df4..9b84f68648 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -46,6 +46,7 @@ from hikari import scheduled_events as scheduled_events_models from hikari import sessions as gateway_models from hikari import snowflakes + from hikari import stage_instances from hikari import stickers as sticker_models from hikari import templates as template_models from hikari import users as user_models @@ -1967,3 +1968,22 @@ def deserialize_sku(self, payload: data_binding.JSONObject) -> entitlement_model hikari.monetization.SKU The deserialized SKU object. """ + + ######################### + # STAGE INSTANCE MODELS # + ######################### + + @abc.abstractmethod + def deserialize_stage_instance(self, payload: data_binding.JSONObject) -> stage_instances.StageInstance: + """Parse a raw payload from Discord into a guild stage instance object. + + Parameters + ---------- + payload : hikari.internal.data_binding.JSONObject + The JSON payload to deserialize. + + Returns + ------- + hikari.stage_intances.StageInstance + The deserialized stage instance object + """ diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index 38b9bb18d1..6996c22d6d 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -53,6 +53,7 @@ from hikari.events import role_events from hikari.events import scheduled_events from hikari.events import shard_events + from hikari.events import stage_events from hikari.events import typing_events from hikari.events import user_events from hikari.events import voice_events @@ -1408,3 +1409,64 @@ def deserialize_entitlement_update_event( hikari.events.entitlement_events.EntitlementUpdateEvent The parsed entitlement update event object. """ + + ######################### + # STAGE INSTANCE EVENTS # + ######################### + + @abc.abstractmethod + def deserialize_stage_instance_create_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> stage_events.StageInstanceCreateEvent: + """Parse a raw payload from Discord into a stage instance create event object. + + Parameters + ---------- + shard + The shard that emitted this event. + payload + The dict payload to parse. + + Returns + ------- + hikari.events.stage_events.StageInstanceCreateEvent + The parsed stage instance create event object. + """ + + @abc.abstractmethod + def deserialize_stage_instance_update_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> stage_events.StageInstanceUpdateEvent: + """Parse a raw payload from Discord into a stage instance update event object. + + Parameters + ---------- + shard + The shard that emitted this event. + payload + The dict payload to parse. + + Returns + ------- + hikari.events.stage_events.StageInstanceUpdateEvent + The parsed stage instance update event object. + """ + + @abc.abstractmethod + def deserialize_stage_instance_delete_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> stage_events.StageInstanceDeleteEvent: + """Parse a raw payload from Discord into a stage instance delete event object. + + Parameters + ---------- + shard + The shard that emitted this event. + payload + The dict payload to parse. + + Returns + ------- + hikari.events.stage_events.StageInstanceDeleteEvent + The parsed stage instance delete event object. + """ diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 23cc255d34..7f9bda6e5e 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -51,6 +51,7 @@ from hikari import permissions as permissions_ from hikari import sessions from hikari import snowflakes + from hikari import stage_instances from hikari import stickers as stickers_ from hikari import templates from hikari import users @@ -8206,3 +8207,172 @@ async def delete_test_entitlement( hikari.errors.InternalServerError If an internal error occurs on Discord while handling the request. """ + + @abc.abstractmethod + async def fetch_stage_instance( + self, channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel] + ) -> stage_instances.StageInstance: + """Fetch the stage instance associated with a guild stage channel. + + Parameters + ---------- + channel + The guild stage channel to fetch the stage instance from. + + Returns + ------- + hikari.stage_instances.StageInstance + The stage instance associated with the guild stage channel. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the stage instance or channel is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def create_stage_instance( + self, + channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel], + *, + topic: str, + privacy_level: undefined.UndefinedOr[typing.Union[int, stage_instances.StageInstancePrivacyLevel]], + send_start_notification: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + scheduled_event_id: undefined.UndefinedOr[ + snowflakes.SnowflakeishOr[scheduled_events.ScheduledEvent] + ] = undefined.UNDEFINED, + ) -> stage_instances.StageInstance: + """Create a stage instance in guild stage channel. + + Parameters + ---------- + channel + The channel to use for the stage instance creation. + topic + The topic for the stage instance. + privacy_level + The privacy level for the stage instance. + send_start_notification + Whether to send a notification to *all* server members that the stage instance has started. + scheduled_event_id + The ID of the scheduled event to associate with the stage instance. + + + Returns + ------- + hikari.stage_instances.StageInstance + The created stage instance. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the interaction or response is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def edit_stage_instance( + self, + channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel], + *, + topic: undefined.UndefinedOr[str] = undefined.UNDEFINED, + privacy_level: undefined.UndefinedOr[ + typing.Union[int, stage_instances.StageInstancePrivacyLevel] + ] = undefined.UNDEFINED, + ) -> stage_instances.StageInstance: + """Edit the stage instance in a guild stage channel. + + Parameters + ---------- + channel + The channel that the stage instance is associated with. + topic + The topic for the stage instance. + privacy_level: + The privacy level for the stage instance. + + Returns + ------- + hikari.stage_instances.StageInstance + The edited stage instance. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token + or you are not a moderator of the stage instance). + hikari.errors.NotFoundError + If the interaction or response is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def delete_stage_instance(self, channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel]) -> None: + """Delete the stage instance. + + Parameters + ---------- + channel + The guild stage channel to fetch the stage instance from. + + Raises + ------ + hikari.errors.UnauthorizedError + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFoundError + If the interaction or response is not found. + hikari.errors.RateLimitTooLongError + Raised in the event that a rate limit occurs that is + longer than `max_rate_limit` when making a request. + hikari.errors.RateLimitedError + Usually, Hikari will handle and retry on hitting + rate-limits automatically. This includes most bucket-specific + rate-limits and global rate-limits. In some rare edge cases, + however, Discord implements other undocumented rules for + rate-limiting, such as limits per attribute. These cannot be + detected or handled normally by Hikari due to their undocumented + nature, and will trigger this exception if they occur. + hikari.errors.InternalServerError + If an internal error occurs on Discord while handling the request. + """ diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 85031b73bf..ad7add20cc 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -327,6 +327,9 @@ class AuditLogEventType(int, enums.Enum): INTEGRATION_CREATE = 80 INTEGRATION_UPDATE = 81 INTEGRATION_DELETE = 82 + STAGE_INSTANCE_CREATE = 83 + STAGE_INSTANCE_UPDATE = 84 + STAGE_INSTANCE_DELETE = 85 STICKER_CREATE = 90 STICKER_UPDATE = 91 STICKER_DELETE = 92 diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index fe873aab2b..704437e6d5 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -38,6 +38,7 @@ from hikari.events.role_events import * from hikari.events.scheduled_events import * from hikari.events.shard_events import * +from hikari.events.stage_events import * from hikari.events.typing_events import * from hikari.events.user_events import * from hikari.events.voice_events import * diff --git a/hikari/events/__init__.pyi b/hikari/events/__init__.pyi index a1c55ea9c0..9e3f60a115 100644 --- a/hikari/events/__init__.pyi +++ b/hikari/events/__init__.pyi @@ -15,6 +15,7 @@ from hikari.events.reaction_events import * from hikari.events.role_events import * from hikari.events.scheduled_events import * from hikari.events.shard_events import * +from hikari.events.stage_events import * from hikari.events.typing_events import * from hikari.events.user_events import * from hikari.events.voice_events import * diff --git a/hikari/events/stage_events.py b/hikari/events/stage_events.py new file mode 100644 index 0000000000..d5201d1e8d --- /dev/null +++ b/hikari/events/stage_events.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Events that fire when stage instances are created/updated/deleted.""" + +from __future__ import annotations + +import abc +import typing + +import attr + +from hikari import intents +from hikari.events import base_events +from hikari.events import shard_events +from hikari.internal import attrs_extensions +from hikari.stage_instances import StageInstance + +if typing.TYPE_CHECKING: + from hikari import traits + from hikari.api import shard as gateway_shard + + +@base_events.requires_intents(intents.Intents.GUILDS) +class StageInstanceEvent(shard_events.ShardEvent, abc.ABC): + """Event base for any event that involves stage instances.""" + + __slots__: typing.Sequence[str] = () + + @property + def app(self) -> traits.RESTAware: + # <>. + return self.stage_instance.app + + @property + @abc.abstractmethod + def stage_instance(self) -> StageInstance: + """Stage instance that this event relates to.""" + + +@attrs_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.GUILDS) +class StageInstanceCreateEvent(StageInstanceEvent): + """Event fired when a stage instance is created.""" + + shard: gateway_shard.GatewayShard = attr.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + stage_instance: StageInstance = attr.field() + """The stage instance that was created.""" + + +@attrs_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.GUILDS) +class StageInstanceUpdateEvent(StageInstanceEvent): + """Event fired when a stage instance is updated.""" + + shard: gateway_shard.GatewayShard = attr.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + stage_instance: StageInstance = attr.field() + """The stage instance that was updated.""" + + +@attrs_extensions.with_copy +@attr.define(kw_only=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.GUILDS) +class StageInstanceDeleteEvent(StageInstanceEvent): + """Event fired when a stage instance is deleted.""" + + shard: gateway_shard.GatewayShard = attr.field(metadata={attrs_extensions.SKIP_DEEP_COPY: True}) + # <>. + + stage_instance: StageInstance = attr.field() + """The stage instance that was deleted.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 665a121839..d97f63136b 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -52,6 +52,7 @@ from hikari import scheduled_events as scheduled_events_models from hikari import sessions as gateway_models from hikari import snowflakes +from hikari import stage_instances from hikari import stickers as sticker_models from hikari import templates as template_models from hikari import traits @@ -1470,6 +1471,21 @@ def deserialize_guild_private_thread( thread_created_at=thread_created_at, ) + def deserialize_stage_instance(self, payload: data_binding.JSONObject) -> stage_instances.StageInstance: + raw_event_id = payload["guild_scheduled_event_id"] + guild_scheduled_event_id = snowflakes.Snowflake(raw_event_id) if raw_event_id else None + + return stage_instances.StageInstance( + app=self._app, + id=snowflakes.Snowflake(payload["id"]), + channel_id=snowflakes.Snowflake(payload["channel_id"]), + guild_id=snowflakes.Snowflake(payload["guild_id"]), + topic=payload["topic"], + privacy_level=stage_instances.StageInstancePrivacyLevel(payload["privacy_level"]), + discoverable_disabled=payload["discoverable_disabled"], + scheduled_event_id=guild_scheduled_event_id, + ) + def deserialize_channel( self, payload: data_binding.JSONObject, diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 465c72caf0..be264a74d0 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -50,6 +50,7 @@ from hikari.events import role_events from hikari.events import scheduled_events from hikari.events import shard_events +from hikari.events import stage_events from hikari.events import typing_events from hikari.events import user_events from hikari.events import voice_events @@ -954,3 +955,28 @@ def deserialize_entitlement_delete_event( return monetization_events.EntitlementDeleteEvent( app=self._app, shard=shard, entitlement=self._app.entity_factory.deserialize_entitlement(payload) ) + + ######################### + # STAGE INSTANCE EVENTS # + ######################### + + def deserialize_stage_instance_create_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> stage_events.StageInstanceCreateEvent: + return stage_events.StageInstanceCreateEvent( + shard=shard, stage_instance=self._app.entity_factory.deserialize_stage_instance(payload) + ) + + def deserialize_stage_instance_update_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> stage_events.StageInstanceUpdateEvent: + return stage_events.StageInstanceUpdateEvent( + shard=shard, stage_instance=self._app.entity_factory.deserialize_stage_instance(payload) + ) + + def deserialize_stage_instance_delete_event( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> stage_events.StageInstanceDeleteEvent: + return stage_events.StageInstanceDeleteEvent( + shard=shard, stage_instance=self._app.entity_factory.deserialize_stage_instance(payload) + ) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 683b4fc984..395046764f 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -48,6 +48,7 @@ from hikari.events import role_events from hikari.events import scheduled_events from hikari.events import shard_events +from hikari.events import stage_events from hikari.events import typing_events from hikari.events import user_events from hikari.events import voice_events @@ -888,3 +889,21 @@ async def on_entitlement_delete(self, shard: gateway_shard.GatewayShard, payload async def on_entitlement_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway-events#entitlement-update for more info.""" await self.dispatch(self._event_factory.deserialize_entitlement_update_event(shard, payload)) + + @event_manager_base.filtered(stage_events.StageInstanceCreateEvent) + async def on_stage_instance_create( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + await self.dispatch(self._event_factory.deserialize_stage_instance_create_event(shard, payload)) + + @event_manager_base.filtered(stage_events.StageInstanceUpdateEvent) + async def on_stage_instance_update( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + await self.dispatch(self._event_factory.deserialize_stage_instance_update_event(shard, payload)) + + @event_manager_base.filtered(stage_events.StageInstanceDeleteEvent) + async def on_stage_instance_delete( + self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject + ) -> None: + await self.dispatch(self._event_factory.deserialize_stage_instance_delete_event(shard, payload)) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 9d3d4b5f1d..e734cdb65b 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -63,6 +63,7 @@ from hikari import permissions as permissions_ from hikari import scheduled_events from hikari import snowflakes +from hikari import stage_instances from hikari import traits from hikari import undefined from hikari import urls @@ -4467,3 +4468,58 @@ async def delete_test_entitlement( ) -> None: route = routes.DELETE_APPLICATION_TEST_ENTITLEMENT.compile(application=application, entitlement=entitlement) await self._request(route) + + async def fetch_stage_instance( + self, channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel] + ) -> stage_instances.StageInstance: + route = routes.GET_STAGE_INSTANCE.compile(channel=channel) + response = await self._request(route) + assert isinstance(response, dict) + return self._entity_factory.deserialize_stage_instance(response) + + async def create_stage_instance( + self, + channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel], + *, + topic: str, + privacy_level: undefined.UndefinedOr[ + typing.Union[int, stage_instances.StageInstancePrivacyLevel] + ] = undefined.UNDEFINED, + send_start_notification: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + scheduled_event_id: undefined.UndefinedOr[ + snowflakes.SnowflakeishOr[scheduled_events.ScheduledEvent] + ] = undefined.UNDEFINED, + ) -> stage_instances.StageInstance: + route = routes.POST_STAGE_INSTANCE.compile() + body = data_binding.JSONObjectBuilder() + body.put_snowflake("channel_id", channel) + body.put("topic", topic) + body.put("privacy_level", privacy_level) + body.put("send_start_notification", send_start_notification) + body.put_snowflake("guild_scheduled_event_id", scheduled_event_id) + + response = await self._request(route, json=body) + assert isinstance(response, dict) + return self._entity_factory.deserialize_stage_instance(response) + + async def edit_stage_instance( + self, + channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel], + *, + topic: undefined.UndefinedOr[str] = undefined.UNDEFINED, + privacy_level: undefined.UndefinedOr[ + typing.Union[int, stage_instances.StageInstancePrivacyLevel] + ] = undefined.UNDEFINED, + ) -> stage_instances.StageInstance: + route = routes.PATCH_STAGE_INSTANCE.compile(channel=channel) + body = data_binding.JSONObjectBuilder() + body.put("topic", topic) + body.put("privacy_level", privacy_level) + + response = await self._request(route, json=body) + assert isinstance(response, dict) + return self._entity_factory.deserialize_stage_instance(response) + + async def delete_stage_instance(self, channel: snowflakes.SnowflakeishOr[channels_.GuildStageChannel]) -> None: + route = routes.DELETE_STAGE_INSTANCE.compile(channel=channel) + await self._request(route) diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py index ad2c572ce2..40bbc77ae2 100644 --- a/hikari/internal/routes.py +++ b/hikari/internal/routes.py @@ -340,6 +340,12 @@ def compile_to_file( POST_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(POST, "/channels/{channel}/webhooks") GET_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(GET, "/channels/{channel}/webhooks") +# Stage instances +POST_STAGE_INSTANCE: typing.Final[Route] = Route(POST, "/stage-instances") +GET_STAGE_INSTANCE: typing.Final[Route] = Route(GET, "/stage-instances/{channel}") +PATCH_STAGE_INSTANCE: typing.Final[Route] = Route(PATCH, "/stage-instances/{channel}") +DELETE_STAGE_INSTANCE: typing.Final[Route] = Route(DELETE, "/stage-instances/{channel}") + # Reactions GET_REACTIONS: typing.Final[Route] = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") DELETE_ALL_REACTIONS: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}/reactions") diff --git a/hikari/stage_instances.py b/hikari/stage_instances.py new file mode 100644 index 0000000000..7a99781c4e --- /dev/null +++ b/hikari/stage_instances.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Application and entities that are used to describe stage instances on Discord.""" +from __future__ import annotations + +__all__: typing.Sequence[str] = ("StageInstancePrivacyLevel", "StageInstance") + +import typing + +import attr + +from hikari import scheduled_events +from hikari import snowflakes +from hikari import traits +from hikari.internal import attrs_extensions +from hikari.internal import enums + + +@typing.final +class StageInstancePrivacyLevel(int, enums.Enum): + """The privacy level of a stage instance.""" + + PUBLIC = 1 + """The Stage instance is visible publicly.""" + + GUILD_ONLY = 2 + """The Stage instance is visible to only guild members.""" + + +@attr.define(hash=True, kw_only=True, weakref_slot=False) +class StageInstance(snowflakes.Unique): + """Represents a stage instance.""" + + id: snowflakes.Snowflake = attr.field(eq=False, hash=False, repr=True) + """ID of the stage instance.""" + + app: traits.RESTAware = attr.field( + repr=False, eq=False, hash=False, metadata={attrs_extensions.SKIP_DEEP_COPY: True} + ) + """The client application that models may use for procedures.""" + + channel_id: snowflakes.Snowflake = attr.field(hash=True, repr=False) + """The channel ID of the stage instance.""" + + guild_id: snowflakes.Snowflake = attr.field(hash=True, repr=False) + """The guild ID of the stage instance.""" + + topic: str = attr.field(eq=False, hash=False, repr=False) + """The topic of the stage instance.""" + + privacy_level: StageInstancePrivacyLevel = attr.field(eq=False, hash=False, repr=False) + """The privacy level of the stage instance.""" + + discoverable_disabled: bool = attr.field(eq=False, hash=False, repr=False) + """Whether or not stage discovery is disabled.""" + + scheduled_event_id: typing.Optional[snowflakes.SnowflakeishOr[scheduled_events.ScheduledEvent]] = attr.field( + eq=False, hash=False, repr=False + ) + """The ID of the scheduled event for this stage instance, if it exists.""" diff --git a/tests/hikari/events/test_stage_events.py b/tests/hikari/events/test_stage_events.py new file mode 100644 index 0000000000..38a8c6b9dc --- /dev/null +++ b/tests/hikari/events/test_stage_events.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import mock +import pytest + +from hikari import stage_instances +from hikari.events import stage_events + + +class TestStageInstanceCreateEvent: + @pytest.fixture + def event(self): + return stage_events.StageInstanceCreateEvent(shard=object(), stage_instance=mock.Mock()) + + def test_app_property(self, event): + assert event.app is event.stage_instance.app + + +class TestStageInstanceUpdateEvent: + @pytest.fixture + def event(self): + return stage_events.StageInstanceUpdateEvent( + shard=object(), stage_instance=mock.Mock(stage_instances.StageInstance) + ) + + def test_app_property(self, event): + assert event.app is event.stage_instance.app + + +class TestStageInstanceDeleteEvent: + @pytest.fixture + def event(self): + return stage_events.StageInstanceDeleteEvent( + shard=object(), stage_instance=mock.Mock(stage_instances.StageInstance) + ) + + def test_app_property(self, event): + assert event.app is event.stage_instance.app diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 6adcede333..fff056c67d 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -45,6 +45,7 @@ from hikari import scheduled_events as scheduled_event_models from hikari import sessions as gateway_models from hikari import snowflakes +from hikari import stage_instances as stage_instance_models from hikari import stickers as sticker_models from hikari import traits from hikari import undefined @@ -7210,3 +7211,30 @@ def test_deserialize_sku(self, entity_factory_impl, sku_payload): assert sku.slug == "hashire-sori-yo-kaze-no-you-ni-tsukimihara-wo-padoru-padoru" assert sku.flags == (monetization_models.SKUFlags.AVAILABLE | monetization_models.SKUFlags.GUILD_SUBSCRIPTION) assert isinstance(sku, monetization_models.SKU) + + ######################### + # Stage instance models # + ######################### + + @pytest.fixture + def stage_instance_payload(self): + return { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 123", + "privacy_level": 2, + "guild_scheduled_event_id": "363820363920323120", + "discoverable_disabled": False, + } + + def test_deserialize_stage_instance(self, entity_factory_impl, stage_instance_payload, mock_app): + stage_instance = entity_factory_impl.deserialize_stage_instance(stage_instance_payload) + + assert stage_instance.app is mock_app + assert stage_instance.id == 840647391636226060 + assert stage_instance.channel_id == 733488538393510049 + assert stage_instance.guild_id == 197038439483310086 + assert stage_instance.topic == "Testing Testing, 123" + assert stage_instance.privacy_level == stage_instance_models.StageInstancePrivacyLevel.GUILD_ONLY + assert stage_instance.discoverable_disabled is False diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py index cb862ff7e3..4fbf2c87fe 100644 --- a/tests/hikari/impl/test_event_factory.py +++ b/tests/hikari/impl/test_event_factory.py @@ -42,6 +42,7 @@ from hikari.events import role_events from hikari.events import scheduled_events from hikari.events import shard_events +from hikari.events import stage_events from hikari.events import typing_events from hikari.events import user_events from hikari.events import voice_events @@ -1517,3 +1518,55 @@ def test_deserialize_entitlement_delete_event(self, event_factory, mock_app, moc event = event_factory.deserialize_entitlement_delete_event(mock_shard, payload) assert isinstance(event, monetization_events.EntitlementDeleteEvent) + + ######################### + # STAGE INSTANCE EVENTS # + ######################### + + def test_deserialize_stage_instance_create_event(self, event_factory, mock_app, mock_shard): + mock_payload = { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 123", + "privacy_level": 1, + "discoverable_disabled": False, + } + event = event_factory.deserialize_stage_instance_create_event(mock_shard, mock_payload) + assert isinstance(event, stage_events.StageInstanceCreateEvent) + + assert event.shard is mock_shard + assert event.app is event.stage_instance.app + assert event.stage_instance == mock_app.entity_factory.deserialize_stage_instance.return_value + + def test_deserialize_stage_instance_update_event(self, event_factory, mock_app, mock_shard): + mock_payload = { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 124", + "privacy_level": 2, + "discoverable_disabled": True, + } + event = event_factory.deserialize_stage_instance_update_event(mock_shard, mock_payload) + assert isinstance(event, stage_events.StageInstanceUpdateEvent) + + assert event.shard is mock_shard + assert event.app is event.stage_instance.app + assert event.stage_instance == mock_app.entity_factory.deserialize_stage_instance.return_value + + def test_deserialize_stage_instance_delete_event(self, event_factory, mock_app, mock_shard): + mock_payload = { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 124", + "privacy_level": 2, + "discoverable_disabled": True, + } + event = event_factory.deserialize_stage_instance_delete_event(mock_shard, mock_payload) + assert isinstance(event, stage_events.StageInstanceDeleteEvent) + + assert event.shard is mock_shard + assert event.app is event.stage_instance.app + assert event.stage_instance == mock_app.entity_factory.deserialize_stage_instance.return_value diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py index 4f28f7029d..2d331e8b0d 100644 --- a/tests/hikari/impl/test_event_manager.py +++ b/tests/hikari/impl/test_event_manager.py @@ -1692,3 +1692,72 @@ async def test_on_guild_audit_log_entry_create( event_manager_impl.dispatch.assert_awaited_once_with( event_factory.deserialize_audit_log_entry_create_event.return_value ) + + @pytest.mark.asyncio + async def test_on_stage_instance_create( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + payload = { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 123", + "privacy_level": 1, + "discoverable_disabled": False, + } + + await event_manager_impl.on_stage_instance_create(shard, payload) + + event_factory.deserialize_stage_instance_create_event.assert_called_once_with(shard, payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_stage_instance_create_event.return_value + ) + + @pytest.mark.asyncio + async def test_on_stage_instance_update( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + payload = { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 123", + "privacy_level": 1, + "discoverable_disabled": False, + } + + await event_manager_impl.on_stage_instance_update(shard, payload) + + event_factory.deserialize_stage_instance_update_event.assert_called_once_with(shard, payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_stage_instance_update_event.return_value + ) + + @pytest.mark.asyncio + async def test_on_stage_instance_delete( + self, + event_manager_impl: event_manager.EventManagerImpl, + shard: mock.Mock, + event_factory: event_factory_.EventFactory, + ): + payload = { + "id": "840647391636226060", + "guild_id": "197038439483310086", + "channel_id": "733488538393510049", + "topic": "Testing Testing, 123", + "privacy_level": 1, + "discoverable_disabled": False, + } + + await event_manager_impl.on_stage_instance_delete(shard, payload) + + event_factory.deserialize_stage_instance_delete_event.assert_called_once_with(shard, payload) + event_manager_impl.dispatch.assert_awaited_once_with( + event_factory.deserialize_stage_instance_delete_event.return_value + ) diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index a781f1f6ca..74e90eadbf 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -47,6 +47,7 @@ from hikari import permissions from hikari import scheduled_events from hikari import snowflakes +from hikari import stage_instances from hikari import undefined from hikari import urls from hikari import users @@ -6431,3 +6432,72 @@ async def test_delete_scheduled_event(self, rest_client: rest.RESTClientImpl): await rest_client.delete_scheduled_event(StubModel(54531123), StubModel(123321123321)) rest_client._request.assert_awaited_once_with(expected_route) + + async def test_fetch_stage_instance(self, rest_client): + expected_route = routes.GET_STAGE_INSTANCE.compile(channel=123) + mock_payload = { + "id": "8406", + "guild_id": "19703", + "channel_id": "123", + "topic": "ur mom", + "privacy_level": 1, + "discoverable_disabled": False, + } + rest_client._request = mock.AsyncMock(return_value=mock_payload) + + result = await rest_client.fetch_stage_instance(channel=StubModel(123)) + + assert result is rest_client._entity_factory.deserialize_stage_instance.return_value + rest_client._request.assert_called_once_with(expected_route) + rest_client._entity_factory.deserialize_stage_instance.assert_called_once_with(mock_payload) + + async def test_create_stage_instance(self, rest_client): + expected_route = routes.POST_STAGE_INSTANCE.compile() + expected_json = {"channel_id": "7334", "topic": "ur mom", "guild_scheduled_event_id": "3361203239"} + mock_payload = { + "id": "8406", + "guild_id": "19703", + "channel_id": "7334", + "topic": "ur mom", + "privacy_level": 2, + "guild_scheduled_event_id": "3361203239", + "discoverable_disabled": False, + } + rest_client._request = mock.AsyncMock(return_value=mock_payload) + + result = await rest_client.create_stage_instance( + channel=StubModel(7334), topic="ur mom", scheduled_event_id=StubModel(3361203239) + ) + + assert result is rest_client._entity_factory.deserialize_stage_instance.return_value + rest_client._request.assert_called_once_with(expected_route, json=expected_json) + rest_client._entity_factory.deserialize_stage_instance.assert_called_once_with(mock_payload) + + async def test_edit_stage_instance(self, rest_client): + expected_route = routes.PATCH_STAGE_INSTANCE.compile(channel=7334) + expected_json = {"topic": "ur mom", "privacy_level": 2} + mock_payload = { + "id": "8406", + "guild_id": "19703", + "channel_id": "7334", + "topic": "ur mom", + "privacy_level": 2, + "discoverable_disabled": False, + } + rest_client._request = mock.AsyncMock(return_value=mock_payload) + + result = await rest_client.edit_stage_instance( + channel=StubModel(7334), topic="ur mom", privacy_level=stage_instances.StageInstancePrivacyLevel.GUILD_ONLY + ) + + assert result is rest_client._entity_factory.deserialize_stage_instance.return_value + rest_client._request.assert_called_once_with(expected_route, json=expected_json) + rest_client._entity_factory.deserialize_stage_instance.assert_called_once_with(mock_payload) + + async def test_delete_stage_instance(self, rest_client): + expected_route = routes.DELETE_STAGE_INSTANCE.compile(channel=7334) + rest_client._request = mock.AsyncMock() + + await rest_client.delete_stage_instance(channel=StubModel(7334)) + + rest_client._request.assert_called_once_with(expected_route) diff --git a/tests/hikari/test_stage_instances.py b/tests/hikari/test_stage_instances.py new file mode 100644 index 0000000000..42e1289626 --- /dev/null +++ b/tests/hikari/test_stage_instances.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# Copyright (c) 2021-present davfsa +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import mock +import pytest + +from hikari import snowflakes +from hikari import stage_instances + + +@pytest.fixture +def mock_app(): + return mock.Mock() + + +class TestStageInstance: + @pytest.fixture + def stage_instance(self, mock_app): + return stage_instances.StageInstance( + app=mock_app, + id=snowflakes.Snowflake(123), + channel_id=snowflakes.Snowflake(6969), + guild_id=snowflakes.Snowflake(420), + topic="beanos", + privacy_level=stage_instances.StageInstancePrivacyLevel.GUILD_ONLY, + discoverable_disabled=True, + scheduled_event_id=snowflakes.Snowflake(1337), + ) + + def test_id_property(self, stage_instance): + assert stage_instance.id == 123 + + def test_app_property(self, stage_instance, mock_app): + assert stage_instance.app is mock_app + + def test_channel_id_property(self, stage_instance): + assert stage_instance.channel_id == 6969 + + def test_guild_id_property(self, stage_instance): + assert stage_instance.guild_id == 420 + + def test_topic_property(self, stage_instance): + assert stage_instance.topic == "beanos" + + def test_privacy_level_property(self, stage_instance): + assert stage_instance.privacy_level == stage_instances.StageInstancePrivacyLevel.GUILD_ONLY + + def test_discoverable_disabled_property(self, stage_instance): + assert stage_instance.discoverable_disabled is True + + def test_guild_scheduled_event_id_property(self, stage_instance): + assert stage_instance.scheduled_event_id == 1337