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