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 if let Some(replying) = $post.replying_to {
35 if replying != 0 {
36 if let Some(post) = $replying_posts.get(&replying) {
37 if post.owner != $ua1.id {
39 continue;
42 }
43 } else {
44 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 if let Some(replying) = $post.replying_to {
66 if replying != 0 {
67 if let Some(post) = $replying_posts.get(&replying) {
68 if post.owner != $user_id {
70 continue;
73 }
74 } else {
75 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 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: get!(x->7(i32)) as isize,
106 dislikes: get!(x->8(i32)) as isize,
107 comment_count: get!(x->9(i32)) as usize,
109 uploads: serde_json::from_str(&get!(x->10(String))).unwrap(),
111 is_deleted: get!(x->11(i32)) as i8 == 1,
112 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 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 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 let owner = match self.get_user_by_id(x.owner).await {
182 Ok(ua) => ua,
183 Err(_) => return (true, None),
184 };
185
186 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 return (!post.content.is_empty(), None);
197 }
198 } else {
199 return (!post.content.is_empty(), None);
201 }
202 }
203
204 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 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 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 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 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 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 if ua.settings.require_account && user.is_none() {
360 continue;
361 }
362
363 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 let (can_view, reposting) =
378 self.get_post_reposting(&post, ignore_users, user).await;
379
380 if !can_view {
381 continue;
382 }
383
384 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 if ua.settings.private_profile {
411 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 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 continue;
443 }
444 }
445
446 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 let (can_view, reposting) =
461 self.get_post_reposting(&post, ignore_users, user).await;
462
463 if !can_view {
464 continue;
465 }
466
467 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 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 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 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 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 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 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 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 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 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 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 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 pub fn posts_muted_phrase_filter(
704 &self,
705 posts: &Vec<FullPost>,
706 muted: Option<&Vec<String>>,
707 ) -> Vec<FullPost> {
708 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 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 if let Some((ref mut x, _)) = post.3 {
760 x.clean();
761 }
762
763 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 out.push(post);
774 }
775
776 out
777 }
778
779 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 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 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 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 let mut hide_nsfw: bool = true;
892
893 if let Some(ua) = user {
894 hide_nsfw = !ua.settings.show_nsfw;
895 }
896
897 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 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 let mut hide_nsfw: bool = true;
939
940 if let Some(ua) = user {
941 hide_nsfw = !ua.settings.show_nsfw;
942 }
943
944 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 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 let mut hide_nsfw: bool = true;
989
990 if let Some(ua) = user {
991 hide_nsfw = !ua.settings.show_nsfw;
992 }
993
994 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 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 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 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 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 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 let mut hide_nsfw: bool = true;
1150
1151 if let Some(ua) = user {
1152 hide_nsfw = !ua.settings.show_nsfw;
1153 }
1154
1155 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 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 let mut hide_nsfw: bool = true;
1199
1200 if let Some(ua) = user {
1201 hide_nsfw = !ua.settings.show_nsfw;
1202 }
1203
1204 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 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 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 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 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 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 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 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 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 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 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 let mut hide_nsfw: bool = true;
1545
1546 if let Some(ua) = as_user {
1547 hide_nsfw = !ua.settings.show_nsfw;
1548 }
1549
1550 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 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 let mut hide_nsfw: bool = true;
1600
1601 if let Some(ua) = as_user {
1602 hide_nsfw = !ua.settings.show_nsfw;
1603 }
1604
1605 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 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 let hide_nsfw: bool = !user.settings.show_nsfw;
1660
1661 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 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 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 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 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 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 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 pub async fn create_post(&self, mut data: Post) -> Result<usize> {
1846 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 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 if stack.owner != data.owner {
1882 return Err(Error::NotAllowed);
1883 }
1884 }
1885 }
1886
1887 let community = if data.stack != 0 {
1889 data.community = self.0.0.town_square;
1891 self.get_community_by_id(self.0.0.town_square).await?
1892 } else {
1893 self.get_community_by_id(data.community).await?
1895 };
1896
1897 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 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 let mut owner = self.get_user_by_id(data.owner).await?;
1922
1923 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 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 self.add_achievement(
1953 &mut owner,
1954 AchievementName::CreatePostWithTitle.into(),
1955 true,
1956 )
1957 .await?;
1958 }
1959 }
1960
1961 if !self.check_can_post(&community, data.owner).await {
1963 return Err(Error::NotAllowed);
1964 }
1965
1966 data.context.is_nsfw = community.context.is_nsfw;
1968
1969 if data.context.answering != 0 {
1971 let question = self.get_question_by_id(data.context.answering).await?;
1972
1973 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 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 if question.context.is_nsfw {
2015 data.context.is_nsfw = question.context.is_nsfw;
2016 }
2017 }
2018
2019 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 data.context.reposts_enabled = false;
2038 data.context.reactions_enabled = false;
2039 data.context.comments_enabled = false;
2040 }
2041
2042 if rt.context.is_nsfw {
2044 data.context.is_nsfw = true;
2045 }
2046
2047 if !rt.context.reposts_enabled {
2058 return Err(Error::MiscError("Post has reposts disabled".to_string()));
2059 }
2060
2061 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 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 self.add_achievement(&mut owner, AchievementName::CreateRepost.into(), true)
2099 .await?;
2100 }
2101
2102 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 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 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 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 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 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 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 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 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 if let Some(rt) = replying_to {
2234 self.incr_post_comments(rt.id).await.unwrap();
2235
2236 if data.owner != rt.owner {
2238 let owner = self.get_user_by_id(data.owner).await?;
2239
2240 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 self.incr_user_post_count(data.owner).await?;
2268
2269 self.incr_community_post_count(data.community).await?;
2271
2272 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 if let Some(replying_to) = y.replying_to
2314 && replying_to != 0 {
2315 self.decr_post_comments(replying_to).await.unwrap();
2316 }
2317
2318 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 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 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 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 if y.poll_id != 0 {
2350 self.delete_poll(y.poll_id, &user).await?;
2351 }
2352
2353 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 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 if let Some(replying_to) = y.replying_to
2409 && replying_to != 0 {
2410 self.decr_post_comments(replying_to).await.unwrap();
2411 }
2412
2413 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 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 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 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 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 if let Some(replying_to) = y.replying_to {
2454 self.incr_post_comments(replying_to).await.unwrap();
2455 }
2456
2457 self.incr_user_post_count(y.owner).await?;
2459
2460 self.incr_community_post_count(y.community).await?;
2462
2463 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 }
2474
2475 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 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 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 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; x.answering = y.context.answering; 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 if x.is_pinned != y.context.is_pinned
2564 && !user_membership.role.check(CommunityPermission::MANAGE_PINS)
2565 {
2566 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 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 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 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 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 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 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 y.context.edited = unix_epoch_timestamp();
2663 self.update_post_context(id, user, y.context).await?;
2664
2665 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 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 y.context.edited = unix_epoch_timestamp();
2716 self.update_post_context(id, user, y.context).await?;
2717
2718 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}