Skip to content

Commit 0312190

Browse files
authored
feat: Implement barebones Forum channel support. (#766)
* docs, feat: Implement barebones Forum channel support. * docs: Update channel attributes. * feat: Implement barebones create thread in forum function. * feat: Implement tags support, implement creating post in Forums. * fix: Include headers. * chore: Remove redundant slots, update flag headers. * feat: Apply previous forum breaking change, document tags in http method.
1 parent ad679f9 commit 0312190

File tree

4 files changed

+174
-1
lines changed

4 files changed

+174
-1
lines changed

interactions/api/http/channel.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,72 @@ async def delete_stage_instance(self, channel_id: int, reason: Optional[str] = N
307307
return await self._req.request(
308308
Route("DELETE", f"/stage-instances/{channel_id}"), reason=reason
309309
)
310+
311+
async def create_tag(
312+
self,
313+
channel_id: int,
314+
name: str,
315+
emoji_id: Optional[int] = None,
316+
emoji_name: Optional[str] = None,
317+
) -> dict:
318+
"""
319+
Create a new tag.
320+
321+
.. note::
322+
Can either have an emoji_id or an emoji_name, but not both.
323+
emoji_id is meant for custom emojis, emoji_name is meant for unicode emojis.
324+
325+
:param channel_id: Channel ID snowflake.
326+
:param name: The name of the tag
327+
:param emoji_id: The ID of the emoji to use for the tag
328+
:param emoji_name: The name of the emoji to use for the tag
329+
"""
330+
331+
_dct = {"name": name}
332+
if emoji_id:
333+
_dct["emoji_id"] = emoji_id
334+
if emoji_name:
335+
_dct["emoji_name"] = emoji_name
336+
337+
return await self._req.request(Route("POST", f"/channels/{channel_id}/tags"), json=_dct)
338+
339+
async def edit_tag(
340+
self,
341+
channel_id: int,
342+
tag_id: int,
343+
name: str,
344+
emoji_id: Optional[int] = None,
345+
emoji_name: Optional[str] = None,
346+
) -> dict:
347+
"""
348+
Update a tag.
349+
350+
.. note::
351+
Can either have an emoji_id or an emoji_name, but not both.
352+
emoji_id is meant for custom emojis, emoji_name is meant for unicode emojis.
353+
354+
:param channel_id: Channel ID snowflake.
355+
:param tag_id: The ID of the tag to update.
356+
:param name: The new name of the tag
357+
:param emoji_id: The ID of the emoji to use for the tag
358+
:param emoji_name: The name of the emoji to use for the tag
359+
"""
360+
361+
_dct = {"name": name}
362+
if emoji_id:
363+
_dct["emoji_id"] = emoji_id
364+
if emoji_name:
365+
_dct["emoji_name"] = emoji_name
366+
367+
return await self._req.request(
368+
Route("PUT", f"/channels/{channel_id}/tags/{tag_id}"), json=_dct
369+
)
370+
371+
async def delete_tag(self, channel_id: int, tag_id: int) -> dict:
372+
"""
373+
Delete a forum tag.
374+
375+
:param channel_id: Channel ID snowflake.
376+
:param tag_id: The ID of the tag to delete
377+
"""
378+
return await self._req.request(Route("DELETE", f"/channels/{channel_id}/tags/{tag_id}"))

interactions/api/http/thread.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from typing import Dict, List, Optional
22

3+
from aiohttp import MultipartWriter
4+
35
from ...api.cache import Cache
6+
from ...utils.missing import MISSING
47
from ..models.channel import Channel
8+
from ..models.misc import File
59
from .request import _Request
610
from .route import Route
711

@@ -189,3 +193,62 @@ async def create_thread(
189193
self.cache[Channel].add(Channel(**request))
190194

191195
return request
196+
197+
async def create_thread_in_forum(
198+
self,
199+
channel_id: int,
200+
name: str,
201+
auto_archive_duration: int,
202+
message_payload: dict,
203+
applied_tags: List[str] = None,
204+
files: Optional[List[File]] = MISSING,
205+
rate_limit_per_user: Optional[int] = None,
206+
reason: Optional[str] = None,
207+
) -> dict:
208+
"""
209+
From a given Forum channel, create a Thread with a message to start with.
210+
211+
:param channel_id: The ID of the channel to create this thread in
212+
:param name: The name of the thread
213+
:param auto_archive_duration: duration in minutes to automatically archive the thread after recent activity,
214+
can be set to: 60, 1440, 4320, 10080
215+
:param message_payload: The payload/dictionary contents of the first message in the forum thread.
216+
:param applied_tags: List of tag ids that can be applied to the forum, if any.
217+
:param files: An optional list of files to send attached to the message.
218+
:param rate_limit_per_user: Seconds a user has to wait before sending another message (0 to 21600), if given.
219+
:param reason: An optional reason for the audit log
220+
:return: Returns a Thread in a Forum object with a starting Message.
221+
"""
222+
query = {"use_nested_fields": 1}
223+
224+
payload = {"name": name, "auto_archive_duration": auto_archive_duration}
225+
if rate_limit_per_user:
226+
payload["rate_limit_per_user"] = rate_limit_per_user
227+
if applied_tags:
228+
payload["applied_tags"] = applied_tags
229+
230+
data = None
231+
if files is not MISSING and len(files) > 0:
232+
233+
data = MultipartWriter("form-data")
234+
part = data.append_json(payload)
235+
part.set_content_disposition("form-data", name="payload_json")
236+
payload = None
237+
238+
for id, file in enumerate(files):
239+
part = data.append(
240+
file._fp,
241+
)
242+
part.set_content_disposition(
243+
"form-data", name=f"files[{str(id)}]", filename=file._filename
244+
)
245+
else:
246+
payload.update(message_payload)
247+
248+
return await self._req.request(
249+
Route("POST", f"/channels/{channel_id}/threads"),
250+
json=payload,
251+
data=data,
252+
params=query,
253+
reason=reason,
254+
)

interactions/api/models/channel.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from ...utils.missing import MISSING
1919
from ..error import LibraryException
20+
from .emoji import Emoji
2021
from .flags import Permissions
2122
from .misc import AllowedMentions, File, IDMixin, Overwrite, Snowflake
2223
from .user import User
@@ -36,6 +37,8 @@
3637
"ThreadMember",
3738
"ThreadMetadata",
3839
"AsyncHistoryIterator",
40+
"AsyncTypingContextManager",
41+
"Tags",
3942
)
4043

4144

@@ -53,6 +56,7 @@ class ChannelType(IntEnum):
5356
PUBLIC_THREAD = 11
5457
PRIVATE_THREAD = 12
5558
GUILD_STAGE_VOICE = 13
59+
GUILD_DIRECTORY = 14
5660
GUILD_FORUM = 15
5761

5862

@@ -280,6 +284,30 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
280284
self.__task.cancel()
281285

282286

287+
@define()
288+
class Tags(DictSerializerMixin):
289+
"""
290+
An object denoting a tag object within a forum channel.
291+
292+
.. note::
293+
If the emoji is custom, it won't have name information.
294+
295+
:ivar str name: Name of the tag. The limit is up to 20 characters.
296+
:ivar int id: ID of the tag. Can also be 0 if manually created.
297+
:ivar bool moderated: A boolean denoting whether this tag can be removed/added by moderators with ``manage_threads`` permissions.
298+
:ivar Optional[Emoji] emoji?: The emoji to represent the tag, if any.
299+
300+
"""
301+
302+
# TODO: Rename these to discord-docs
303+
name: str = field()
304+
id: int = field()
305+
moderated: bool = field()
306+
emoji: Optional[Emoji] = field(converter=Emoji, default=None)
307+
308+
# Maybe on post_attrs_init replace emoji object with one from cache for name population?
309+
310+
283311
@define()
284312
class Channel(ClientSerializerMixin, IDMixin):
285313
"""
@@ -311,14 +339,20 @@ class Channel(ClientSerializerMixin, IDMixin):
311339
:ivar Optional[int] video_quality_mode?: The set quality mode for video streaming in the channel.
312340
:ivar int message_count: The amount of messages in the channel.
313341
:ivar Optional[int] member_count?: The amount of members in the channel.
342+
:ivar Optional[bool] newly_created?: Boolean representing if a thread is created.
314343
:ivar Optional[ThreadMetadata] thread_metadata?: The thread metadata of the channel.
315344
:ivar Optional[ThreadMember] member?: The member of the thread in the channel.
316345
:ivar Optional[int] default_auto_archive_duration?: The set auto-archive time for all threads to naturally follow in the channel.
317346
:ivar Optional[str] permissions?: The permissions of the channel.
318347
:ivar Optional[int] flags?: The flags of the channel.
319348
:ivar Optional[int] total_message_sent?: Number of messages ever sent in a thread.
349+
:ivar Optional[int] default_thread_slowmode_delay?: The default slowmode delay in seconds for threads, if this channel is a forum.
350+
:ivar Optional[List[Tags]] available_tags: Tags in a forum channel, if any.
351+
:ivar Optional[Emoji] default_reaction_emoji: Default reaction emoji for threads created in a forum, if any.
320352
"""
321353

354+
# Template attribute isn't live/documented, this line exists as a placeholder 'TODO' of sorts
355+
322356
__slots__ = (
323357
# TODO: Document banner when Discord officially documents them.
324358
"banner",
@@ -351,6 +385,7 @@ class Channel(ClientSerializerMixin, IDMixin):
351385
video_quality_mode: Optional[int] = field(default=None, repr=False)
352386
message_count: Optional[int] = field(default=None, repr=False)
353387
member_count: Optional[int] = field(default=None, repr=False)
388+
newly_created: Optional[int] = field(default=None, repr=False)
354389
thread_metadata: Optional[ThreadMetadata] = field(converter=ThreadMetadata, default=None)
355390
member: Optional[ThreadMember] = field(
356391
converter=ThreadMember, default=None, add_client=True, repr=False
@@ -359,6 +394,9 @@ class Channel(ClientSerializerMixin, IDMixin):
359394
permissions: Optional[str] = field(default=None, repr=False)
360395
flags: Optional[int] = field(default=None, repr=False)
361396
total_message_sent: Optional[int] = field(default=None, repr=False)
397+
default_thread_slowmode_delay: Optional[int] = field(default=None, repr=False)
398+
tags: Optional[List[Tags]] = field(converter=convert_list(Tags), default=None, repr=False)
399+
default_reaction_emoji: Optional[Emoji] = field(converter=Emoji, default=None)
362400

363401
def __attrs_post_init__(self): # sourcery skip: last-if-guard
364402
if self._client:
@@ -1505,7 +1543,6 @@ async def get_permissions_for(self, member: "Member") -> Permissions:
15051543
@define()
15061544
class Thread(Channel):
15071545
"""An object representing a thread.
1508-
15091546
.. note::
15101547
This is a derivation of the base Channel, since a
15111548
thread can be its own event.

interactions/api/models/gw.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,10 @@ class MessageReactionRemove(MessageReaction):
517517
# todo see if the missing member attribute affects anything
518518

519519

520+
# Thread object typically used for ``THREAD_X`` is found in the channel models instead, as its identical.
521+
# and all attributes of Thread are in Channel.
522+
523+
520524
@define()
521525
class ThreadList(DictSerializerMixin):
522526
"""

0 commit comments

Comments
 (0)