Skip to main content

tetratto_core/model/
communities.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
5use super::communities_permissions::CommunityPermission;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Community {
9    pub id: usize,
10    pub created: usize,
11    pub title: String,
12    pub context: CommunityContext,
13    /// The ID of the owner of the community.
14    pub owner: usize,
15    /// Who can read the community.
16    pub read_access: CommunityReadAccess,
17    /// Who can write to the community (create posts belonging to it).
18    ///
19    /// The owner of the community (and moderators) are the ***only*** people
20    /// capable of removing posts.
21    pub write_access: CommunityWriteAccess,
22    /// Who can join the community.
23    pub join_access: CommunityJoinAccess,
24    pub likes: isize,
25    pub dislikes: isize,
26    pub member_count: usize,
27    pub post_count: usize,
28    pub is_forum: bool,
29    /// The topics of a community if the community has `is_forum` enabled.
30    ///
31    /// Since topics are given a unique ID (the key of the hashmap), a removal of a topic
32    /// should be done through a specific DELETE endpoint which ALSO deletes all posts
33    /// within the topic.
34    ///
35    /// Communities should be limited to 10 topics per community.
36    pub topics: HashMap<usize, ForumTopic>,
37}
38
39impl Community {
40    /// Create a new [`Community`].
41    pub fn new(title: String, owner: usize) -> Self {
42        Self {
43            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
44            created: unix_epoch_timestamp(),
45            title: title.clone(),
46            context: CommunityContext {
47                display_name: title,
48                ..Default::default()
49            },
50            owner,
51            read_access: CommunityReadAccess::default(),
52            write_access: CommunityWriteAccess::default(),
53            join_access: CommunityJoinAccess::default(),
54            likes: 0,
55            dislikes: 0,
56            member_count: 0,
57            post_count: 0,
58            is_forum: false,
59            topics: HashMap::new(),
60        }
61    }
62
63    /// Create the "void" community. This is where all posts with a deleted community
64    /// resolve to.
65    pub fn void() -> Self {
66        Self {
67            id: 0,
68            created: 0,
69            title: "void".to_string(),
70            context: CommunityContext::default(),
71            owner: 0,
72            read_access: CommunityReadAccess::Joined,
73            write_access: CommunityWriteAccess::Owner,
74            join_access: CommunityJoinAccess::Nobody,
75            likes: 0,
76            dislikes: 0,
77            member_count: 0,
78            post_count: 0,
79            is_forum: false,
80            topics: HashMap::new(),
81        }
82    }
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize, Default)]
86pub struct CommunityContext {
87    #[serde(default)]
88    pub display_name: String,
89    #[serde(default)]
90    pub description: String,
91    #[serde(default)]
92    pub is_nsfw: bool,
93    #[serde(default)]
94    pub enable_questions: bool,
95    /// If posts are allowed to set a `title` field.
96    #[serde(default)]
97    pub enable_titles: bool,
98    /// If posts are required to set a `title` field.
99    ///
100    /// `enable_titles` is required for this setting to work.
101    #[serde(default)]
102    pub require_titles: bool,
103}
104
105/// Who can read a [`Community`].
106#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
107pub enum CommunityReadAccess {
108    /// Everybody can view the community.
109    #[default]
110    Everybody,
111    /// Only people in the community can view the community.
112    Joined,
113}
114
115/// Who can write to a [`Community`].
116#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
117pub enum CommunityWriteAccess {
118    /// Everybody.
119    Everybody,
120    /// Only people who joined the community can write to it.
121    ///
122    /// Memberships can be managed by the owner of the community.
123    #[default]
124    Joined,
125    /// Only the owner of the community.
126    Owner,
127}
128
129/// Who can join a [`Community`].
130#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
131pub enum CommunityJoinAccess {
132    /// Joins are closed. Nobody can join the community.
133    Nobody,
134    /// All authenticated users can join the community.
135    #[default]
136    Everybody,
137    /// People must send a request to join.
138    Request,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct CommunityMembership {
143    pub id: usize,
144    pub created: usize,
145    pub owner: usize,
146    pub community: usize,
147    pub role: CommunityPermission,
148}
149
150impl CommunityMembership {
151    /// Create a new [`CommunityMembership`].
152    pub fn new(owner: usize, community: usize, role: CommunityPermission) -> Self {
153        Self {
154            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
155            created: unix_epoch_timestamp(),
156            owner,
157            community,
158            role,
159        }
160    }
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize)]
164pub struct PostContext {
165    #[serde(default = "default_comments_enabled")]
166    pub comments_enabled: bool,
167    #[serde(default)]
168    pub is_pinned: bool,
169    #[serde(default)]
170    pub is_profile_pinned: bool,
171    #[serde(default)]
172    pub edited: usize,
173    #[serde(default)]
174    pub is_nsfw: bool,
175    #[serde(default)]
176    pub repost: Option<RepostContext>,
177    #[serde(default = "default_reposts_enabled")]
178    pub reposts_enabled: bool,
179    /// The ID of the question this post is answering.
180    #[serde(default)]
181    pub answering: usize,
182    #[serde(default = "default_reactions_enabled")]
183    pub reactions_enabled: bool,
184    #[serde(default)]
185    pub content_warning: String,
186    #[serde(default)]
187    pub tags: Vec<String>,
188    #[serde(default)]
189    pub full_unlist: bool,
190}
191
192fn default_comments_enabled() -> bool {
193    true
194}
195
196fn default_reposts_enabled() -> bool {
197    true
198}
199
200fn default_reactions_enabled() -> bool {
201    true
202}
203
204impl Default for PostContext {
205    fn default() -> Self {
206        Self {
207            comments_enabled: default_comments_enabled(),
208            reposts_enabled: default_reposts_enabled(),
209            is_pinned: false,
210            is_profile_pinned: false,
211            edited: 0,
212            is_nsfw: false,
213            repost: None,
214            answering: 0,
215            reactions_enabled: default_reactions_enabled(),
216            content_warning: String::new(),
217            tags: Vec::new(),
218            full_unlist: false,
219        }
220    }
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize)]
224pub struct RepostContext {
225    /// Should be `false` is `reposting` is `Some`.
226    ///
227    /// Declares the post to be a repost of another post.
228    pub is_repost: bool,
229    /// Should be `None` if `is_repost` is true.
230    ///
231    /// Sets the ID of the other post to load.
232    pub reposting: Option<usize>,
233}
234
235#[derive(Clone, Debug, Serialize, Deserialize)]
236pub struct Post {
237    pub id: usize,
238    pub created: usize,
239    pub content: String,
240    /// The ID of the owner of this post.
241    pub owner: usize,
242    /// The ID of the [`Community`] this post belongs to.
243    pub community: usize,
244    /// Extra information about the post.
245    pub context: PostContext,
246    /// The ID of the post this post is a comment on.
247    pub replying_to: Option<usize>,
248    pub likes: isize,
249    pub dislikes: isize,
250    pub comment_count: usize,
251    /// IDs of all uploads linked to this post.
252    pub uploads: Vec<usize>,
253    /// If the post was deleted.
254    pub is_deleted: bool,
255    /// The ID of the poll associated with this post. 0 means no poll is connected.
256    pub poll_id: usize,
257    /// The title of the post (in communities where titles are enabled).
258    pub title: String,
259    /// The ID of the stack this post belongs to. 0 means no stack is connected.
260    ///
261    /// If stack is not 0, community should be 0 (and vice versa).
262    pub stack: usize,
263    /// The ID of the topic this post belongs to. 0 means no topic is connected.
264    ///
265    /// This can only be set if the post is created in a community with `is_forum: true`,
266    /// where this is also a required field.
267    pub topic: usize,
268    pub views: usize,
269}
270
271impl Post {
272    /// Create a new [`Post`].
273    pub fn new(
274        content: String,
275        community: usize,
276        replying_to: Option<usize>,
277        owner: usize,
278        poll_id: usize,
279    ) -> Self {
280        Self {
281            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
282            created: unix_epoch_timestamp(),
283            content,
284            owner,
285            community,
286            context: PostContext::default(),
287            replying_to,
288            likes: 0,
289            dislikes: 0,
290            comment_count: 0,
291            uploads: Vec::new(),
292            is_deleted: false,
293            poll_id,
294            title: String::new(),
295            stack: 0,
296            topic: 0,
297            views: 0,
298        }
299    }
300
301    /// Create a new [`Post`] (as a repost of the given `post_id`).
302    pub fn repost(content: String, community: usize, owner: usize, post_id: usize) -> Self {
303        let mut post = Self::new(content, community, None, owner, 0);
304
305        post.context.repost = Some(RepostContext {
306            is_repost: false,
307            reposting: Some(post_id),
308        });
309
310        post
311    }
312
313    /// Make the given post a reposted post.
314    pub fn mark_as_repost(&mut self) {
315        self.context.repost = Some(RepostContext {
316            is_repost: true,
317            reposting: None,
318        });
319    }
320}
321
322#[derive(Clone, Debug, Serialize, Deserialize)]
323pub struct PostView {
324    pub id: usize,
325    pub created: usize,
326    pub owner: usize,
327    pub post: usize,
328}
329
330impl PostView {
331    /// Create a new [`PostView`]
332    pub fn new(owner: usize, post: usize) -> Self {
333        Self {
334            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
335            created: unix_epoch_timestamp(),
336            owner,
337            post,
338        }
339    }
340}
341
342#[derive(Clone, Debug, Serialize, Deserialize)]
343pub struct Question {
344    pub id: usize,
345    pub created: usize,
346    pub owner: usize,
347    pub receiver: usize,
348    pub content: String,
349    /// The `is_global` flag allows any (authenticated) user to respond
350    /// to the question. Normally, only the `receiver` can do so.
351    ///
352    /// If `is_global` is true, `receiver` should be 0 (and vice versa).
353    pub is_global: bool,
354    /// The number of answers the question has. Should never really be changed
355    /// unless the question has `is_global` set to true.
356    pub answer_count: usize,
357    /// The ID of the community this question is asked to. This should only be > 0
358    /// if `is_global` is set to true.
359    pub community: usize,
360    // likes
361    #[serde(default)]
362    pub likes: isize,
363    #[serde(default)]
364    pub dislikes: isize,
365    // ...
366    #[serde(default)]
367    pub context: QuestionContext,
368    /// The IP of the question creator for IP blocking and identifying anonymous users.
369    #[serde(default)]
370    pub ip: String,
371    /// The IDs of all uploads which hold this question's drawings.
372    #[serde(default)]
373    pub drawings: Vec<usize>,
374}
375
376impl Question {
377    /// Create a new [`Question`].
378    pub fn new(
379        owner: usize,
380        receiver: usize,
381        content: String,
382        is_global: bool,
383        ip: String,
384    ) -> Self {
385        Self {
386            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
387            created: unix_epoch_timestamp(),
388            owner,
389            receiver,
390            content,
391            is_global,
392            answer_count: 0,
393            community: 0,
394            likes: 0,
395            dislikes: 0,
396            context: QuestionContext::default(),
397            ip,
398            drawings: Vec::new(),
399        }
400    }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, Default)]
404pub struct QuestionContext {
405    #[serde(default)]
406    pub is_nsfw: bool,
407    /// If the owner is shown as anonymous in the UI.
408    #[serde(default)]
409    pub mask_owner: bool,
410    /// The POST this question is asking about.
411    #[serde(default)]
412    pub asking_about: Option<usize>,
413}
414
415#[derive(Clone, Debug, Serialize, Deserialize)]
416pub struct PostDraft {
417    pub id: usize,
418    pub created: usize,
419    pub content: String,
420    pub owner: usize,
421}
422
423impl PostDraft {
424    /// Create a new [`PostDraft`].
425    pub fn new(content: String, owner: usize) -> Self {
426        Self {
427            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
428            created: unix_epoch_timestamp(),
429            content,
430            owner,
431        }
432    }
433}
434
435#[derive(Clone, Debug, Serialize, Deserialize)]
436pub struct Poll {
437    pub id: usize,
438    pub owner: usize,
439    pub created: usize,
440    /// The number of milliseconds until this poll can no longer receive votes.
441    pub expires: usize,
442    // options
443    pub option_a: String,
444    pub option_b: String,
445    pub option_c: String,
446    pub option_d: String,
447    // votes
448    pub votes_a: usize,
449    pub votes_b: usize,
450    pub votes_c: usize,
451    pub votes_d: usize,
452}
453
454impl Poll {
455    /// Create a new [`Poll`].
456    pub fn new(
457        owner: usize,
458        expires: usize,
459        option_a: String,
460        option_b: String,
461        option_c: String,
462        option_d: String,
463    ) -> Self {
464        Self {
465            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
466            owner,
467            created: unix_epoch_timestamp(),
468            expires,
469            // options
470            option_a,
471            option_b,
472            option_c,
473            option_d,
474            // votes
475            votes_a: 0,
476            votes_b: 0,
477            votes_c: 0,
478            votes_d: 0,
479        }
480    }
481}
482
483/// Poll option (selectors) are stored in the database as numbers 0 to 3.
484///
485/// This enum allows us to convert from these numbers into letters.
486#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
487pub enum PollOption {
488    A,
489    B,
490    C,
491    D,
492}
493
494impl From<u8> for PollOption {
495    fn from(value: u8) -> Self {
496        match value {
497            0 => Self::A,
498            1 => Self::B,
499            2 => Self::C,
500            3 => Self::D,
501            _ => Self::A,
502        }
503    }
504}
505
506impl From<PollOption> for u8 {
507    fn from(val: PollOption) -> Self {
508        match val {
509            PollOption::A => 0,
510            PollOption::B => 1,
511            PollOption::C => 2,
512            PollOption::D => 3,
513        }
514    }
515}
516
517#[derive(Clone, Debug, Serialize, Deserialize)]
518pub struct PollVote {
519    pub id: usize,
520    pub owner: usize,
521    pub created: usize,
522    pub poll_id: usize,
523    pub vote: PollOption,
524}
525
526impl PollVote {
527    /// Create a new [`PollVote`].
528    pub fn new(owner: usize, poll_id: usize, vote: PollOption) -> Self {
529        Self {
530            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
531            owner,
532            created: unix_epoch_timestamp(),
533            poll_id,
534            vote,
535        }
536    }
537}
538
539#[derive(Clone, Debug, Serialize, Deserialize)]
540pub struct ForumTopic {
541    pub title: String,
542    pub description: String,
543    pub color: String,
544    pub position: i32,
545    #[serde(default)]
546    pub write_access: CommunityWriteAccess,
547}
548
549impl ForumTopic {
550    /// Create a new [`ForumTopic`].
551    ///
552    /// # Returns
553    /// * ID for [`Community`] hashmap
554    /// * [`ForumTopic`]
555    pub fn new(
556        title: String,
557        description: String,
558        color: String,
559        position: i32,
560        write_access: CommunityWriteAccess,
561    ) -> (usize, Self) {
562        (
563            Snowflake::new().to_string().parse::<usize>().unwrap(),
564            Self {
565                title,
566                description,
567                color,
568                position,
569                write_access,
570            },
571        )
572    }
573}