tetratto_core/database/
posts.rs

1use std::collections::HashMap;
2use crate::config::StringBan;
3use crate::model::auth::{AchievementName, Notification};
4use crate::model::communities::{CommunityMembership, CommunityReadAccess, Poll, Question};
5use crate::model::communities_permissions::CommunityPermission;
6use crate::model::moderation::AuditLogEntry;
7use crate::model::stacks::{StackMode, StackSort, UserStack};
8use crate::model::{
9    Error, Result,
10    auth::User,
11    communities::{Community, CommunityWriteAccess, Post, PostContext},
12    permissions::FinePermission,
13};
14use tetratto_shared::unix_epoch_timestamp;
15use crate::{auto_method, DataManager};
16use oiseau::{PostgresRow, execute, get, query_row, query_rows, params, cache::Cache};
17
18pub type FullPost = (
19    Post,
20    User,
21    Community,
22    Option<(User, Post)>,
23    Option<(Question, User, Option<(User, Post)>)>,
24    Option<(Poll, bool, bool)>,
25    Option<UserStack>,
26);
27pub type FullQuestion = (Question, User, Option<(User, Post)>);
28
29macro_rules! private_post_replying {
30    ($post:ident, $replying_posts:ident, $ua1:ident, $data:ident) => {
31        // post owner is not following us
32        // check if we're the owner of the post the post is replying to
33        // all routes but 1 must lead to continue
34        if let Some(replying) = $post.replying_to {
35            if replying != 0 {
36                if let Some(post) = $replying_posts.get(&replying) {
37                    // we've seen this post before
38                    if post.owner != $ua1.id {
39                        // we aren't the owner of this post,
40                        // so we can't see their comment
41                        continue;
42                    }
43                } else {
44                    // we haven't seen this post before
45                    let post = $data.get_post_by_id(replying).await?;
46
47                    if post.owner != $ua1.id {
48                        continue;
49                    }
50
51                    $replying_posts.insert(post.id, post);
52                }
53            } else {
54                continue;
55            }
56        } else {
57            continue;
58        }
59    };
60
61    ($post:ident, $replying_posts:ident, id=$user_id:ident, $data:ident) => {
62        // post owner is not following us
63        // check if we're the owner of the post the post is replying to
64        // all routes but 1 must lead to continue
65        if let Some(replying) = $post.replying_to {
66            if replying != 0 {
67                if let Some(post) = $replying_posts.get(&replying) {
68                    // we've seen this post before
69                    if post.owner != $user_id {
70                        // we aren't the owner of this post,
71                        // so we can't see their comment
72                        continue;
73                    }
74                } else {
75                    // we haven't seen this post before
76                    let post = $data.get_post_by_id(replying).await?;
77
78                    if post.owner != $user_id {
79                        continue;
80                    }
81
82                    $replying_posts.insert(post.id, post);
83                }
84            } else {
85                continue;
86            }
87        } else {
88            continue;
89        }
90    };
91}
92
93impl DataManager {
94    /// Get a [`Post`] from an SQL row.
95    pub(crate) fn get_post_from_row(x: &PostgresRow) -> Post {
96        Post {
97            id: get!(x->0(i64)) as usize,
98            created: get!(x->1(i64)) as usize,
99            content: get!(x->2(String)),
100            owner: get!(x->3(i64)) as usize,
101            community: get!(x->4(i64)) as usize,
102            context: serde_json::from_str(&get!(x->5(String))).unwrap(),
103            replying_to: get!(x->6(Option<i64>)).map(|id| id as usize),
104            // likes
105            likes: get!(x->7(i32)) as isize,
106            dislikes: get!(x->8(i32)) as isize,
107            // other counts
108            comment_count: get!(x->9(i32)) as usize,
109            // ...
110            uploads: serde_json::from_str(&get!(x->10(String))).unwrap(),
111            is_deleted: get!(x->11(i32)) as i8 == 1,
112            // SKIP tsvector (12)
113            poll_id: get!(x->13(i64)) as usize,
114            title: get!(x->14(String)),
115            is_open: get!(x->15(i32)) as i8 == 1,
116            stack: get!(x->16(i64)) as usize,
117            topic: get!(x->17(i64)) as usize,
118        }
119    }
120
121    auto_method!(get_post_by_id()@get_post_from_row -> "SELECT * FROM posts WHERE id = $1" --name="post" --returns=Post --cache-key-tmpl="atto.post:{}");
122
123    /// Get all posts which are comments on the given post by ID.
124    ///
125    /// # Arguments
126    /// * `id` - the ID of the post the requested posts are commenting on
127    /// * `batch` - the limit of posts in each page
128    /// * `page` - the page number
129    pub async fn get_replies_by_post(
130        &self,
131        id: usize,
132        batch: usize,
133        page: usize,
134        sort: &str,
135    ) -> Result<Vec<Post>> {
136        let conn = match self.0.connect().await {
137            Ok(c) => c,
138            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
139        };
140
141        let res = query_rows!(
142            &conn,
143            &format!(
144                "SELECT * FROM posts WHERE replying_to = $1 ORDER BY created {sort} LIMIT $2 OFFSET $3"
145            ),
146            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
147            |x| { Self::get_post_from_row(x) }
148        );
149
150        if res.is_err() {
151            return Err(Error::GeneralNotFound("post".to_string()));
152        }
153
154        Ok(res.unwrap())
155    }
156
157    /// Get the post the given post is reposting (if some).
158    pub async fn get_post_reposting(
159        &self,
160        post: &Post,
161        ignore_users: &[usize],
162        user: &Option<User>,
163    ) -> (bool, Option<(User, Post)>) {
164        if let Some(ref repost) = post.context.repost {
165            if let Some(reposting) = repost.reposting {
166                let mut x = match self.get_post_by_id(reposting).await {
167                    Ok(p) => p,
168                    Err(_) => return (true, None),
169                };
170
171                if x.is_deleted {
172                    return (!post.content.is_empty(), None);
173                }
174
175                if ignore_users.contains(&x.owner) {
176                    return (!post.content.is_empty(), None);
177                }
178
179                // check private profile settings
180                let owner = match self.get_user_by_id(x.owner).await {
181                    Ok(ua) => ua,
182                    Err(_) => return (true, None),
183                };
184
185                // TODO: maybe check community membership to see if we can MANAGE_POSTS in community
186                if owner.settings.private_profile {
187                    if let Some(ua) = user {
188                        if owner.id != ua.id && !ua.permissions.check(FinePermission::MANAGE_POSTS)
189                        {
190                            if self
191                                .get_userfollow_by_initiator_receiver(owner.id, ua.id)
192                                .await
193                                .is_err()
194                            {
195                                // owner isn't following us, we aren't the owner, AND we don't have MANAGE_POSTS permission
196                                return (!post.content.is_empty(), None);
197                            }
198                        }
199                    } else {
200                        // private profile, but we're an unauthenticated user
201                        return (!post.content.is_empty(), None);
202                    }
203                }
204
205                // ...
206                x.mark_as_repost();
207                (
208                    true,
209                    Some((
210                        match self.get_user_by_id(x.owner).await {
211                            Ok(ua) => ua,
212                            Err(_) => return (true, None),
213                        },
214                        x,
215                    )),
216                )
217            } else {
218                (true, None)
219            }
220        } else {
221            (true, None)
222        }
223    }
224
225    /// Get the question of a given post.
226    pub async fn get_post_question(
227        &self,
228        post: &Post,
229        ignore_users: &[usize],
230        seen_questions: &mut HashMap<usize, FullQuestion>,
231    ) -> Result<Option<FullQuestion>> {
232        if post.context.answering != 0 {
233            if let Some(q) = seen_questions.get(&post.context.answering) {
234                return Ok(Some(q.to_owned()));
235            }
236
237            // ...
238            let question = self.get_question_by_id(post.context.answering).await?;
239
240            if ignore_users.contains(&question.owner) {
241                return Ok(None);
242            }
243
244            let user = if question.owner == 0 {
245                User::anonymous()
246            } else {
247                self.get_user_by_id_with_void(question.owner).await?
248            };
249
250            let asking_about = self.get_question_asking_about(&question).await?;
251            let full_question = (question, user, asking_about);
252
253            seen_questions.insert(post.context.answering, full_question.to_owned());
254            Ok(Some(full_question))
255        } else {
256            Ok(None)
257        }
258    }
259
260    /// Get the poll of the given post (if some).
261    ///
262    /// # Returns
263    /// `Result<Option<(poll, voted, expired)>>`
264    pub async fn get_post_poll(
265        &self,
266        post: &Post,
267        user: &Option<User>,
268    ) -> Result<Option<(Poll, bool, bool)>> {
269        let user = if let Some(ua) = user {
270            ua
271        } else {
272            return Ok(None);
273        };
274
275        if post.poll_id != 0 {
276            Ok(Some(match self.get_poll_by_id(post.poll_id).await {
277                Ok(p) => {
278                    let expired = unix_epoch_timestamp() - p.created > p.expires;
279                    (
280                        p,
281                        self.get_pollvote_by_owner_poll(user.id, post.poll_id)
282                            .await
283                            .is_ok(),
284                        expired,
285                    )
286                }
287                Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())),
288            }))
289        } else {
290            Ok(None)
291        }
292    }
293
294    /// Get the stack of the given post (if some).
295    ///
296    /// # Returns
297    /// `(can view post, stack)`
298    pub async fn get_post_stack(
299        &self,
300        seen_stacks: &mut HashMap<usize, UserStack>,
301        post: &Post,
302        as_user_id: usize,
303    ) -> (bool, Option<UserStack>) {
304        if post.stack != 0 {
305            if let Some(s) = seen_stacks.get(&post.stack) {
306                (
307                    (s.owner == as_user_id) | s.users.contains(&as_user_id),
308                    Some(s.to_owned()),
309                )
310            } else {
311                let s = match self.get_stack_by_id(post.stack).await {
312                    Ok(s) => s,
313                    Err(_) => return (true, None),
314                };
315
316                seen_stacks.insert(s.id, s.to_owned());
317                (
318                    (s.owner == as_user_id) | s.users.contains(&as_user_id),
319                    Some(s.to_owned()),
320                )
321            }
322        } else {
323            (true, None)
324        }
325    }
326
327    /// Complete a vector of just posts with their owner as well.
328    pub async fn fill_posts(
329        &self,
330        posts: Vec<Post>,
331        ignore_users: &[usize],
332        user: &Option<User>,
333    ) -> Result<
334        Vec<(
335            Post,
336            User,
337            Option<(User, Post)>,
338            Option<(Question, User, Option<(User, Post)>)>,
339            Option<(Poll, bool, bool)>,
340            Option<UserStack>,
341        )>,
342    > {
343        let mut out = Vec::new();
344
345        let mut users: HashMap<usize, User> = HashMap::new();
346        let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
347        let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
348        let mut seen_questions: HashMap<usize, FullQuestion> = HashMap::new();
349        let mut replying_posts: HashMap<usize, Post> = HashMap::new();
350
351        for post in posts {
352            if post.is_deleted {
353                continue;
354            }
355
356            let owner = post.owner;
357
358            if let Some(ua) = users.get(&owner) {
359                // check if owner requires an account to view their posts (and if we have one)
360                if ua.settings.require_account && user.is_none() {
361                    continue;
362                }
363
364                // stack
365                let (can_view, stack) = self
366                    .get_post_stack(
367                        &mut seen_stacks,
368                        &post,
369                        if let Some(ua) = user { ua.id } else { 0 },
370                    )
371                    .await;
372
373                if !can_view {
374                    continue;
375                }
376
377                // reposting
378                let (can_view, reposting) =
379                    self.get_post_reposting(&post, ignore_users, user).await;
380
381                if !can_view {
382                    continue;
383                }
384
385                // ...
386                out.push((
387                    post.clone(),
388                    ua.clone(),
389                    reposting,
390                    self.get_post_question(&post, ignore_users, &mut seen_questions)
391                        .await?,
392                    self.get_post_poll(&post, user).await?,
393                    stack,
394                ));
395            } else {
396                let ua = self.get_user_by_id(owner).await?;
397
398                if ua.settings.require_account && user.is_none() {
399                    continue;
400                }
401
402                if (ua.permissions.check_banned()
403                    | ignore_users.contains(&owner)
404                    | ua.is_deactivated)
405                    && !ua.permissions.check(FinePermission::MANAGE_POSTS)
406                {
407                    continue;
408                }
409
410                // check relationship
411                if ua.settings.private_profile {
412                    // if someone were to look for places to optimize memory usage,
413                    // look no further than here
414                    if let Some(ua1) = user {
415                        if ua1.id == 0 {
416                            continue;
417                        }
418
419                        if ua1.id != ua.id && !ua1.permissions.check(FinePermission::MANAGE_POSTS) {
420                            if let Some(is_following) =
421                                seen_user_follow_statuses.get(&(ua.id, ua1.id))
422                            {
423                                if !is_following && ua.id != ua1.id {
424                                    private_post_replying!(post, replying_posts, ua1, self);
425                                }
426                            } else {
427                                if self
428                                    .get_userfollow_by_initiator_receiver(ua.id, ua1.id)
429                                    .await
430                                    .is_err()
431                                    && ua.id != ua1.id
432                                {
433                                    // post owner is not following us
434                                    seen_user_follow_statuses.insert((ua.id, ua1.id), false);
435                                    private_post_replying!(post, replying_posts, ua1, self);
436                                }
437
438                                seen_user_follow_statuses.insert((ua.id, ua1.id), true);
439                            }
440                        }
441                    } else {
442                        // private post, but not authenticated
443                        continue;
444                    }
445                }
446
447                // stack
448                let (can_view, stack) = self
449                    .get_post_stack(
450                        &mut seen_stacks,
451                        &post,
452                        if let Some(ua) = user { ua.id } else { 0 },
453                    )
454                    .await;
455
456                if !can_view {
457                    continue;
458                }
459
460                // reposting
461                let (can_view, reposting) =
462                    self.get_post_reposting(&post, ignore_users, user).await;
463
464                if !can_view {
465                    continue;
466                }
467
468                // ...
469                users.insert(owner, ua.clone());
470                out.push((
471                    post.clone(),
472                    ua,
473                    reposting,
474                    self.get_post_question(&post, ignore_users, &mut seen_questions)
475                        .await?,
476                    self.get_post_poll(&post, user).await?,
477                    stack,
478                ));
479            }
480        }
481
482        Ok(out)
483    }
484
485    /// Complete a vector of just posts with their owner and community as well.
486    pub async fn fill_posts_with_community(
487        &self,
488        posts: Vec<Post>,
489        user_id: usize,
490        ignore_users: &[usize],
491        user: &Option<User>,
492    ) -> Result<Vec<FullPost>> {
493        let mut out = Vec::new();
494
495        let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new();
496        let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
497        let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
498        let mut seen_questions: HashMap<usize, FullQuestion> = HashMap::new();
499        let mut replying_posts: HashMap<usize, Post> = HashMap::new();
500        let mut memberships: HashMap<usize, CommunityMembership> = HashMap::new();
501
502        for post in posts {
503            if post.is_deleted {
504                continue;
505            }
506
507            let owner = post.owner;
508            let community = post.community;
509
510            if let Some((ua, community)) = seen_before.get(&(owner, community)) {
511                if ua.settings.require_account && user.is_none() {
512                    continue;
513                }
514
515                // check membership
516                if community.read_access == CommunityReadAccess::Joined {
517                    if let Some(user) = user {
518                        if let Some(membership) = memberships.get(&community.id) {
519                            if !membership.role.check(CommunityPermission::MEMBER) {
520                                continue;
521                            }
522                        } else {
523                            if let Ok(membership) = self
524                                .get_membership_by_owner_community_no_void(user.id, community.id)
525                                .await
526                            {
527                                if !membership.role.check(CommunityPermission::MEMBER) {
528                                    continue;
529                                }
530                            } else {
531                                continue;
532                            }
533                        }
534                    } else {
535                        continue;
536                    }
537                }
538
539                // stack
540                let (can_view, stack) = self
541                    .get_post_stack(
542                        &mut seen_stacks,
543                        &post,
544                        if let Some(ua) = user { ua.id } else { 0 },
545                    )
546                    .await;
547
548                if !can_view {
549                    continue;
550                }
551
552                // reposting
553                let (can_view, reposting) =
554                    self.get_post_reposting(&post, ignore_users, user).await;
555
556                if !can_view {
557                    continue;
558                }
559
560                // ...
561                out.push((
562                    post.clone(),
563                    ua.clone(),
564                    community.to_owned(),
565                    reposting,
566                    self.get_post_question(&post, ignore_users, &mut seen_questions)
567                        .await?,
568                    self.get_post_poll(&post, user).await?,
569                    stack,
570                ));
571            } else {
572                let ua = self.get_user_by_id(owner).await?;
573
574                if ua.settings.require_account && user.is_none() {
575                    continue;
576                }
577
578                if ua.permissions.check_banned() | ignore_users.contains(&owner)
579                    && !ua.permissions.check(FinePermission::MANAGE_POSTS)
580                {
581                    continue;
582                }
583
584                // check membership
585                let community = self.get_community_by_id(community).await?;
586                if community.read_access == CommunityReadAccess::Joined {
587                    if let Some(user) = user {
588                        if let Some(membership) = memberships.get(&community.id) {
589                            if !membership.role.check(CommunityPermission::MEMBER) {
590                                continue;
591                            }
592                        } else {
593                            if let Ok(membership) = self
594                                .get_membership_by_owner_community_no_void(user.id, community.id)
595                                .await
596                            {
597                                memberships.insert(owner, membership.clone());
598                                if !membership.role.check(CommunityPermission::MEMBER) {
599                                    continue;
600                                }
601                            } else {
602                                continue;
603                            }
604                        }
605                    } else {
606                        continue;
607                    }
608                }
609
610                // check relationship
611                if ua.settings.private_profile && ua.id != user_id {
612                    if user_id == 0 {
613                        continue;
614                    }
615
616                    if user_id != ua.id {
617                        if let Some(is_following) = seen_user_follow_statuses.get(&(ua.id, user_id))
618                        {
619                            if !is_following {
620                                private_post_replying!(post, replying_posts, id = user_id, self);
621                            }
622                        } else {
623                            if self
624                                .get_userfollow_by_initiator_receiver(ua.id, user_id)
625                                .await
626                                .is_err()
627                            {
628                                // post owner is not following us
629                                seen_user_follow_statuses.insert((ua.id, user_id), false);
630                                private_post_replying!(post, replying_posts, id = user_id, self);
631                            }
632
633                            seen_user_follow_statuses.insert((ua.id, user_id), true);
634                        }
635                    }
636                }
637
638                // stack
639                let (can_view, stack) = self
640                    .get_post_stack(
641                        &mut seen_stacks,
642                        &post,
643                        if let Some(ua) = user { ua.id } else { 0 },
644                    )
645                    .await;
646
647                if !can_view {
648                    continue;
649                }
650
651                // reposting
652                let (can_view, reposting) =
653                    self.get_post_reposting(&post, ignore_users, user).await;
654
655                if !can_view {
656                    continue;
657                }
658
659                // ...
660                seen_before.insert((owner, community.id), (ua.clone(), community.clone()));
661                out.push((
662                    post.clone(),
663                    ua,
664                    community,
665                    reposting,
666                    self.get_post_question(&post, ignore_users, &mut seen_questions)
667                        .await?,
668                    self.get_post_poll(&post, user).await?,
669                    stack,
670                ));
671            }
672        }
673
674        Ok(out)
675    }
676
677    /// Update posts which contain a muted phrase.
678    pub fn posts_muted_phrase_filter(
679        &self,
680        posts: &Vec<FullPost>,
681        muted: Option<&Vec<String>>,
682    ) -> Vec<FullPost> {
683        // this shit is actually ass bro it has to clone
684        // very useless
685        let muted = match muted {
686            Some(m) => m,
687            None => return posts.to_owned(),
688        };
689
690        let mut out: Vec<FullPost> = Vec::new();
691
692        for mut post in posts.clone() {
693            for phrase in muted {
694                if phrase.is_empty() {
695                    continue;
696                }
697
698                if post
699                    .0
700                    .content
701                    .to_lowercase()
702                    .contains(&phrase.to_lowercase())
703                {
704                    post.0.context.content_warning = "Contains muted phrase".to_string();
705                    break;
706                }
707
708                if let Some(ref mut reposting) = post.3 {
709                    if reposting
710                        .1
711                        .content
712                        .to_lowercase()
713                        .contains(&phrase.to_lowercase())
714                    {
715                        reposting.1.context.content_warning = "Contains muted phrase".to_string();
716                        break;
717                    }
718                }
719            }
720
721            out.push(post);
722        }
723
724        out
725    }
726
727    /// Filter to update posts to clean their owner for public APIs.
728    pub fn posts_owner_filter(&self, posts: &Vec<FullPost>) -> Vec<FullPost> {
729        let mut out: Vec<FullPost> = Vec::new();
730
731        for mut post in posts.clone() {
732            post.1.clean();
733
734            // reposting
735            if let Some((ref mut x, _)) = post.3 {
736                x.clean();
737            }
738
739            // question
740            if let Some((_, ref mut x, ref mut y)) = post.4 {
741                x.clean();
742
743                if y.is_some() {
744                    y.as_mut().unwrap().0.clean();
745                }
746            }
747
748            // ...
749            out.push(post);
750        }
751
752        out
753    }
754
755    /// Get all posts from the given user (from most recent).
756    ///
757    /// # Arguments
758    /// * `id` - the ID of the user the requested posts belong to
759    /// * `batch` - the limit of posts in each page
760    /// * `page` - the page number
761    pub async fn get_posts_by_user(
762        &self,
763        id: usize,
764        batch: usize,
765        page: usize,
766    ) -> Result<Vec<Post>> {
767        let conn = match self.0.connect().await {
768            Ok(c) => c,
769            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
770        };
771
772        let res = query_rows!(
773            &conn,
774            "SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
775            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
776            |x| { Self::get_post_from_row(x) }
777        );
778
779        if res.is_err() {
780            return Err(Error::GeneralNotFound("post".to_string()));
781        }
782
783        Ok(res.unwrap())
784    }
785
786    /// Get all posts (that are answering a question) from the given user (from most recent).
787    ///
788    /// # Arguments
789    /// * `id` - the ID of the user the requested posts belong to
790    /// * `batch` - the limit of posts in each page
791    /// * `page` - the page number
792    pub async fn get_responses_by_user(
793        &self,
794        id: usize,
795        batch: usize,
796        page: usize,
797    ) -> Result<Vec<Post>> {
798        let conn = match self.0.connect().await {
799            Ok(c) => c,
800            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
801        };
802
803        let res = query_rows!(
804            &conn,
805            "SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 AND NOT context::jsonb->>'answering' = '0' ORDER BY created DESC LIMIT $2 OFFSET $3",
806            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
807            |x| { Self::get_post_from_row(x) }
808        );
809
810        if res.is_err() {
811            return Err(Error::GeneralNotFound("post".to_string()));
812        }
813
814        Ok(res.unwrap())
815    }
816
817    /// Get all replies from the given user (from most recent).
818    ///
819    /// # Arguments
820    /// * `id` - the ID of the user the requested posts belong to
821    /// * `batch` - the limit of posts in each page
822    /// * `page` - the page number
823    pub async fn get_replies_by_user(
824        &self,
825        id: usize,
826        batch: usize,
827        page: usize,
828        user: &Option<User>,
829    ) -> Result<Vec<Post>> {
830        let conn = match self.0.connect().await {
831            Ok(c) => c,
832            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
833        };
834
835        // check if we should hide nsfw posts
836        let mut hide_nsfw: bool = true;
837
838        if let Some(ua) = user {
839            hide_nsfw = !ua.settings.show_nsfw;
840        }
841
842        // ...
843        let res = query_rows!(
844            &conn,
845            &format!(
846                "SELECT * FROM posts WHERE owner = $1 AND NOT replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $2 OFFSET $3",
847                if hide_nsfw {
848                    "AND NOT (context::json->>'is_nsfw')::boolean"
849                } else {
850                    ""
851                }
852            ),
853            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
854            |x| { Self::get_post_from_row(x) }
855        );
856
857        if res.is_err() {
858            return Err(Error::GeneralNotFound("post".to_string()));
859        }
860
861        Ok(res.unwrap())
862    }
863
864    /// Get all posts containing media from the given user (from most recent).
865    ///
866    /// # Arguments
867    /// * `id` - the ID of the user the requested posts belong to
868    /// * `batch` - the limit of posts in each page
869    /// * `page` - the page number
870    pub async fn get_media_posts_by_user(
871        &self,
872        id: usize,
873        batch: usize,
874        page: usize,
875        user: &Option<User>,
876    ) -> Result<Vec<Post>> {
877        let conn = match self.0.connect().await {
878            Ok(c) => c,
879            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
880        };
881
882        // check if we should hide nsfw posts
883        let mut hide_nsfw: bool = true;
884
885        if let Some(ua) = user {
886            hide_nsfw = !ua.settings.show_nsfw;
887        }
888
889        // ...
890        let res = query_rows!(
891            &conn,
892            &format!(
893                "SELECT * FROM posts WHERE owner = $1 AND NOT uploads = '[]' AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $2 OFFSET $3",
894                if hide_nsfw {
895                    "AND NOT (context::json->>'is_nsfw')::boolean"
896                } else {
897                    ""
898                }
899            ),
900            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
901            |x| { Self::get_post_from_row(x) }
902        );
903
904        if res.is_err() {
905            return Err(Error::GeneralNotFound("post".to_string()));
906        }
907
908        Ok(res.unwrap())
909    }
910
911    /// Get all posts from the given user (searched).
912    ///
913    /// # Arguments
914    /// * `id` - the ID of the user the requested posts belong to
915    /// * `batch` - the limit of posts in each page
916    /// * `page` - the page number
917    /// * `text_query` - the search query
918    /// * `user` - the user who is viewing the posts
919    pub async fn get_posts_by_user_searched(
920        &self,
921        id: usize,
922        batch: usize,
923        page: usize,
924        text_query: &str,
925        user: &Option<&User>,
926    ) -> Result<Vec<Post>> {
927        let conn = match self.0.connect().await {
928            Ok(c) => c,
929            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
930        };
931
932        // check if we should hide nsfw posts
933        let mut hide_nsfw: bool = true;
934
935        if let Some(ua) = user {
936            hide_nsfw = !ua.settings.show_nsfw;
937        }
938
939        // ...
940        let res = query_rows!(
941            &conn,
942            &format!(
943                "SELECT * FROM posts WHERE owner = $1 AND tsvector_content @@ to_tsquery($2) {} AND is_deleted = 0 ORDER BY created DESC LIMIT $3 OFFSET $4",
944                if hide_nsfw {
945                    "AND NOT (context::json->>'is_nsfw')::boolean"
946                } else {
947                    ""
948                }
949            ),
950            params![
951                &(id as i64),
952                &text_query,
953                &(batch as i64),
954                &((page * batch) as i64)
955            ],
956            |x| { Self::get_post_from_row(x) }
957        );
958
959        if res.is_err() {
960            return Err(Error::GeneralNotFound("post".to_string()));
961        }
962
963        Ok(res.unwrap())
964    }
965
966    /// Get all post (searched).
967    ///
968    /// # Arguments
969    /// * `batch` - the limit of posts in each page
970    /// * `page` - the page number
971    /// * `text_query` - the search query
972    pub async fn get_posts_searched(
973        &self,
974        batch: usize,
975        page: usize,
976        text_query: &str,
977    ) -> Result<Vec<Post>> {
978        let conn = match self.0.connect().await {
979            Ok(c) => c,
980            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
981        };
982
983        // ...
984        let res = query_rows!(
985            &conn,
986            "SELECT * FROM posts WHERE tsvector_content @@ to_tsquery($1) AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
987            params![&text_query, &(batch as i64), &((page * batch) as i64)],
988            |x| { Self::get_post_from_row(x) }
989        );
990
991        if res.is_err() {
992            return Err(Error::GeneralNotFound("post".to_string()));
993        }
994
995        Ok(res.unwrap())
996    }
997
998    /// Get all posts from the given user with the given tag (from most recent).
999    ///
1000    /// # Arguments
1001    /// * `id` - the ID of the user the requested posts belong to
1002    /// * `tag` - the tag to filter by
1003    /// * `batch` - the limit of posts in each page
1004    /// * `page` - the page number
1005    pub async fn get_posts_by_user_tag(
1006        &self,
1007        id: usize,
1008        tag: &str,
1009        batch: usize,
1010        page: usize,
1011    ) -> Result<Vec<Post>> {
1012        let conn = match self.0.connect().await {
1013            Ok(c) => c,
1014            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1015        };
1016
1017        let res = query_rows!(
1018            &conn,
1019            "SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 AND is_deleted = 0 ORDER BY created DESC LIMIT $3 OFFSET $4",
1020            params![
1021                &(id as i64),
1022                &format!("%\"{tag}\"%"),
1023                &(batch as i64),
1024                &((page * batch) as i64)
1025            ],
1026            |x| { Self::get_post_from_row(x) }
1027        );
1028
1029        if res.is_err() {
1030            return Err(Error::GeneralNotFound("post".to_string()));
1031        }
1032
1033        Ok(res.unwrap())
1034    }
1035
1036    /// Get all posts (that are answering a question) from the given user
1037    /// with the given tag (from most recent).
1038    ///
1039    /// # Arguments
1040    /// * `id` - the ID of the user the requested posts belong to
1041    /// * `tag` - the tag to filter by
1042    /// * `batch` - the limit of posts in each page
1043    /// * `page` - the page number
1044    pub async fn get_responses_by_user_tag(
1045        &self,
1046        id: usize,
1047        tag: &str,
1048        batch: usize,
1049        page: usize,
1050    ) -> Result<Vec<Post>> {
1051        let conn = match self.0.connect().await {
1052            Ok(c) => c,
1053            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1054        };
1055
1056        let res = query_rows!(
1057            &conn,
1058            "SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 AND is_deleted = 0 AND NOT context::jsonb->>'answering' = '0' ORDER BY created DESC LIMIT $3 OFFSET $4",
1059            params![
1060                &(id as i64),
1061                &format!("%\"{tag}\"%"),
1062                &(batch as i64),
1063                &((page * batch) as i64)
1064            ],
1065            |x| { Self::get_post_from_row(x) }
1066        );
1067
1068        if res.is_err() {
1069            return Err(Error::GeneralNotFound("post".to_string()));
1070        }
1071
1072        Ok(res.unwrap())
1073    }
1074
1075    /// Get all posts from the given community (from most recent).
1076    ///
1077    /// # Arguments
1078    /// * `id` - the ID of the community the requested posts belong to
1079    /// * `batch` - the limit of posts in each page
1080    /// * `page` - the page number
1081    pub async fn get_posts_by_community(
1082        &self,
1083        id: usize,
1084        batch: usize,
1085        page: usize,
1086        user: &Option<User>,
1087    ) -> Result<Vec<Post>> {
1088        let conn = match self.0.connect().await {
1089            Ok(c) => c,
1090            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1091        };
1092
1093        // check if we should hide nsfw posts
1094        let mut hide_nsfw: bool = true;
1095
1096        if let Some(ua) = user {
1097            hide_nsfw = !ua.settings.show_nsfw;
1098        }
1099
1100        // ...
1101        let res = query_rows!(
1102            &conn,
1103            &format!(
1104                "SELECT * FROM posts WHERE community = $1 AND replying_to = 0 AND NOT context LIKE '%\"is_pinned\":true%' AND is_deleted = 0 {} ORDER BY created DESC LIMIT $2 OFFSET $3",
1105                if hide_nsfw {
1106                    "AND NOT (context::json->>'is_nsfw')::boolean"
1107                } else {
1108                    ""
1109                }
1110            ),
1111            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
1112            |x| { Self::get_post_from_row(x) }
1113        );
1114
1115        if res.is_err() {
1116            return Err(Error::GeneralNotFound("post".to_string()));
1117        }
1118
1119        Ok(res.unwrap())
1120    }
1121
1122    /// Get all posts from the given community and topic (from most recent).
1123    ///
1124    /// # Arguments
1125    /// * `id` - the ID of the community the requested posts belong to
1126    /// * `topic` - the ID of the topic the requested posts belong to
1127    /// * `batch` - the limit of posts in each page
1128    /// * `page` - the page number
1129    pub async fn get_posts_by_community_topic(
1130        &self,
1131        id: usize,
1132        topic: usize,
1133        batch: usize,
1134        page: usize,
1135        user: &Option<User>,
1136    ) -> Result<Vec<Post>> {
1137        let conn = match self.0.connect().await {
1138            Ok(c) => c,
1139            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1140        };
1141
1142        // check if we should hide nsfw posts
1143        let mut hide_nsfw: bool = true;
1144
1145        if let Some(ua) = user {
1146            hide_nsfw = !ua.settings.show_nsfw;
1147        }
1148
1149        // ...
1150        let res = query_rows!(
1151            &conn,
1152            &format!(
1153                "SELECT * FROM posts WHERE community = $1 AND topic = $2 AND replying_to = 0 AND NOT context LIKE '%\"is_pinned\":true%' AND is_deleted = 0 {} ORDER BY created DESC LIMIT $3 OFFSET $4",
1154                if hide_nsfw {
1155                    "AND NOT (context::json->>'is_nsfw')::boolean"
1156                } else {
1157                    ""
1158                }
1159            ),
1160            &[
1161                &(id as i64),
1162                &(topic as i64),
1163                &(batch as i64),
1164                &((page * batch) as i64)
1165            ],
1166            |x| { Self::get_post_from_row(x) }
1167        );
1168
1169        if res.is_err() {
1170            return Err(Error::GeneralNotFound("post".to_string()));
1171        }
1172
1173        Ok(res.unwrap())
1174    }
1175
1176    /// Get all posts from the given stack (from most recent).
1177    ///
1178    /// # Arguments
1179    /// * `id` - the ID of the stack the requested posts belong to
1180    /// * `batch` - the limit of posts in each page
1181    /// * `page` - the page number
1182    pub async fn get_posts_by_stack(
1183        &self,
1184        id: usize,
1185        batch: usize,
1186        page: usize,
1187    ) -> Result<Vec<Post>> {
1188        let conn = match self.0.connect().await {
1189            Ok(c) => c,
1190            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1191        };
1192
1193        let res = query_rows!(
1194            &conn,
1195            "SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
1196            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
1197            |x| { Self::get_post_from_row(x) }
1198        );
1199
1200        if res.is_err() {
1201            return Err(Error::GeneralNotFound("post".to_string()));
1202        }
1203
1204        Ok(res.unwrap())
1205    }
1206
1207    /// Get all pinned posts from the given community (from most recent).
1208    ///
1209    /// # Arguments
1210    /// * `id` - the ID of the community the requested posts belong to
1211    pub async fn get_pinned_posts_by_community(&self, id: usize) -> Result<Vec<Post>> {
1212        let conn = match self.0.connect().await {
1213            Ok(c) => c,
1214            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1215        };
1216
1217        let res = query_rows!(
1218            &conn,
1219            "SELECT * FROM posts WHERE community = $1 AND context LIKE '%\"is_pinned\":true%' ORDER BY created DESC",
1220            &[&(id as i64),],
1221            |x| { Self::get_post_from_row(x) }
1222        );
1223
1224        if res.is_err() {
1225            return Err(Error::GeneralNotFound("post".to_string()));
1226        }
1227
1228        Ok(res.unwrap())
1229    }
1230
1231    /// Get all pinned posts from the given community (from most recent).
1232    ///
1233    /// # Arguments
1234    /// * `id` - the ID of the community the requested posts belong to
1235    /// * `topic` - the ID of the topic the requested posts belong to
1236    pub async fn get_pinned_posts_by_community_topic(
1237        &self,
1238        id: usize,
1239        topic: usize,
1240    ) -> Result<Vec<Post>> {
1241        let conn = match self.0.connect().await {
1242            Ok(c) => c,
1243            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1244        };
1245
1246        let res = query_rows!(
1247            &conn,
1248            "SELECT * FROM posts WHERE community = $1 AND topic = $2 AND context LIKE '%\"is_pinned\":true%' ORDER BY created DESC",
1249            &[&(id as i64), &(topic as i64)],
1250            |x| { Self::get_post_from_row(x) }
1251        );
1252
1253        if res.is_err() {
1254            return Err(Error::GeneralNotFound("post".to_string()));
1255        }
1256
1257        Ok(res.unwrap())
1258    }
1259
1260    /// Get all pinned posts from the given user (from most recent).
1261    ///
1262    /// # Arguments
1263    /// * `id` - the ID of the user the requested posts belong to
1264    pub async fn get_pinned_posts_by_user(&self, id: usize) -> Result<Vec<Post>> {
1265        let conn = match self.0.connect().await {
1266            Ok(c) => c,
1267            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1268        };
1269
1270        let res = query_rows!(
1271            &conn,
1272            "SELECT * FROM posts WHERE owner = $1 AND context LIKE '%\"is_profile_pinned\":true%' ORDER BY created DESC",
1273            &[&(id as i64),],
1274            |x| { Self::get_post_from_row(x) }
1275        );
1276
1277        if res.is_err() {
1278            return Err(Error::GeneralNotFound("post".to_string()));
1279        }
1280
1281        Ok(res.unwrap())
1282    }
1283
1284    /// Get all posts answering the given question (from most recent).
1285    ///
1286    /// # Arguments
1287    /// * `id` - the ID of the question the requested posts belong to
1288    /// * `batch` - the limit of posts in each page
1289    /// * `page` - the page number
1290    pub async fn get_posts_by_question(
1291        &self,
1292        id: usize,
1293        batch: usize,
1294        page: usize,
1295    ) -> Result<Vec<Post>> {
1296        let conn = match self.0.connect().await {
1297            Ok(c) => c,
1298            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1299        };
1300
1301        let res = query_rows!(
1302            &conn,
1303            "SELECT * FROM posts WHERE context LIKE $1 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
1304            params![
1305                &format!("%\"answering\":{id}%"),
1306                &(batch as i64),
1307                &((page * batch) as i64)
1308            ],
1309            |x| { Self::get_post_from_row(x) }
1310        );
1311
1312        if res.is_err() {
1313            return Err(Error::GeneralNotFound("post".to_string()));
1314        }
1315
1316        Ok(res.unwrap())
1317    }
1318
1319    /// Get a post given its owner and question ID.
1320    ///
1321    /// # Arguments
1322    /// * `owner` - the ID of the post owner
1323    /// * `question` - the ID of the post question
1324    pub async fn get_post_by_owner_question(&self, owner: usize, question: usize) -> Result<Post> {
1325        let conn = match self.0.connect().await {
1326            Ok(c) => c,
1327            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1328        };
1329
1330        let res = query_row!(
1331            &conn,
1332            "SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 AND is_deleted = 0 LIMIT 1",
1333            params![&format!("%\"answering\":{question}%"), &(owner as i64),],
1334            |x| { Ok(Self::get_post_from_row(x)) }
1335        );
1336
1337        if res.is_err() {
1338            return Err(Error::GeneralNotFound("post".to_string()));
1339        }
1340
1341        Ok(res.unwrap())
1342    }
1343
1344    /// Get all quoting posts by the post their quoting.
1345    ///
1346    /// Requires that the post has content. See [`Self::get_reposts_by_quoting`]
1347    /// for the no-content version.
1348    ///
1349    /// # Arguments
1350    /// * `id` - the ID of the post that is being quoted
1351    /// * `batch` - the limit of posts in each page
1352    /// * `page` - the page number
1353    pub async fn get_quoting_posts_by_quoting(
1354        &self,
1355        id: usize,
1356        batch: usize,
1357        page: usize,
1358    ) -> Result<Vec<Post>> {
1359        let conn = match self.0.connect().await {
1360            Ok(c) => c,
1361            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1362        };
1363
1364        let res = query_rows!(
1365            &conn,
1366            "SELECT * FROM posts WHERE NOT content = '' AND context LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
1367            params![
1368                &format!("%\"reposting\":{id}%"),
1369                &(batch as i64),
1370                &((page * batch) as i64)
1371            ],
1372            |x| { Self::get_post_from_row(x) }
1373        );
1374
1375        if res.is_err() {
1376            return Err(Error::GeneralNotFound("post".to_string()));
1377        }
1378
1379        Ok(res.unwrap())
1380    }
1381
1382    /// Get all quoting posts by the post their quoting.
1383    ///
1384    /// Requires that the post has no content. See [`Self::get_quoting_posts_by_quoting`]
1385    /// for the content-required version.
1386    ///
1387    /// # Arguments
1388    /// * `id` - the ID of the post that is being quoted
1389    /// * `batch` - the limit of posts in each page
1390    /// * `page` - the page number
1391    pub async fn get_reposts_by_quoting(
1392        &self,
1393        id: usize,
1394        batch: usize,
1395        page: usize,
1396    ) -> Result<Vec<Post>> {
1397        let conn = match self.0.connect().await {
1398            Ok(c) => c,
1399            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1400        };
1401
1402        let res = query_rows!(
1403            &conn,
1404            "SELECT * FROM posts WHERE content = '' AND context LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
1405            params![
1406                &format!("%\"reposting\":{id}%"),
1407                &(batch as i64),
1408                &((page * batch) as i64)
1409            ],
1410            |x| { Self::get_post_from_row(x) }
1411        );
1412
1413        if res.is_err() {
1414            return Err(Error::GeneralNotFound("post".to_string()));
1415        }
1416
1417        Ok(res.unwrap())
1418    }
1419
1420    /// Get posts from all communities, sorted by likes.
1421    ///
1422    /// # Arguments
1423    /// * `batch` - the limit of posts in each page
1424    /// * `page` - the page number
1425    /// * `cutoff` - the maximum number of milliseconds ago the post could have been created
1426    pub async fn get_popular_posts(
1427        &self,
1428        batch: usize,
1429        page: usize,
1430        cutoff: usize,
1431    ) -> Result<Vec<Post>> {
1432        let conn = match self.0.connect().await {
1433            Ok(c) => c,
1434            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1435        };
1436
1437        let res = query_rows!(
1438            &conn,
1439            "SELECT * FROM posts WHERE replying_to = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2 ORDER BY likes - dislikes DESC, created ASC LIMIT $3 OFFSET $4",
1440            &[
1441                &(unix_epoch_timestamp() as i64),
1442                &(cutoff as i64),
1443                &(batch as i64),
1444                &((page * batch) as i64)
1445            ],
1446            |x| { Self::get_post_from_row(x) }
1447        );
1448
1449        if res.is_err() {
1450            return Err(Error::GeneralNotFound("post".to_string()));
1451        }
1452
1453        Ok(res.unwrap())
1454    }
1455
1456    /// Get posts from all communities, sorted by creation.
1457    ///
1458    /// # Arguments
1459    /// * `batch` - the limit of posts in each page
1460    /// * `page` - the page number
1461    pub async fn get_latest_posts(
1462        &self,
1463        batch: usize,
1464        page: usize,
1465        as_user: &Option<User>,
1466        before_time: usize,
1467    ) -> Result<Vec<Post>> {
1468        let hide_answers: bool = if let Some(user) = as_user {
1469            user.settings.all_timeline_hide_answers
1470        } else {
1471            false
1472        };
1473
1474        // check if we should hide nsfw posts
1475        let mut hide_nsfw: bool = true;
1476
1477        if let Some(ua) = as_user {
1478            hide_nsfw = !ua.settings.show_nsfw;
1479        }
1480
1481        // ...
1482        let conn = match self.0.connect().await {
1483            Ok(c) => c,
1484            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1485        };
1486
1487        let res = query_rows!(
1488            &conn,
1489            &format!(
1490                "SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1491                if before_time > 0 {
1492                    format!(" AND created < {before_time}")
1493                } else {
1494                    String::new()
1495                },
1496                if hide_nsfw {
1497                    " AND NOT context LIKE '%\"is_nsfw\":true%'"
1498                } else {
1499                    ""
1500                },
1501                if hide_answers {
1502                    " AND context::jsonb->>'answering' = '0'"
1503                } else {
1504                    ""
1505                }
1506            ),
1507            &[&(batch as i64), &((page * batch) as i64)],
1508            |x| { Self::get_post_from_row(x) }
1509        );
1510
1511        if res.is_err() {
1512            return Err(Error::GeneralNotFound("post".to_string()));
1513        }
1514
1515        Ok(res.unwrap())
1516    }
1517
1518    /// Get forum posts from all communities, sorted by creation.
1519    ///
1520    /// # Arguments
1521    /// * `batch` - the limit of posts in each page
1522    /// * `page` - the page number
1523    pub async fn get_latest_forum_posts(
1524        &self,
1525        batch: usize,
1526        page: usize,
1527        as_user: &Option<User>,
1528        before_time: usize,
1529    ) -> Result<Vec<Post>> {
1530        // check if we should hide nsfw posts
1531        let mut hide_nsfw: bool = true;
1532
1533        if let Some(ua) = as_user {
1534            hide_nsfw = !ua.settings.show_nsfw;
1535        }
1536
1537        // ...
1538        let conn = match self.0.connect().await {
1539            Ok(c) => c,
1540            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1541        };
1542
1543        let res = query_rows!(
1544            &conn,
1545            &format!(
1546                "SELECT * FROM posts WHERE replying_to = 0{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1547                if before_time > 0 {
1548                    format!(" AND created < {before_time}")
1549                } else {
1550                    String::new()
1551                },
1552                if hide_nsfw {
1553                    " AND NOT context LIKE '%\"is_nsfw\":true%'"
1554                } else {
1555                    ""
1556                }
1557            ),
1558            &[&(batch as i64), &((page * batch) as i64)],
1559            |x| { Self::get_post_from_row(x) }
1560        );
1561
1562        if res.is_err() {
1563            return Err(Error::GeneralNotFound("post".to_string()));
1564        }
1565
1566        Ok(res.unwrap())
1567    }
1568
1569    /// Get posts from all communities the given user is in.
1570    ///
1571    /// # Arguments
1572    /// * `id` - the ID of the user
1573    /// * `batch` - the limit of posts in each page
1574    /// * `page` - the page number
1575    pub async fn get_posts_from_user_communities(
1576        &self,
1577        id: usize,
1578        batch: usize,
1579        page: usize,
1580        user: &User,
1581    ) -> Result<Vec<Post>> {
1582        let memberships = self.get_memberships_by_owner(id).await?;
1583        let mut memberships = memberships.iter();
1584        let first = match memberships.next() {
1585            Some(f) => f,
1586            None => return Ok(Vec::new()),
1587        };
1588
1589        let mut query_string: String = String::new();
1590
1591        for membership in memberships {
1592            query_string.push_str(&format!(" OR community = {}", membership.community));
1593        }
1594
1595        // check if we should hide nsfw posts
1596        let hide_nsfw: bool = !user.settings.show_nsfw;
1597
1598        // ...
1599        let conn = match self.0.connect().await {
1600            Ok(c) => c,
1601            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1602        };
1603
1604        let res = query_rows!(
1605            &conn,
1606            &format!(
1607                "SELECT * FROM posts WHERE (community = {} {query_string}){} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1608                first.community,
1609                if hide_nsfw {
1610                    " AND NOT context LIKE '%\"is_nsfw\":true%'"
1611                } else {
1612                    ""
1613                },
1614            ),
1615            &[&(batch as i64), &((page * batch) as i64)],
1616            |x| { Self::get_post_from_row(x) }
1617        );
1618
1619        if res.is_err() {
1620            return Err(Error::GeneralNotFound("post".to_string()));
1621        }
1622
1623        Ok(res.unwrap())
1624    }
1625
1626    /// Get posts from all users the given user is following.
1627    ///
1628    /// # Arguments
1629    /// * `id` - the ID of the user
1630    /// * `batch` - the limit of posts in each page
1631    /// * `page` - the page number
1632    pub async fn get_posts_from_user_following(
1633        &self,
1634        id: usize,
1635        batch: usize,
1636        page: usize,
1637    ) -> Result<Vec<Post>> {
1638        let following = self.get_userfollows_by_initiator_all(id).await?;
1639        let mut following = following.iter();
1640        let first = match following.next() {
1641            Some(f) => f,
1642            None => return Ok(Vec::new()),
1643        };
1644
1645        let mut query_string: String = String::new();
1646
1647        for user in following {
1648            query_string.push_str(&format!(" OR owner = {}", user.receiver));
1649        }
1650
1651        // ...
1652        let conn = match self.0.connect().await {
1653            Ok(c) => c,
1654            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1655        };
1656
1657        let res = query_rows!(
1658            &conn,
1659            &format!(
1660                "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1661                first.receiver
1662            ),
1663            &[&(batch as i64), &((page * batch) as i64)],
1664            |x| { Self::get_post_from_row(x) }
1665        );
1666
1667        if res.is_err() {
1668            return Err(Error::GeneralNotFound("post".to_string()));
1669        }
1670
1671        Ok(res.unwrap())
1672    }
1673
1674    /// Get posts from all users in the given stack.
1675    ///
1676    /// # Arguments
1677    /// * `id` - the ID of the stack
1678    /// * `batch` - the limit of posts in each page
1679    /// * `page` - the page number
1680    pub async fn get_posts_from_stack(
1681        &self,
1682        id: usize,
1683        batch: usize,
1684        page: usize,
1685        sort: StackSort,
1686    ) -> Result<Vec<Post>> {
1687        let users = self.get_stack_by_id(id).await?.users;
1688        let mut users = users.iter();
1689
1690        let first = match users.next() {
1691            Some(f) => f,
1692            None => return Ok(Vec::new()),
1693        };
1694
1695        let mut query_string: String = String::new();
1696
1697        for user in users {
1698            query_string.push_str(&format!(" OR owner = {}", user));
1699        }
1700
1701        // ...
1702        let conn = match self.0.connect().await {
1703            Ok(c) => c,
1704            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1705        };
1706
1707        let res = query_rows!(
1708            &conn,
1709            &format!(
1710                "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY {} DESC LIMIT $1 OFFSET $2",
1711                first,
1712                if sort == StackSort::Created {
1713                    "created"
1714                } else {
1715                    "likes"
1716                }
1717            ),
1718            &[&(batch as i64), &((page * batch) as i64)],
1719            |x| { Self::get_post_from_row(x) }
1720        );
1721
1722        if res.is_err() {
1723            return Err(Error::GeneralNotFound("post".to_string()));
1724        }
1725
1726        Ok(res.unwrap())
1727    }
1728
1729    /// Check if the given `uid` can post in the given `community`.
1730    pub async fn check_can_post(&self, community: &Community, uid: usize) -> bool {
1731        match community.write_access {
1732            CommunityWriteAccess::Owner => uid == community.owner,
1733            CommunityWriteAccess::Joined => {
1734                match self
1735                    .get_membership_by_owner_community(uid, community.id)
1736                    .await
1737                {
1738                    Ok(m) => m.role.check_member(),
1739                    Err(_) => false,
1740                }
1741            }
1742            _ => true,
1743        }
1744    }
1745
1746    /// Check if the given `uid` can post in the given `community` with the given `access`.
1747    pub async fn check_can_post_with_access(
1748        &self,
1749        community: &Community,
1750        access: &CommunityWriteAccess,
1751        uid: usize,
1752    ) -> bool {
1753        match *access {
1754            CommunityWriteAccess::Owner => uid == community.owner,
1755            CommunityWriteAccess::Joined => {
1756                match self
1757                    .get_membership_by_owner_community(uid, community.id)
1758                    .await
1759                {
1760                    Ok(m) => m.role.check_member(),
1761                    Err(_) => false,
1762                }
1763            }
1764            _ => true,
1765        }
1766    }
1767
1768    /// Create a new post in the database.
1769    ///
1770    /// # Arguments
1771    /// * `data` - a mock [`Post`] object to insert
1772    pub async fn create_post(&self, mut data: Post) -> Result<usize> {
1773        // check characters
1774        for ban in &self.0.0.banned_data {
1775            match ban {
1776                StringBan::String(x) => {
1777                    if data.content.contains(x) {
1778                        return Ok(0);
1779                    }
1780                }
1781                StringBan::Unicode(x) => {
1782                    if data.content.contains(&match char::from_u32(x.to_owned()) {
1783                        Some(c) => c.to_string(),
1784                        None => continue,
1785                    }) {
1786                        return Ok(0);
1787                    }
1788                }
1789            }
1790        }
1791
1792        // check stack
1793        if data.stack != 0 {
1794            let stack = self.get_stack_by_id(data.stack).await?;
1795
1796            if stack.mode != StackMode::Circle {
1797                return Err(Error::MiscError(
1798                    "You must use a \"Circle\" stack for this".to_string(),
1799                ));
1800            }
1801
1802            if stack.owner != data.owner && !stack.users.contains(&data.owner) {
1803                return Err(Error::NotAllowed);
1804            }
1805        }
1806
1807        // ...
1808        let community = if data.stack != 0 {
1809            // if we're posting to a stack, the community should always be the town square
1810            data.community = self.0.0.town_square;
1811            self.get_community_by_id(self.0.0.town_square).await?
1812        } else {
1813            // otherwise, load whatever community the post is requesting
1814            self.get_community_by_id(data.community).await?
1815        };
1816
1817        // check is_forum
1818        if community.is_forum {
1819            if data.topic == 0 {
1820                return Err(Error::MiscError(
1821                    "Topic is required for this community".to_string(),
1822                ));
1823            }
1824
1825            if let Some(topic) = community.topics.get(&data.topic) {
1826                // check permission
1827                if !self
1828                    .check_can_post_with_access(&community, &topic.write_access, data.owner)
1829                    .await
1830                {
1831                    return Err(Error::NotAllowed);
1832                }
1833            } else {
1834                return Err(Error::GeneralNotFound("topic".to_string()));
1835            }
1836        } else if data.topic != 0 {
1837            return Err(Error::DoesNotSupportField("Community".to_string()));
1838        }
1839
1840        // ...
1841        let mut owner = self.get_user_by_id(data.owner).await?;
1842
1843        // check values (if this isn't reposting something else)
1844        let is_reposting = if let Some(ref repost) = data.context.repost {
1845            repost.reposting.is_some()
1846        } else {
1847            false
1848        };
1849
1850        if !is_reposting {
1851            if data.content.len() < 2 && data.uploads.len() == 0 {
1852                return Err(Error::DataTooShort("content".to_string()));
1853            } else if data.content.len() > 4096 {
1854                return Err(Error::DataTooLong("content".to_string()));
1855            }
1856
1857            // check title
1858            if !community.context.enable_titles {
1859                if !data.title.is_empty() {
1860                    return Err(Error::MiscError(
1861                        "Community does not allow titles".to_string(),
1862                    ));
1863                }
1864            } else if data.replying_to.is_none() {
1865                if data.title.trim().len() < 2 && community.context.require_titles {
1866                    return Err(Error::DataTooShort("title".to_string()));
1867                } else if data.title.len() > 128 {
1868                    return Err(Error::DataTooLong("title".to_string()));
1869                }
1870
1871                // award achievement
1872                self.add_achievement(
1873                    &mut owner,
1874                    AchievementName::CreatePostWithTitle.into(),
1875                    true,
1876                )
1877                .await?;
1878            }
1879        }
1880
1881        // check permission in community
1882        if !self.check_can_post(&community, data.owner).await {
1883            return Err(Error::NotAllowed);
1884        }
1885
1886        // mirror nsfw state
1887        data.context.is_nsfw = community.context.is_nsfw;
1888
1889        // remove request if we were answering a question
1890        if data.context.answering != 0 {
1891            let question = self.get_question_by_id(data.context.answering).await?;
1892
1893            // check if we've already answered this
1894            if self
1895                .get_post_by_owner_question(owner.id, question.id)
1896                .await
1897                .is_ok()
1898            {
1899                return Err(Error::MiscError(
1900                    "You've already answered this question".to_string(),
1901                ));
1902            }
1903
1904            if !question.is_global {
1905                self.delete_request(question.id, question.id, &owner, false)
1906                    .await?;
1907            } else {
1908                self.incr_question_answer_count(data.context.answering)
1909                    .await?;
1910            }
1911
1912            // create notification for question owner
1913            // (if the current user isn't the owner)
1914            if (question.owner != data.owner)
1915                && (question.owner != 0)
1916                && (!owner.settings.private_profile
1917                    | self
1918                        .get_userfollow_by_initiator_receiver(data.owner, question.owner)
1919                        .await
1920                        .is_ok())
1921            {
1922                self.create_notification(Notification::new(
1923                    "Your question has received a new answer!".to_string(),
1924                    format!(
1925                        "[@{}](/api/v1/auth/user/find/{}) has answered your [question](/question/{}).",
1926                        owner.username, owner.id, question.id
1927                    ),
1928                    question.owner,
1929                ))
1930                .await?;
1931            }
1932
1933            // inherit nsfw status if we didn't get it from the community
1934            if question.context.is_nsfw {
1935                data.context.is_nsfw = question.context.is_nsfw;
1936            }
1937        }
1938
1939        // check if we're reposting a post
1940        let reposting = if let Some(ref repost) = data.context.repost {
1941            if let Some(id) = repost.reposting {
1942                Some(self.get_post_by_id(id).await?)
1943            } else {
1944                None
1945            }
1946        } else {
1947            None
1948        };
1949
1950        if let Some(ref rt) = reposting {
1951            if rt.stack != data.stack && rt.stack != 0 {
1952                return Err(Error::MiscError("Cannot repost out of stack".to_string()));
1953            }
1954
1955            if data.content.is_empty() {
1956                // reposting but NOT quoting... we shouldn't be able to repost a direct repost
1957                data.context.reposts_enabled = false;
1958                data.context.reactions_enabled = false;
1959                data.context.comments_enabled = false;
1960            }
1961
1962            // mirror nsfw status
1963            if rt.context.is_nsfw {
1964                data.context.is_nsfw = true;
1965            }
1966
1967            // // make sure we aren't trying to repost a repost
1968            // if if let Some(ref repost) = rt.context.repost {
1969            //     repost.reposting.is_some()
1970            // } else {
1971            //     false
1972            // } {
1973            //     return Err(Error::MiscError("Cannot repost a repost".to_string()));
1974            // }
1975
1976            // ...
1977            if !rt.context.reposts_enabled {
1978                return Err(Error::MiscError("Post has reposts disabled".to_string()));
1979            }
1980
1981            // check blocked status
1982            if self
1983                .get_userblock_by_initiator_receiver(rt.owner, data.owner)
1984                .await
1985                .is_ok()
1986                | self
1987                    .get_user_stack_blocked_users(rt.owner)
1988                    .await
1989                    .contains(&data.owner)
1990            {
1991                return Err(Error::NotAllowed);
1992            }
1993
1994            // send notification
1995            // this would look better if rustfmt didn't give up on this line
1996            if owner.id != rt.owner && !owner.settings.private_profile && data.stack == 0 {
1997                self.create_notification(
1998                    Notification::new(
1999                        format!(
2000                            "[@{}](/api/v1/auth/user/find/{}) has [quoted](/post/{}) your [post](/post/{})",
2001                            owner.username,
2002                            owner.id,
2003                            data.id,
2004                            rt.id
2005                        ),
2006                        if data.content.is_empty() {
2007                            String::new()
2008                        } else {
2009                            format!("\"{}\"", data.content)
2010                        },
2011                        rt.owner
2012                    )
2013                )
2014                .await?;
2015            }
2016
2017            // award achievement
2018            self.add_achievement(&mut owner, AchievementName::CreateRepost.into(), true)
2019                .await?;
2020        }
2021
2022        // check if the post we're replying to allows commments
2023        let replying_to = if let Some(id) = data.replying_to {
2024            Some(self.get_post_by_id(id).await?)
2025        } else {
2026            None
2027        };
2028
2029        if let Some(ref rt) = replying_to {
2030            if !rt.context.comments_enabled {
2031                return Err(Error::MiscError("Post has comments disabled".to_string()));
2032            }
2033
2034            // check blocked status
2035            if self
2036                .get_userblock_by_initiator_receiver(rt.owner, data.owner)
2037                .await
2038                .is_ok()
2039                | self
2040                    .get_user_stack_blocked_users(rt.owner)
2041                    .await
2042                    .contains(&data.owner)
2043            {
2044                return Err(Error::NotAllowed);
2045            }
2046        }
2047
2048        // send mention notifications
2049        let mut already_notified: HashMap<String, User> = HashMap::new();
2050        for username in User::parse_mentions(&data.content) {
2051            let user = {
2052                if let Some(ua) = already_notified.get(&username) {
2053                    ua.to_owned()
2054                } else {
2055                    let user = self.get_user_by_username(&username).await?;
2056
2057                    // check blocked status
2058                    if self
2059                        .get_userblock_by_initiator_receiver(user.id, data.owner)
2060                        .await
2061                        .is_ok()
2062                        | self
2063                            .get_user_stack_blocked_users(user.id)
2064                            .await
2065                            .contains(&data.owner)
2066                    {
2067                        return Err(Error::NotAllowed);
2068                    }
2069
2070                    // check private status
2071                    if user.settings.private_profile {
2072                        if self
2073                            .get_userfollow_by_initiator_receiver(user.id, data.owner)
2074                            .await
2075                            .is_err()
2076                        {
2077                            return Err(Error::NotAllowed);
2078                        }
2079                    }
2080
2081                    // send notif
2082                    self.create_notification(Notification::new(
2083                        "You've been mentioned in a post!".to_string(),
2084                        format!(
2085                            "[@{}](/api/v1/auth/user/find/{}) has mentioned you in their [post](/post/{}).",
2086                            owner.username, owner.id, data.id
2087                        ),
2088                        user.id,
2089                    ))
2090                    .await?;
2091
2092                    // ...
2093                    already_notified.insert(username.to_owned(), user.clone());
2094                    user
2095                }
2096            };
2097
2098            data.content = data.content.replace(
2099                &format!("@{username}"),
2100                &format!("[@{username}](/api/v1/auth/user/find/{})", user.id),
2101            );
2102        }
2103
2104        // auto unlist
2105        if owner.settings.auto_unlist {
2106            data.context.is_nsfw = true;
2107        }
2108
2109        if owner.settings.auto_full_unlist {
2110            data.context.full_unlist = true;
2111        }
2112
2113        // ...
2114        let conn = match self.0.connect().await {
2115            Ok(c) => c,
2116            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2117        };
2118
2119        let replying_to_id = data.replying_to.unwrap_or(0).to_string();
2120
2121        let res = execute!(
2122            &conn,
2123            "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13, $14, $15, $16, $17)",
2124            params![
2125                &(data.id as i64),
2126                &(data.created as i64),
2127                &data.content,
2128                &(data.owner as i64),
2129                &(data.community as i64),
2130                &serde_json::to_string(&data.context).unwrap(),
2131                &if replying_to_id != "0" {
2132                    replying_to_id.parse::<i64>().unwrap()
2133                } else {
2134                    0_i64
2135                },
2136                &0_i32,
2137                &0_i32,
2138                &0_i32,
2139                &serde_json::to_string(&data.uploads).unwrap(),
2140                &{ if data.is_deleted { 1 } else { 0 } },
2141                &(data.poll_id as i64),
2142                &data.title,
2143                &{ if data.is_open { 1 } else { 0 } },
2144                &(data.stack as i64),
2145                &(data.topic as i64),
2146            ]
2147        );
2148
2149        if let Err(e) = res {
2150            return Err(Error::DatabaseError(e.to_string()));
2151        }
2152
2153        // incr comment count and send notification
2154        if let Some(rt) = replying_to {
2155            self.incr_post_comments(rt.id).await.unwrap();
2156
2157            // send notification
2158            if data.owner != rt.owner {
2159                let owner = self.get_user_by_id(data.owner).await?;
2160
2161                // make sure we're actually following the person we're commenting to
2162                // we shouldn't send the notif if we aren't, because they can't see it
2163                // (only if our profile is private)
2164                if !owner.settings.private_profile
2165                    | self
2166                        .get_userfollow_by_initiator_receiver(data.owner, rt.owner)
2167                        .await
2168                        .is_ok()
2169                {
2170                    self.create_notification(Notification::new(
2171                        "Your post has received a new comment!".to_string(),
2172                        format!(
2173                            "[@{}](/api/v1/auth/user/find/{}) has commented on your [post](/post/{}).",
2174                            owner.username, owner.id, rt.id
2175                        ),
2176                        rt.owner,
2177                    ))
2178                    .await?;
2179                }
2180
2181                if !rt.context.comments_enabled {
2182                    return Err(Error::NotAllowed);
2183                }
2184            }
2185        }
2186
2187        // increase user post count
2188        self.incr_user_post_count(data.owner).await?;
2189
2190        // increase community post count
2191        self.incr_community_post_count(data.community).await?;
2192
2193        // return
2194        Ok(data.id)
2195    }
2196
2197    pub async fn delete_post(&self, id: usize, user: User) -> Result<()> {
2198        let y = self.get_post_by_id(id).await?;
2199
2200        let user_membership = self
2201            .get_membership_by_owner_community(user.id, y.community)
2202            .await?;
2203
2204        if (user.id != y.owner)
2205            && !user_membership
2206                .role
2207                .check(CommunityPermission::MANAGE_POSTS)
2208        {
2209            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2210                return Err(Error::NotAllowed);
2211            } else {
2212                self.create_audit_log_entry(AuditLogEntry::new(
2213                    user.id,
2214                    format!("invoked `delete_post` with x value `{id}`"),
2215                ))
2216                .await?
2217            }
2218        }
2219
2220        let conn = match self.0.connect().await {
2221            Ok(c) => c,
2222            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2223        };
2224
2225        let res = execute!(&conn, "DELETE FROM posts WHERE id = $1", &[&(id as i64)]);
2226
2227        if let Err(e) = res {
2228            return Err(Error::DatabaseError(e.to_string()));
2229        }
2230
2231        self.0.1.remove(format!("atto.post:{}", id)).await;
2232
2233        // decr parent comment count
2234        if let Some(replying_to) = y.replying_to {
2235            if replying_to != 0 {
2236                self.decr_post_comments(replying_to).await.unwrap();
2237            }
2238        }
2239
2240        // decr user post count
2241        let owner = self.get_user_by_id(y.owner).await?;
2242
2243        if owner.post_count > 0 {
2244            self.decr_user_post_count(y.owner).await?;
2245        }
2246
2247        // decr community post count
2248        let community = self.get_community_by_id_no_void(y.community).await?;
2249
2250        if community.post_count > 0 {
2251            self.decr_community_post_count(y.community).await?;
2252        }
2253
2254        // decr question answer count
2255        if y.context.answering != 0 {
2256            let question = self.get_question_by_id(y.context.answering).await?;
2257
2258            if question.is_global {
2259                self.decr_question_answer_count(y.context.answering).await?;
2260            }
2261        }
2262
2263        // delete uploads
2264        for upload in y.uploads {
2265            self.delete_upload(upload).await?;
2266        }
2267
2268        // remove poll
2269        if y.poll_id != 0 {
2270            self.delete_poll(y.poll_id, &user).await?;
2271        }
2272
2273        // delete question (if not global question)
2274        if y.context.answering != 0 {
2275            let question = self.get_question_by_id(y.context.answering).await?;
2276
2277            if !question.is_global {
2278                self.delete_question(question.id, &user).await?;
2279            }
2280        }
2281
2282        // return
2283        Ok(())
2284    }
2285
2286    pub async fn fake_delete_post(&self, id: usize, user: User, is_deleted: bool) -> Result<()> {
2287        let y = self.get_post_by_id(id).await?;
2288
2289        let user_membership = self
2290            .get_membership_by_owner_community(user.id, y.community)
2291            .await?;
2292
2293        if (user.id != y.owner)
2294            && !user_membership
2295                .role
2296                .check(CommunityPermission::MANAGE_POSTS)
2297        {
2298            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2299                return Err(Error::NotAllowed);
2300            } else {
2301                self.create_audit_log_entry(AuditLogEntry::new(
2302                    user.id,
2303                    format!("invoked `fake_delete_post` with x value `{id}`"),
2304                ))
2305                .await?
2306            }
2307        }
2308
2309        let conn = match self.0.connect().await {
2310            Ok(c) => c,
2311            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2312        };
2313
2314        let res = execute!(
2315            &conn,
2316            "UPDATE posts SET is_deleted = $1 WHERE id = $2",
2317            params![&if is_deleted { 1 } else { 0 }, &(id as i64)]
2318        );
2319
2320        if let Err(e) = res {
2321            return Err(Error::DatabaseError(e.to_string()));
2322        }
2323
2324        self.0.1.remove(format!("atto.post:{}", id)).await;
2325
2326        if is_deleted {
2327            // decr parent comment count
2328            if let Some(replying_to) = y.replying_to {
2329                if replying_to != 0 {
2330                    self.decr_post_comments(replying_to).await.unwrap();
2331                }
2332            }
2333
2334            // decr user post count
2335            let owner = self.get_user_by_id(y.owner).await?;
2336
2337            if owner.post_count > 0 {
2338                self.decr_user_post_count(y.owner).await?;
2339            }
2340
2341            // decr community post count
2342            let community = self.get_community_by_id_no_void(y.community).await?;
2343
2344            if community.post_count > 0 {
2345                self.decr_community_post_count(y.community).await?;
2346            }
2347
2348            // decr question answer count
2349            if y.context.answering != 0 {
2350                let question = self.get_question_by_id(y.context.answering).await?;
2351
2352                if question.is_global {
2353                    self.decr_question_answer_count(y.context.answering).await?;
2354                }
2355            }
2356
2357            // delete uploads
2358            for upload in y.uploads {
2359                self.delete_upload(upload).await?;
2360            }
2361
2362            // delete question (if not global question)
2363            if y.context.answering != 0 {
2364                let question = self.get_question_by_id(y.context.answering).await?;
2365
2366                if !question.is_global {
2367                    self.delete_question(question.id, &user).await?;
2368                }
2369            }
2370        } else {
2371            // incr parent comment count
2372            if let Some(replying_to) = y.replying_to {
2373                self.incr_post_comments(replying_to).await.unwrap();
2374            }
2375
2376            // incr user post count
2377            self.incr_user_post_count(y.owner).await?;
2378
2379            // incr community post count
2380            self.incr_community_post_count(y.community).await?;
2381
2382            // incr question answer count
2383            if y.context.answering != 0 {
2384                let question = self.get_question_by_id(y.context.answering).await?;
2385
2386                if question.is_global {
2387                    self.incr_question_answer_count(y.context.answering).await?;
2388                }
2389            }
2390
2391            // unfortunately, uploads will not be restored
2392        }
2393
2394        // return
2395        Ok(())
2396    }
2397
2398    pub async fn update_post_is_open(&self, id: usize, user: User, is_open: bool) -> Result<()> {
2399        let y = self.get_post_by_id(id).await?;
2400
2401        // make sure this is a forge community
2402        let community = self.get_community_by_id(y.community).await?;
2403
2404        if !community.is_forge {
2405            return Err(Error::MiscError(
2406                "This community does not support this".to_string(),
2407            ));
2408        }
2409
2410        // check permissions
2411        let user_membership = self
2412            .get_membership_by_owner_community(user.id, y.community)
2413            .await?;
2414
2415        if (user.id != y.owner)
2416            && !user_membership
2417                .role
2418                .check(CommunityPermission::MANAGE_POSTS)
2419        {
2420            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2421                return Err(Error::NotAllowed);
2422            } else {
2423                self.create_audit_log_entry(AuditLogEntry::new(
2424                    user.id,
2425                    format!("invoked `update_post_is_open` with x value `{id}`"),
2426                ))
2427                .await?
2428            }
2429        }
2430
2431        // ...
2432        let conn = match self.0.connect().await {
2433            Ok(c) => c,
2434            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2435        };
2436
2437        let res = execute!(
2438            &conn,
2439            "UPDATE posts SET is_open = $1 WHERE id = $2",
2440            params![&if is_open { 1 } else { 0 }, &(id as i64)]
2441        );
2442
2443        if let Err(e) = res {
2444            return Err(Error::DatabaseError(e.to_string()));
2445        }
2446
2447        self.0.1.remove(format!("atto.post:{}", id)).await;
2448        Ok(())
2449    }
2450
2451    pub async fn update_post_context(
2452        &self,
2453        id: usize,
2454        user: User,
2455        mut x: PostContext,
2456    ) -> Result<()> {
2457        let y = self.get_post_by_id(id).await?;
2458        x.repost = y.context.repost; // cannot change repost settings at all
2459        x.answering = y.context.answering; // cannot change answering settings at all
2460
2461        let user_membership = self
2462            .get_membership_by_owner_community(user.id, y.community)
2463            .await?;
2464
2465        if (user.id != y.owner)
2466            && !user_membership
2467                .role
2468                .check(CommunityPermission::MANAGE_POSTS)
2469        {
2470            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2471                return Err(Error::NotAllowed);
2472            } else {
2473                self.create_audit_log_entry(AuditLogEntry::new(
2474                    user.id,
2475                    format!("invoked `update_post_context` with x value `{id}`"),
2476                ))
2477                .await?
2478            }
2479        }
2480
2481        // check if we can manage pins
2482        if x.is_pinned != y.context.is_pinned
2483            && !user_membership.role.check(CommunityPermission::MANAGE_PINS)
2484        {
2485            // lacking this permission is overtaken by having the MANAGE_POSTS
2486            // global permission
2487            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2488                return Err(Error::NotAllowed);
2489            } else {
2490                self.create_audit_log_entry(AuditLogEntry::new(
2491                    user.id,
2492                    format!("invoked `update_post_context(pinned)` with x value `{id}`"),
2493                ))
2494                .await?
2495            }
2496        }
2497
2498        // check if we can manage profile pins
2499        if (x.is_profile_pinned != y.context.is_profile_pinned) && (user.id != y.owner) {
2500            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2501                return Err(Error::NotAllowed);
2502            } else {
2503                self.create_audit_log_entry(AuditLogEntry::new(
2504                    user.id,
2505                    format!("invoked `update_post_context(profile_pinned)` with x value `{id}`"),
2506                ))
2507                .await?
2508            }
2509        }
2510
2511        // auto unlist
2512        if user.settings.auto_unlist {
2513            x.is_nsfw = true;
2514        }
2515
2516        if user.settings.auto_full_unlist {
2517            x.full_unlist = true;
2518        }
2519
2520        // ...
2521        let conn = match self.0.connect().await {
2522            Ok(c) => c,
2523            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2524        };
2525
2526        let res = execute!(
2527            &conn,
2528            "UPDATE posts SET context = $1 WHERE id = $2",
2529            params![&serde_json::to_string(&x).unwrap(), &(id as i64)]
2530        );
2531
2532        if let Err(e) = res {
2533            return Err(Error::DatabaseError(e.to_string()));
2534        }
2535
2536        self.0.1.remove(format!("atto.post:{}", id)).await;
2537
2538        // return
2539        Ok(())
2540    }
2541
2542    pub async fn update_post_content(&self, id: usize, user: User, x: String) -> Result<()> {
2543        let mut y = self.get_post_by_id(id).await?;
2544
2545        if user.id != y.owner {
2546            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2547                return Err(Error::NotAllowed);
2548            } else {
2549                self.create_audit_log_entry(AuditLogEntry::new(
2550                    user.id,
2551                    format!("invoked `update_post_content` with x value `{id}`"),
2552                ))
2553                .await?
2554            }
2555        }
2556
2557        // check length
2558        if x.len() < 2 {
2559            return Err(Error::DataTooShort("content".to_string()));
2560        } else if x.len() > 4096 {
2561            return Err(Error::DataTooLong("content".to_string()));
2562        }
2563
2564        // ...
2565        let conn = match self.0.connect().await {
2566            Ok(c) => c,
2567            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2568        };
2569
2570        let res = execute!(
2571            &conn,
2572            "UPDATE posts SET content = $1 WHERE id = $2",
2573            params![&x, &(id as i64)]
2574        );
2575
2576        if let Err(e) = res {
2577            return Err(Error::DatabaseError(e.to_string()));
2578        }
2579
2580        // update context
2581        y.context.edited = unix_epoch_timestamp();
2582        self.update_post_context(id, user, y.context).await?;
2583
2584        // return
2585        Ok(())
2586    }
2587
2588    pub async fn update_post_title(&self, id: usize, user: User, x: String) -> Result<()> {
2589        let mut y = self.get_post_by_id(id).await?;
2590
2591        if user.id != y.owner {
2592            if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2593                return Err(Error::NotAllowed);
2594            } else {
2595                self.create_audit_log_entry(AuditLogEntry::new(
2596                    user.id,
2597                    format!("invoked `update_post_title` with x value `{id}`"),
2598                ))
2599                .await?
2600            }
2601        }
2602
2603        let community = self.get_community_by_id(y.community).await?;
2604
2605        if !community.context.enable_titles {
2606            return Err(Error::MiscError(
2607                "Community does not allow titles".to_string(),
2608            ));
2609        }
2610
2611        if x.len() < 2 && community.context.require_titles {
2612            return Err(Error::DataTooShort("title".to_string()));
2613        } else if x.len() > 128 {
2614            return Err(Error::DataTooLong("title".to_string()));
2615        }
2616
2617        // ...
2618        let conn = match self.0.connect().await {
2619            Ok(c) => c,
2620            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2621        };
2622
2623        let res = execute!(
2624            &conn,
2625            "UPDATE posts SET title = $1 WHERE id = $2",
2626            params![&x, &(id as i64)]
2627        );
2628
2629        if let Err(e) = res {
2630            return Err(Error::DatabaseError(e.to_string()));
2631        }
2632
2633        // update context
2634        y.context.edited = unix_epoch_timestamp();
2635        self.update_post_context(id, user, y.context).await?;
2636
2637        // return
2638        Ok(())
2639    }
2640
2641    auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2642    auto_method!(incr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2643    auto_method!(decr_post_likes() -> "UPDATE posts SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
2644    auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
2645
2646    auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2647    auto_method!(decr_post_comments()@get_post_by_id -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr=comment_count);
2648}