Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for guild forums #1430

Merged
merged 1 commit into from
Jan 1, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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
324 changes: 321 additions & 3 deletions hikari/api/rest.py

Large diffs are not rendered by default.

189 changes: 188 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,147 @@ class GuildStageChannel(PermissibleGuildChannel):
"""


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

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, converter=snowflakes.Snowflake, 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, int, emojis.Emoji, 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, emojis.CustomEmoji)):
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 +1703,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