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