twilight_model/channel/
forum.rs

1use crate::id::{
2    marker::{EmojiMarker, TagMarker},
3    Id,
4};
5use serde::{Deserialize, Serialize};
6
7/// Emoji to use as the default way to react to a forum post.
8///
9/// Exactly one of `emoji_id` and `emoji_name` must be set.
10#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
11pub struct DefaultReaction {
12    /// ID of custom guild emoji.
13    ///
14    /// Conflicts with `emoji_name`.
15    pub emoji_id: Option<Id<EmojiMarker>>,
16    /// Unicode emoji character.
17    ///
18    /// Conflicts with `emoji_id`.
19    pub emoji_name: Option<String>,
20}
21
22/// Layout of a [channel] that is a [forum].
23///
24/// [channel]: super::Channel
25/// [forum]: super::ChannelType::GuildForum
26#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
27#[non_exhaustive]
28#[serde(from = "u8", into = "u8")]
29pub enum ForumLayout {
30    /// Display posts as a collection of tiles.
31    GalleryView,
32    /// Display posts as a list.
33    ListView,
34    /// No default has been set for the forum channel.
35    NotSet,
36    /// Variant value is unknown to the library.
37    Unknown(u8),
38}
39
40impl ForumLayout {
41    pub const fn name(self) -> &'static str {
42        match self {
43            Self::ListView => "ListView",
44            Self::NotSet => "NotSet",
45            Self::GalleryView => "GalleryView",
46            Self::Unknown(_) => "Unknown",
47        }
48    }
49}
50
51impl From<u8> for ForumLayout {
52    fn from(value: u8) -> Self {
53        match value {
54            0 => Self::NotSet,
55            1 => Self::ListView,
56            2 => Self::GalleryView,
57            unknown => Self::Unknown(unknown),
58        }
59    }
60}
61
62impl From<ForumLayout> for u8 {
63    fn from(value: ForumLayout) -> Self {
64        match value {
65            ForumLayout::NotSet => 0,
66            ForumLayout::ListView => 1,
67            ForumLayout::GalleryView => 2,
68            ForumLayout::Unknown(unknown) => unknown,
69        }
70    }
71}
72
73/// Layout of a [channel] that is a [forum].
74///
75/// [channel]: super::Channel
76/// [forum]: super::ChannelType::GuildForum
77#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
78#[non_exhaustive]
79#[serde(from = "u8", into = "u8")]
80pub enum ForumSortOrder {
81    /// Sort forum posts by creation time (from most recent to oldest).
82    CreationDate,
83    /// Sort forum posts by activity.
84    LatestActivity,
85    /// Variant value is unknown to the library.
86    Unknown(u8),
87}
88
89impl ForumSortOrder {
90    pub const fn name(self) -> &'static str {
91        match self {
92            Self::CreationDate => "CreationDate",
93            Self::LatestActivity => "LatestActivity",
94            Self::Unknown(_) => "Unknown",
95        }
96    }
97}
98
99impl From<u8> for ForumSortOrder {
100    fn from(value: u8) -> Self {
101        match value {
102            0 => Self::LatestActivity,
103            1 => Self::CreationDate,
104            unknown => Self::Unknown(unknown),
105        }
106    }
107}
108
109impl From<ForumSortOrder> for u8 {
110    fn from(value: ForumSortOrder) -> Self {
111        match value {
112            ForumSortOrder::LatestActivity => 0,
113            ForumSortOrder::CreationDate => 1,
114            ForumSortOrder::Unknown(unknown) => unknown,
115        }
116    }
117}
118
119/// Tag that is able to be applied to a thread in a [`GuildForum`] [`Channel`].
120///
121/// May at most contain one of `emoji_id` and `emoji_name`.
122///
123/// [`Channel`]: super::Channel
124/// [`GuildForum`]: super::ChannelType::GuildForum
125#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
126pub struct ForumTag {
127    /// ID of custom guild emoji.
128    ///
129    /// Some guilds can have forum tags that have an ID of 0; if this is the
130    /// case, then the emoji ID is `None`.
131    ///
132    /// Conflicts with `emoji_name`.
133    #[serde(with = "crate::visitor::zeroable_id")]
134    pub emoji_id: Option<Id<EmojiMarker>>,
135    /// Unicode emoji character.
136    ///
137    /// Conflicts with `emoji_name`.
138    pub emoji_name: Option<String>,
139    /// ID of the tag.
140    pub id: Id<TagMarker>,
141    /// Whether the tag can only be added or removed by [`Member`]s with the
142    /// [`MANAGE_THREADS`] permission.
143    ///
144    /// [`MANAGE_THREADS`]: crate::guild::Permissions::MANAGE_THREADS
145    /// [`Member`]: crate::guild::Member
146    pub moderated: bool,
147    /// Name of the tag (0--20 characters).
148    pub name: String,
149}
150
151#[cfg(test)]
152mod tests {
153    use super::{DefaultReaction, ForumLayout, ForumSortOrder, ForumTag};
154    use crate::id::{
155        marker::{EmojiMarker, TagMarker},
156        Id,
157    };
158    use serde::{Deserialize, Serialize};
159    use serde_test::{assert_tokens, Token};
160    use static_assertions::assert_impl_all;
161    use std::{fmt::Debug, hash::Hash};
162
163    assert_impl_all!(
164        ForumLayout: Clone,
165        Copy,
166        Debug,
167        Deserialize<'static>,
168        Eq,
169        Hash,
170        PartialEq,
171        Send,
172        Serialize,
173        Sync
174    );
175    assert_impl_all!(
176        ForumSortOrder: Clone,
177        Copy,
178        Debug,
179        Deserialize<'static>,
180        Eq,
181        Hash,
182        PartialEq,
183        Send,
184        Serialize,
185        Sync
186    );
187
188    const EMOJI_ID: Id<EmojiMarker> = Id::new(1);
189    const TAG_ID: Id<TagMarker> = Id::new(2);
190
191    #[test]
192    fn default_reaction() {
193        let value = DefaultReaction {
194            emoji_id: None,
195            emoji_name: Some("name".to_owned()),
196        };
197
198        serde_test::assert_tokens(
199            &value,
200            &[
201                Token::Struct {
202                    name: "DefaultReaction",
203                    len: 2,
204                },
205                Token::Str("emoji_id"),
206                Token::None,
207                Token::Str("emoji_name"),
208                Token::Some,
209                Token::Str("name"),
210                Token::StructEnd,
211            ],
212        );
213    }
214
215    #[test]
216    fn forum_layout() {
217        const MAP: &[(ForumLayout, u8, &str)] = &[
218            (ForumLayout::NotSet, 0, "NotSet"),
219            (ForumLayout::ListView, 1, "ListView"),
220            (ForumLayout::GalleryView, 2, "GalleryView"),
221            (ForumLayout::Unknown(3), 3, "Unknown"),
222        ];
223
224        for (layout, number, name) in MAP {
225            assert_eq!(layout.name(), *name);
226            assert_eq!(u8::from(*layout), *number);
227            assert_eq!(ForumLayout::from(*number), *layout);
228            assert_tokens(layout, &[Token::U8(*number)]);
229        }
230    }
231
232    #[test]
233    fn forum_sort_order() {
234        const MAP: &[(ForumSortOrder, u8, &str)] = &[
235            (ForumSortOrder::LatestActivity, 0, "LatestActivity"),
236            (ForumSortOrder::CreationDate, 1, "CreationDate"),
237            (ForumSortOrder::Unknown(100), 100, "Unknown"),
238        ];
239
240        for (layout, number, name) in MAP {
241            assert_eq!(layout.name(), *name);
242            assert_eq!(u8::from(*layout), *number);
243            assert_eq!(ForumSortOrder::from(*number), *layout);
244            assert_tokens(layout, &[Token::U8(*number)]);
245        }
246    }
247
248    /// Assert the (de)serialization of a forum tag with an emoji name and no
249    /// emoji ID.
250    #[test]
251    fn forum_tag_emoji_name() {
252        let value = ForumTag {
253            emoji_id: None,
254            emoji_name: Some("emoji".to_owned()),
255            id: TAG_ID,
256            moderated: true,
257            name: "tag".into(),
258        };
259
260        serde_test::assert_tokens(
261            &value,
262            &[
263                Token::Struct {
264                    name: "ForumTag",
265                    len: 5,
266                },
267                Token::Str("emoji_id"),
268                Token::None,
269                Token::Str("emoji_name"),
270                Token::Some,
271                Token::Str("emoji"),
272                Token::Str("id"),
273                Token::NewtypeStruct { name: "Id" },
274                Token::Str("2"),
275                Token::Str("moderated"),
276                Token::Bool(true),
277                Token::Str("name"),
278                Token::Str("tag"),
279                Token::StructEnd,
280            ],
281        );
282    }
283
284    #[test]
285    fn forum_tag() {
286        let value = ForumTag {
287            emoji_id: Some(EMOJI_ID),
288            emoji_name: None,
289            id: TAG_ID,
290            moderated: false,
291            name: "other".into(),
292        };
293
294        serde_test::assert_tokens(
295            &value,
296            &[
297                Token::Struct {
298                    name: "ForumTag",
299                    len: 5,
300                },
301                Token::Str("emoji_id"),
302                Token::Some,
303                Token::NewtypeStruct { name: "Id" },
304                Token::Str("1"),
305                Token::Str("emoji_name"),
306                Token::None,
307                Token::Str("id"),
308                Token::NewtypeStruct { name: "Id" },
309                Token::Str("2"),
310                Token::Str("moderated"),
311                Token::Bool(false),
312                Token::Str("name"),
313                Token::Str("other"),
314                Token::StructEnd,
315            ],
316        );
317    }
318
319    /// Assert that an emoji ID can be deserialized from a string value of "0".
320    ///
321    /// This is a bug on Discord's end that has consistently been causing issues
322    /// for Twilight users.
323    #[test]
324    fn forum_tag_emoji_id_zero() {
325        let value = ForumTag {
326            emoji_id: None,
327            emoji_name: None,
328            id: TAG_ID,
329            moderated: true,
330            name: "tag".into(),
331        };
332
333        serde_test::assert_de_tokens(
334            &value,
335            &[
336                Token::Struct {
337                    name: "ForumTag",
338                    len: 5,
339                },
340                Token::Str("emoji_id"),
341                Token::U64(0),
342                Token::Str("emoji_name"),
343                Token::None,
344                Token::Str("id"),
345                Token::NewtypeStruct { name: "Id" },
346                Token::Str("2"),
347                Token::Str("moderated"),
348                Token::Bool(true),
349                Token::Str("name"),
350                Token::Str("tag"),
351                Token::StructEnd,
352            ],
353        );
354
355        serde_test::assert_de_tokens(
356            &value,
357            &[
358                Token::Struct {
359                    name: "ForumTag",
360                    len: 5,
361                },
362                Token::Str("emoji_id"),
363                Token::Unit,
364                Token::Str("emoji_name"),
365                Token::None,
366                Token::Str("id"),
367                Token::NewtypeStruct { name: "Id" },
368                Token::Str("2"),
369                Token::Str("moderated"),
370                Token::Bool(true),
371                Token::Str("name"),
372                Token::Str("tag"),
373                Token::StructEnd,
374            ],
375        );
376    }
377}