Skip to content

Commit

Permalink
Add support for guild forums
Browse files Browse the repository at this point in the history
  • Loading branch information
davfsa committed Dec 28, 2022
1 parent b01b5b9 commit 829db67
Show file tree
Hide file tree
Showing 11 changed files with 998 additions and 6 deletions.
1 change: 1 addition & 0 deletions changes/1430.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for guild forum channels.
50 changes: 50 additions & 0 deletions hikari/api/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,56 @@ def deserialize_guild_stage_channel(
`"guild_id"` is not present in the passed payload.
"""

@abc.abstractmethod
def deserialize_guild_forum_channel(
self,
payload: data_binding.JSONObject,
*,
guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
) -> channel_models.GuildForumChannel:
"""Parse a raw payload from Discord into a guild forum channel object.
Parameters
----------
payload : hikari.internal.data_binding.JSONObject
The JSON payload to deserialize.
Other Parameters
----------------
guild_id : hikari.snowflakes.Snowflake
The ID of the guild this channel belongs to. If passed then this
will be prioritised over `"guild_id"` in the payload.
This currently only covers the gateway `GUILD_CREATE` event,
where it is not included in the channel's payload.
Returns
-------
hikari.channels.GuildForumChannel
The deserialized guild forum channel object.
Raises
------
KeyError
If `guild_id` is left as `hikari.undefined.UNDEFINED` when
`"guild_id"` is not present in the passed payload.
"""

@abc.abstractmethod
def serialize_forum_tag(self, tag: channel_models.ForumTag) -> data_binding.JSONObject:
"""Serialize a forum tag object to a json serializable dict.
Parameters
----------
tag : hikari.channels.ForumTag
The forum tag object to serialize.
Returns
-------
hikari.internal.data_binding.JSONObject
The serialized representation of the forum tag.
"""

@abc.abstractmethod
def deserialize_thread_member(
self,
Expand Down
318 changes: 315 additions & 3 deletions hikari/api/rest.py

Large diffs are not rendered by default.

192 changes: 191 additions & 1 deletion hikari/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

__all__: typing.Sequence[str] = (
"ChannelType",
"ChannelFlag",
"VideoQualityMode",
"ChannelFollow",
"PermissionOverwrite",
Expand All @@ -45,6 +46,10 @@
"GuildNewsThread",
"GuildPrivateThread",
"GuildPublicThread",
"ForumSortOrderType",
"ForumLayoutType",
"ForumTag",
"GuildForumChannel",
"GuildVoiceChannel",
"GuildStageChannel",
"WebhookChannelT",
Expand All @@ -56,6 +61,7 @@

import attr

from hikari import emojis
from hikari import permissions
from hikari import snowflakes
from hikari import traits
Expand Down Expand Up @@ -119,6 +125,32 @@ class ChannelType(int, enums.Enum):
GUILD_STAGE = 13
"""A few to many voice channel for hosting events."""

GUILD_FORUM = 15
"""A channel consisting of a collection of public guild threads."""


@typing.final
class ChannelFlag(enums.Flag):
"""The flags for a channel."""

NONE = 0 << 1
"""None."""

PINNED = 1 << 1
"""The thread is pinned in a forum channel.
.. note::
As of writing, this can only be set for threads
belonging to a forum channel.
"""

REQUIRE_TAG = 1 << 4
"""Whether a tag is required to be specified when creating a thread in a forum channel
.. note::
As of writing, this can only be set for forum channels.
"""


@typing.final
class VideoQualityMode(int, enums.Enum):
Expand Down Expand Up @@ -1386,6 +1418,150 @@ class GuildStageChannel(PermissibleGuildChannel):
"""


@typing.final
class ForumSortOrderType(int, enums.Enum):
"""The sort order for forum channels."""

# Discord send `None` for NOT_SET, but, as ugly as this is here,
# it provides a nicer DX.
NOT_SET = -1
"""Not Set."""

LATEST_ACTIVITY = 0
"""Latest Activity."""

CREATION_DATE = 1
"""Creation Date."""


@typing.final
class ForumLayoutType(int, enums.Enum):
"""The layout type for forum channels."""

NOT_SET = 0
"""Not Set."""

LIST_VIEW = 1
"""List View."""

GALLERY_VIEW = 2
"""Gallery View."""


@attr.define(hash=True, kw_only=True, weakref_slot=False)
class ForumTag(snowflakes.Unique):
"""Represents a forum tag."""

id: snowflakes.Snowflake = attr.field(eq=True, hash=True, repr=True, factory=snowflakes.Snowflake.min)
"""The ID of the tag.
When creating tags, this will be `0`.
"""

name: str = attr.field(eq=False, hash=False, repr=True)
"""The name of the tag."""

moderated: bool = attr.field(eq=False, hash=False, repr=False, default=False)
"""The whether this flag can only be applied by moderators.
Moderators are those with `MANAGE_CHANNEL` or `ADMINISTRATOR` permissions.
"""

_emoji: typing.Union[str, emojis.Emoji, snowflakes.Snowflake, None] = attr.field(alias="emoji", default=None)
# Discord will send either emoji_id or emoji_name, never both.
# Thus, we can take in a generic "emoji" argument when the user
# creates the class and then demystify it later.

@property
def unicode_emoji(self) -> typing.Optional[emojis.UnicodeEmoji]:
"""Unicode emoji of this tag."""
if isinstance(self._emoji, str):
return emojis.UnicodeEmoji(self._emoji)

return None

@property
def emoji_id(self) -> typing.Optional[snowflakes.Snowflake]:
"""ID of the emoji of this tag."""
if isinstance(self._emoji, int):
return snowflakes.Snowflake(self._emoji)

return None


@attr.define(hash=True, kw_only=True, weakref_slot=False)
class GuildForumChannel(PermissibleGuildChannel):
"""Represents a guild forum channel."""

topic: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False)
"""The guidelines for the channel."""

last_thread_id: typing.Optional[snowflakes.Snowflake] = attr.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 = attr.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 = attr.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 = attr.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 = attr.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] = attr.field(eq=False, hash=False, repr=False)
"""The available tags to select from when creating a thread."""

default_sort_order: ForumSortOrderType = attr.field(eq=False, hash=False, repr=False)
"""The default sort order for the forum."""

default_layout: ForumLayoutType = attr.field(eq=False, hash=False, repr=False)
"""The default layout for the forum."""

default_reaction_emoji_id: typing.Optional[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=False)
"""The ID of the default reaction emoji."""

default_reaction_emoji_name: typing.Union[str, emojis.UnicodeEmoji, None] = attr.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).
"""


WebhookChannelT = typing.Union[GuildTextChannel, GuildNewsChannel]
"""Union of the channel types which incoming and follower webhooks can be attached to.
Expand Down Expand Up @@ -1530,10 +1706,24 @@ class GuildNewsThread(GuildThreadChannel):
__slots__: typing.Sequence[str] = ()


@attr.define(hash=True, kw_only=True, weakref_slot=False)
class GuildPublicThread(GuildThreadChannel):
"""Represents a non-news guild channel public thread."""

__slots__: typing.Sequence[str] = ()
applied_tag_ids: typing.Sequence[snowflakes.Snowflake] = attr.field(eq=False, hash=False, repr=False)
"""The IDs of the applied tags on this thread.
This will only apply to threads created inside a forum channel.
"""

flags: ChannelFlag = attr.field(eq=False, hash=False, repr=False)
"""The channel flags for this thread.
This will only apply to threads created inside a forum channel.
.. note::
As of writing, the only flag that can be set is `ChannelFlag.PINNED`.
"""


@attr.define(hash=True, kw_only=True, weakref_slot=False)
Expand Down
2 changes: 1 addition & 1 deletion hikari/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def _dump_errors(obj: data_binding.JSONObject, obj_string: str = "") -> str:
string = ""
for key, value in obj.items():
if isinstance(value, typing.Sequence):
string += obj_string + ":"
string += (obj_string or "root") + ":"

for item in value:
string += f"\n - {item['message']}"
Expand Down
Loading

0 comments on commit 829db67

Please sign in to comment.