Skip to main content

tetratto_core2/model/
posts.rs

1use super::id::Id;
2use serde::{Serialize, Deserialize};
3use serde_valid::Validate;
4use tritools::time::unix_epoch_timestamp;
5
6#[derive(Clone, Debug, Serialize, Deserialize, Validate)]
7pub struct PostContext {
8    #[serde(default = "default_comments_enabled")]
9    pub comments_enabled: bool,
10    #[serde(default)]
11    pub is_profile_pinned: bool,
12    #[serde(default)]
13    pub edited: u128,
14    #[serde(default)]
15    pub is_nsfw: bool,
16    #[serde(default)]
17    pub repost: Option<RepostContext>,
18    #[serde(default = "default_reposts_enabled")]
19    pub reposts_enabled: bool,
20    /// The ID of the question this post is answering.
21    #[serde(default)]
22    pub answering: Option<Id>,
23    #[serde(default = "default_reactions_enabled")]
24    pub reactions_enabled: bool,
25    #[serde(default)]
26    #[validate(max_length = 1024)]
27    pub content_warning: String,
28    #[serde(default)]
29    #[validate(max_items = 12)]
30    pub tags: Vec<String>,
31}
32
33fn default_comments_enabled() -> bool {
34    true
35}
36
37fn default_reposts_enabled() -> bool {
38    true
39}
40
41fn default_reactions_enabled() -> bool {
42    true
43}
44
45impl Default for PostContext {
46    fn default() -> Self {
47        Self {
48            comments_enabled: default_comments_enabled(),
49            reposts_enabled: default_reposts_enabled(),
50            is_profile_pinned: false,
51            edited: 0,
52            is_nsfw: false,
53            repost: None,
54            answering: None,
55            reactions_enabled: default_reactions_enabled(),
56            content_warning: String::new(),
57            tags: Vec::new(),
58        }
59    }
60}
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
63pub struct RepostContext {
64    /// Should be `false` is `reposting` is `Some`.
65    ///
66    /// Declares the post to be a repost of another post.
67    pub is_repost: bool,
68    /// Should be `None` if `is_repost` is true.
69    ///
70    /// Sets the ID of the other post to load.
71    pub reposting: Option<Id>,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct Post {
76    pub id: Id,
77    pub created: u128,
78    pub content: String,
79    /// The ID of the owner of this post.
80    pub owner: Id,
81    /// Extra information about the post.
82    pub context: PostContext,
83    /// The ID of the post this post is a comment on.
84    pub replying_to: Option<Id>,
85    pub likes: isize,
86    pub dislikes: isize,
87    pub comment_count: usize,
88    /// IDs of all uploads linked to this post.
89    pub uploads: Vec<usize>,
90    /// If the post was deleted.
91    pub is_deleted: bool,
92    /// The ID of the poll associated with this post.
93    pub poll_id: Option<Id>,
94    pub views: usize,
95    pub group: Option<Id>,
96    pub limited_reach: bool,
97}
98
99impl Post {
100    /// Create a new [`Post`].
101    pub fn new(
102        content: String,
103        replying_to: Option<Id>,
104        owner: Id,
105        poll_id: Option<Id>,
106        group: Option<Id>,
107    ) -> Self {
108        Self {
109            id: Id::new(),
110            created: unix_epoch_timestamp(),
111            content,
112            owner,
113            context: PostContext::default(),
114            replying_to,
115            likes: 0,
116            dislikes: 0,
117            comment_count: 0,
118            uploads: Vec::new(),
119            is_deleted: false,
120            poll_id,
121            views: 0,
122            group,
123            limited_reach: false,
124        }
125    }
126
127    /// Create a new [`Post`] (as a repost of the given `post_id`).
128    pub fn repost(content: String, owner: Id, post_id: Id) -> Self {
129        let mut post = Self::new(content, None, owner, None, None);
130
131        post.context.repost = Some(RepostContext {
132            is_repost: false,
133            reposting: Some(post_id),
134        });
135
136        post
137    }
138
139    /// Make the given post a reposted post.
140    pub fn mark_as_repost(&mut self) {
141        self.context.repost = Some(RepostContext {
142            is_repost: true,
143            reposting: None,
144        });
145    }
146
147    /// Check if a particular user can comment on this post.
148    pub fn can_comment(&self) -> bool {
149        self.context.repost.is_none() || !self.content.is_empty()
150    }
151}
152
153#[derive(Clone, Debug, Serialize, Deserialize)]
154pub struct Question {
155    pub id: Id,
156    pub created: u128,
157    pub owner: Id,
158    pub receiver: Id,
159    pub content: String,
160    /// The `is_global` flag allows any (authenticated) user to respond
161    /// to the question. Normally, only the `receiver` can do so.
162    ///
163    /// If `is_global` is true, `receiver` should be 0 (and vice versa).
164    pub is_global: bool,
165    /// The number of answers the question has. Should never really be changed
166    /// unless the question has `is_global` set to true.
167    pub answer_count: usize,
168    // likes
169    #[serde(default)]
170    pub likes: isize,
171    #[serde(default)]
172    pub dislikes: isize,
173    // ...
174    #[serde(default)]
175    pub context: QuestionContext,
176    /// The IP of the question creator for IP blocking and identifying anonymous users.
177    #[serde(default)]
178    pub ip: String,
179    /// The IDs of all uploads which hold this question's drawings.
180    #[serde(default)]
181    pub drawings: Vec<usize>,
182}
183
184impl Question {
185    /// Create a new [`Question`].
186    pub fn new(owner: Id, receiver: Id, content: String, is_global: bool, ip: String) -> Self {
187        Self {
188            id: Id::new(),
189            created: unix_epoch_timestamp(),
190            owner,
191            receiver,
192            content,
193            is_global,
194            answer_count: 0,
195            likes: 0,
196            dislikes: 0,
197            context: QuestionContext::default(),
198            ip,
199            drawings: Vec::new(),
200        }
201    }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
205pub struct QuestionContext {
206    #[serde(default)]
207    pub is_nsfw: bool,
208    /// If the owner is shown as anonymous in the UI.
209    #[serde(default)]
210    pub mask_owner: bool,
211    /// The POST this question is asking about.
212    #[serde(default)]
213    pub asking_about: Option<Id>,
214}
215
216#[derive(Clone, Debug, Serialize, Deserialize)]
217pub struct PostDraft {
218    pub id: Id,
219    pub created: u128,
220    pub content: String,
221    pub owner: Id,
222}
223
224impl PostDraft {
225    /// Create a new [`PostDraft`].
226    pub fn new(content: String, owner: Id) -> Self {
227        Self {
228            id: Id::new(),
229            created: unix_epoch_timestamp(),
230            content,
231            owner,
232        }
233    }
234}
235
236#[derive(Clone, Debug, Serialize, Deserialize)]
237pub struct Poll {
238    pub id: Id,
239    pub owner: Id,
240    pub created: u128,
241    /// The number of milliseconds until this poll can no longer receive votes.
242    pub expires: u128,
243    // options
244    pub option_a: String,
245    pub option_b: String,
246    pub option_c: String,
247    pub option_d: String,
248    // votes
249    pub votes_a: usize,
250    pub votes_b: usize,
251    pub votes_c: usize,
252    pub votes_d: usize,
253}
254
255impl Poll {
256    /// Create a new [`Poll`].
257    pub fn new(
258        owner: Id,
259        expires: u128,
260        option_a: String,
261        option_b: String,
262        option_c: String,
263        option_d: String,
264    ) -> Self {
265        Self {
266            id: Id::new(),
267            owner,
268            created: unix_epoch_timestamp(),
269            expires,
270            // options
271            option_a,
272            option_b,
273            option_c,
274            option_d,
275            // votes
276            votes_a: 0,
277            votes_b: 0,
278            votes_c: 0,
279            votes_d: 0,
280        }
281    }
282}
283
284/// Poll option (selectors) are stored in the database as numbers 0 to 3.
285///
286/// This enum allows us to convert from these numbers into letters.
287#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
288pub enum PollOption {
289    A,
290    B,
291    C,
292    D,
293}
294
295impl From<u8> for PollOption {
296    fn from(value: u8) -> Self {
297        match value {
298            0 => Self::A,
299            1 => Self::B,
300            2 => Self::C,
301            3 => Self::D,
302            _ => Self::A,
303        }
304    }
305}
306
307impl From<PollOption> for u8 {
308    fn from(val: PollOption) -> Self {
309        match val {
310            PollOption::A => 0,
311            PollOption::B => 1,
312            PollOption::C => 2,
313            PollOption::D => 3,
314        }
315    }
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
319pub struct PollVote {
320    pub id: Id,
321    pub owner: Id,
322    pub created: u128,
323    pub poll_id: Id,
324    pub vote: PollOption,
325}
326
327impl PollVote {
328    /// Create a new [`PollVote`].
329    pub fn new(owner: Id, poll_id: Id, vote: PollOption) -> Self {
330        Self {
331            id: Id::new(),
332            owner,
333            created: unix_epoch_timestamp(),
334            poll_id,
335            vote,
336        }
337    }
338}