diff --git a/changes/idk.feature.md b/changes/idk.feature.md new file mode 100644 index 0000000000..6b35b15cc3 --- /dev/null +++ b/changes/idk.feature.md @@ -0,0 +1 @@ +Add support for guild media channels. diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index a6ff73c438..ff8ce9f96e 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -851,6 +851,45 @@ def deserialize_guild_private_thread( `"guild_id"` is not present in the passed payload. """ + @abc.abstractmethod + def deserialize_guild_media_channel( + self, + payload: data_binding.JSONObject, + *, + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED, + ) -> channel_models.GuildMediaChannel: + """Parse a raw payload from Discord into a guild media channel object. + + Parameters + ---------- + payload : hikari.internal.data_binding.JSONObject + The JSON payload to deserialize. + + Other Parameters + ---------------- + guild_id : hikari.undefined.UndefinedOr[hikari.snowflakes.Snowflake] + The ID of the guild this channel belongs to. This will be ignored + for DM and group DM channels and will be prioritised over + `"guild_id"` in the payload when passed. + + This is necessary in GUILD_CREATE events, where `"guild_id"` is not + included in the channel's payload + + Returns + ------- + hikari.channels.GuildMediaChannel + The deserialized guild media channel object. + + Raises + ------ + KeyError + If `guild_id` is left as `hikari.undefined.UNDEFINED` when + `"guild_id"` is not present in the passed payload of a guild + channel. + hikari.errors.UnrecognisedEntityError + If the channel type is unknown. + """ + @abc.abstractmethod def deserialize_channel( self, diff --git a/hikari/channels.py b/hikari/channels.py index 7c7519d6a0..a462ea730a 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -50,6 +50,7 @@ "ForumLayoutType", "ForumTag", "GuildForumChannel", + "GuildMediaChannel", "GuildVoiceChannel", "GuildStageChannel", "WebhookChannelT", @@ -129,6 +130,10 @@ class ChannelType(int, enums.Enum): GUILD_FORUM = 15 """A channel consisting of a collection of public guild threads.""" + GUILD_MEDIA = 16 + """""" + + @typing.final class ChannelFlag(enums.Flag): @@ -1707,3 +1712,73 @@ class GuildPrivateThread(GuildThreadChannel): is_invitable: bool = attrs.field(eq=False, hash=False, repr=True) """Whether non-moderators can add other non-moderators to a private thread.""" + + +@attrs.define(hash=True, kw_only=True, weakref_slot=False) +class GuildMediaChannel(PermissibleGuildChannel): + """Represents a guild media channel.""" + + topic: typing.Optional[str] = attrs.field(eq=False, hash=False, repr=False) + """The guidelines for the channel.""" + + last_thread_id: typing.Optional[snowflakes.Snowflake] = attrs.field(eq=False, hash=False, repr=False) + """The ID of the last thread created in this channel. + + .. warning:: + This might point to an invalid or deleted message. Do not assume that + this will always be valid. + """ + + rate_limit_per_user: datetime.timedelta = attrs.field(eq=False, hash=False, repr=False) + """The delay (in seconds) between a user can create threads in this channel. + + If there is no rate limit, this will be 0 seconds. + + .. note:: + Any user that has permissions allowing `MANAGE_MESSAGES`, + `MANAGE_CHANNEL`, `ADMINISTRATOR` will not be limited. Likewise, bots + will not be affected by this rate limit. + """ + + default_thread_rate_limit_per_user: datetime.timedelta = attrs.field(eq=False, hash=False, repr=False) + """The default delay (in seconds) between a user can send a message in created threads. + + If there is no rate limit, this will be 0 seconds. + + .. note:: + Any user that has permissions allowing `MANAGE_MESSAGES`, + `MANAGE_CHANNEL`, `ADMINISTRATOR` will not be limited. Likewise, bots + will not be affected by this rate limit. + """ + + default_auto_archive_duration: datetime.timedelta = attrs.field(eq=False, hash=False, repr=False) + """The auto archive duration Discord's client defaults to for threads in this channel. + + This may be be either 1 hour, 1 day, 3 days or 1 week. + """ + + flags: ChannelFlag = attrs.field(eq=False, hash=False, repr=False) + """The channel flags for this channel. + + .. note:: + As of writing, the only flag that can be set is `ChannelFlag.REQUIRE_TAG`. + """ + + available_tags: typing.Sequence[ForumTag] = attrs.field(eq=False, hash=False, repr=False) + """The available tags to select from when creating a thread.""" + + default_sort_order: ForumSortOrderType = attrs.field(eq=False, hash=False, repr=False) + """The default sort order for the forum.""" + + default_reaction_emoji_id: typing.Optional[snowflakes.Snowflake] = attrs.field(eq=False, hash=False, repr=False) + """The ID of the default reaction emoji.""" + + default_reaction_emoji_name: typing.Union[str, emojis.UnicodeEmoji, None] = attrs.field( + eq=False, hash=False, repr=False + ) + """Name of the default reaction emoji. + + Either the string name of the custom emoji, the object + of the `hikari.emojis.UnicodeEmoji` or `None` when the relevant + custom emoji's data is not available (e.g. the emoji has been deleted). + """ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index aaa8fbedfa..57562ee1fc 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -528,6 +528,7 @@ def __init__(self, app: traits.RESTAware) -> None: channel_models.ChannelType.GUILD_VOICE: self.deserialize_guild_voice_channel, channel_models.ChannelType.GUILD_STAGE: self.deserialize_guild_stage_channel, channel_models.ChannelType.GUILD_FORUM: self.deserialize_guild_forum_channel, + channel_models.ChannelType.GUILD_MEDIA: self.deserialize_guild_media_channel } self._thread_channel_type_mapping = { channel_models.ChannelType.GUILD_NEWS_THREAD: self.deserialize_guild_news_thread, @@ -1309,7 +1310,7 @@ def deserialize_guild_thread( if deserialize := self._thread_channel_type_mapping.get(channel_type): return deserialize(payload, guild_id=guild_id, member=member, user_id=user_id) - _LOGGER.debug(f"Unrecognised thread channel type {channel_type}") + _LOGGER.debug("Unrecognised thread channel type %s", channel_type) raise errors.UnrecognisedEntityError(f"Unrecognised thread channel type {channel_type}") def deserialize_guild_news_thread( @@ -1464,6 +1465,79 @@ def deserialize_guild_private_thread( is_invitable=metadata["invitable"], thread_created_at=thread_created_at, ) + + def deserialize_guild_media_channel( + self, + payload: data_binding.JSONObject, + *, + guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED, + ) -> channel_models.GuildMediaChannel: + channel_fields = self._set_guild_channel_attrsibutes(payload, guild_id=guild_id) + + # Discord's docs are just wrong about this always being included. + default_auto_archive_duration = datetime.timedelta(minutes=payload.get("default_auto_archive_duration", 1440)) + default_thread_rate_limit_per_user = datetime.timedelta( + seconds=payload.get("default_thread_rate_limit_per_user", 0) + ) + + permission_overwrites = { + snowflakes.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) + for overwrite in payload["permission_overwrites"] + } + + last_thread_id: typing.Optional[snowflakes.Snowflake] = None + if raw_last_thread_id := payload.get("last_message_id"): + last_thread_id = snowflakes.Snowflake(raw_last_thread_id) + + available_tags: typing.List[channel_models.ForumTag] = [] + for tag_payload in payload.get("available_tags", ()): + tag_emoji: typing.Union[emoji_models.UnicodeEmoji, snowflakes.Snowflake, None] + if tag_emoji := tag_payload["emoji_id"]: + tag_emoji = snowflakes.Snowflake(tag_emoji) + + elif tag_emoji := tag_payload["emoji_name"]: + tag_emoji = emoji_models.UnicodeEmoji(tag_emoji) + + available_tags.append( + channel_models.ForumTag( + id=snowflakes.Snowflake(tag_payload["id"]), + name=tag_payload["name"], + moderated=tag_payload["moderated"], + emoji=tag_emoji, + ) + ) + + reaction_emoji_id: typing.Optional[snowflakes.Snowflake] = None + reaction_emoji_name: typing.Union[None, emoji_models.UnicodeEmoji, str] = None + if reaction_emoji_payload := payload.get("default_reaction_emoji"): + if reaction_emoji_id := reaction_emoji_payload["emoji_id"]: + reaction_emoji_id = snowflakes.Snowflake(reaction_emoji_id) + + if reaction_emoji_name := reaction_emoji_payload["emoji_name"]: + reaction_emoji_name = emoji_models.UnicodeEmoji(reaction_emoji_name) + + return channel_models.GuildMediaChannel( + app=self._app, + id=channel_fields.id, + name=channel_fields.name, + type=channel_fields.type, + guild_id=channel_fields.guild_id, + permission_overwrites=permission_overwrites, + is_nsfw=payload.get("nsfw", False), + parent_id=channel_fields.parent_id, + topic=payload["topic"], + last_thread_id=last_thread_id, + rate_limit_per_user=datetime.timedelta(seconds=payload.get("rate_limit_per_user", 0)), + default_thread_rate_limit_per_user=default_thread_rate_limit_per_user, + default_auto_archive_duration=default_auto_archive_duration, + position=int(payload["position"]), + available_tags=available_tags, + flags=channel_models.ChannelFlag(payload["flags"]), + # Discord's docs are just wrong about this never being null. + default_sort_order=channel_models.ForumSortOrderType(payload.get("default_sort_order") or 0), + default_reaction_emoji_id=reaction_emoji_id, + default_reaction_emoji_name=reaction_emoji_name, + ) def deserialize_channel( self, @@ -1481,7 +1555,7 @@ def deserialize_channel( if dm_channel_model := self._dm_channel_type_mapping.get(channel_type): return dm_channel_model(payload) - _LOGGER.debug(f"Unrecognised channel type {channel_type}") + _LOGGER.debug("Unrecognised channel type %s", channel_type) raise errors.UnrecognisedEntityError(f"Unrecognised channel type {channel_type}") ################ @@ -3427,7 +3501,7 @@ def deserialize_scheduled_event(self, payload: data_binding.JSONObject) -> sched if converter := self._scheduled_event_type_mapping.get(event_type): return converter(payload) - _LOGGER.debug(f"Unrecognised scheduled event type {event_type}") + _LOGGER.debug("Unrecognised scheduled event type %s", event_type) raise errors.UnrecognisedEntityError(f"Unrecognised scheduled event type {event_type}") def deserialize_scheduled_event_user( @@ -3701,5 +3775,5 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_model if converter := self._webhook_type_mapping.get(webhook_type): return converter(payload) - _LOGGER.debug(f"Unrecognised webhook type {webhook_type}") + _LOGGER.debug("Unrecognised webhook type %s", webhook_type) raise errors.UnrecognisedEntityError(f"Unrecognised webhook type {webhook_type}")