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 stack: get!(x->15(i64)) as usize,
116 topic: get!(x->16(i64)) as usize,
117 views: get!(x->17(i32)) as usize,
118 }
119 }
120
121 auto_method!(get_post_by_id()@get_post_from_row -> "SELECT * FROM posts WHERE id = $1" --name="post" --returns=Post --cache-key-tmpl="atto.post:{}");
122
123 pub async fn get_replies_by_post(
130 &self,
131 id: usize,
132 batch: usize,
133 page: usize,
134 sort: &str,
135 ) -> Result<Vec<Post>> {
136 let conn = match self.0.connect().await {
137 Ok(c) => c,
138 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
139 };
140
141 let res = query_rows!(
142 &conn,
143 &format!(
144 "SELECT * FROM posts WHERE replying_to = $1 ORDER BY created {sort} LIMIT $2 OFFSET $3"
145 ),
146 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
147 |x| { Self::get_post_from_row(x) }
148 );
149
150 if res.is_err() {
151 return Err(Error::GeneralNotFound("post".to_string()));
152 }
153
154 Ok(res.unwrap())
155 }
156
157 pub async fn get_post_reposting(
159 &self,
160 post: &Post,
161 ignore_users: &[usize],
162 user: &Option<User>,
163 ) -> (bool, Option<(User, Post)>) {
164 if let Some(ref repost) = post.context.repost {
165 if let Some(reposting) = repost.reposting {
166 let mut x = match self.get_post_by_id(reposting).await {
167 Ok(p) => p,
168 Err(_) => return (true, None),
169 };
170
171 if x.is_deleted {
172 return (!post.content.is_empty(), None);
173 }
174
175 if ignore_users.contains(&x.owner) {
176 return (!post.content.is_empty(), None);
177 }
178
179 let owner = match self.get_user_by_id(x.owner).await {
181 Ok(ua) => ua,
182 Err(_) => return (true, None),
183 };
184
185 if owner.settings.private_profile {
187 if let Some(ua) = user {
188 if owner.id != ua.id
189 && !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: &[FullPost]) -> Vec<FullPost> {
753 let mut out: Vec<FullPost> = Vec::new();
754
755 for mut post in posts.to_owned() {
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 &(data.stack as i64),
2223 &(data.topic as i64),
2224 &0_i32
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 {
2316 self.decr_post_comments(replying_to).await.unwrap();
2317 }
2318
2319 let owner = self.get_user_by_id(y.owner).await?;
2321
2322 if owner.post_count > 0 {
2323 self.decr_user_post_count(y.owner).await?;
2324 }
2325
2326 let community = self.get_community_by_id_no_void(y.community).await?;
2328
2329 if community.post_count > 0 {
2330 self.decr_community_post_count(y.community).await?;
2331 }
2332
2333 if y.context.answering != 0 {
2335 let question = self.get_question_by_id(y.context.answering).await?;
2336
2337 if question.is_global {
2338 self.decr_question_answer_count(y.context.answering).await?;
2339 }
2340 }
2341
2342 for upload in y.uploads {
2344 if let Err(e) = self.2.delete_upload(upload).await {
2345 return Err(Error::MiscError(e.to_string()));
2346 }
2347 }
2348
2349 if y.poll_id != 0 {
2351 self.delete_poll(y.poll_id, &user).await?;
2352 }
2353
2354 if y.context.answering != 0 {
2356 let question = self.get_question_by_id(y.context.answering).await?;
2357
2358 if !question.is_global {
2359 self.delete_question(question.id, &user).await?;
2360 }
2361 }
2362
2363 Ok(())
2365 }
2366
2367 pub async fn fake_delete_post(&self, id: usize, user: User, is_deleted: bool) -> Result<()> {
2368 let y = self.get_post_by_id(id).await?;
2369
2370 let user_membership = self
2371 .get_membership_by_owner_community(user.id, y.community)
2372 .await?;
2373
2374 if (user.id != y.owner)
2375 && !user_membership
2376 .role
2377 .check(CommunityPermission::MANAGE_POSTS)
2378 {
2379 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2380 return Err(Error::NotAllowed);
2381 } else {
2382 self.create_audit_log_entry(AuditLogEntry::new(
2383 user.id,
2384 format!("invoked `fake_delete_post` with x value `{id}`"),
2385 ))
2386 .await?
2387 }
2388 }
2389
2390 let conn = match self.0.connect().await {
2391 Ok(c) => c,
2392 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2393 };
2394
2395 let res = execute!(
2396 &conn,
2397 "UPDATE posts SET is_deleted = $1 WHERE id = $2",
2398 params![&if is_deleted { 1 } else { 0 }, &(id as i64)]
2399 );
2400
2401 if let Err(e) = res {
2402 return Err(Error::DatabaseError(e.to_string()));
2403 }
2404
2405 self.0.1.remove(format!("atto.post:{}", id)).await;
2406
2407 if is_deleted {
2408 if let Some(replying_to) = y.replying_to
2410 && replying_to != 0
2411 {
2412 self.decr_post_comments(replying_to).await.unwrap();
2413 }
2414
2415 let owner = self.get_user_by_id(y.owner).await?;
2417
2418 if owner.post_count > 0 {
2419 self.decr_user_post_count(y.owner).await?;
2420 }
2421
2422 let community = self.get_community_by_id_no_void(y.community).await?;
2424
2425 if community.post_count > 0 {
2426 self.decr_community_post_count(y.community).await?;
2427 }
2428
2429 if y.context.answering != 0 {
2431 let question = self.get_question_by_id(y.context.answering).await?;
2432
2433 if question.is_global {
2434 self.decr_question_answer_count(y.context.answering).await?;
2435 }
2436 }
2437
2438 for upload in y.uploads {
2440 if let Err(e) = self.2.delete_upload(upload).await {
2441 return Err(Error::MiscError(e.to_string()));
2442 }
2443 }
2444
2445 if y.context.answering != 0 {
2447 let question = self.get_question_by_id(y.context.answering).await?;
2448
2449 if !question.is_global {
2450 self.delete_question(question.id, &user).await?;
2451 }
2452 }
2453 } else {
2454 if let Some(replying_to) = y.replying_to {
2456 self.incr_post_comments(replying_to).await.unwrap();
2457 }
2458
2459 self.incr_user_post_count(y.owner).await?;
2461
2462 self.incr_community_post_count(y.community).await?;
2464
2465 if y.context.answering != 0 {
2467 let question = self.get_question_by_id(y.context.answering).await?;
2468
2469 if question.is_global {
2470 self.incr_question_answer_count(y.context.answering).await?;
2471 }
2472 }
2473
2474 }
2476
2477 Ok(())
2479 }
2480
2481 pub async fn update_post_context(
2482 &self,
2483 id: usize,
2484 user: User,
2485 mut x: PostContext,
2486 ) -> Result<()> {
2487 let y = self.get_post_by_id(id).await?;
2488 x.repost = y.context.repost; x.answering = y.context.answering; let user_membership = self
2492 .get_membership_by_owner_community(user.id, y.community)
2493 .await?;
2494
2495 if (user.id != y.owner)
2496 && !user_membership
2497 .role
2498 .check(CommunityPermission::MANAGE_POSTS)
2499 {
2500 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2501 return Err(Error::NotAllowed);
2502 } else {
2503 self.create_audit_log_entry(AuditLogEntry::new(
2504 user.id,
2505 format!("invoked `update_post_context` with x value `{id}`"),
2506 ))
2507 .await?
2508 }
2509 }
2510
2511 if x.is_pinned != y.context.is_pinned
2513 && !user_membership.role.check(CommunityPermission::MANAGE_PINS)
2514 {
2515 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2518 return Err(Error::NotAllowed);
2519 } else {
2520 self.create_audit_log_entry(AuditLogEntry::new(
2521 user.id,
2522 format!("invoked `update_post_context(pinned)` with x value `{id}`"),
2523 ))
2524 .await?
2525 }
2526 }
2527
2528 if (x.is_profile_pinned != y.context.is_profile_pinned) && (user.id != y.owner) {
2530 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2531 return Err(Error::NotAllowed);
2532 } else {
2533 self.create_audit_log_entry(AuditLogEntry::new(
2534 user.id,
2535 format!("invoked `update_post_context(profile_pinned)` with x value `{id}`"),
2536 ))
2537 .await?
2538 }
2539 }
2540
2541 if user.settings.auto_unlist {
2543 x.is_nsfw = true;
2544 }
2545
2546 if user.settings.auto_full_unlist {
2547 x.full_unlist = true;
2548 }
2549
2550 let conn = match self.0.connect().await {
2552 Ok(c) => c,
2553 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2554 };
2555
2556 let res = execute!(
2557 &conn,
2558 "UPDATE posts SET context = $1 WHERE id = $2",
2559 params![&serde_json::to_string(&x).unwrap(), &(id as i64)]
2560 );
2561
2562 if let Err(e) = res {
2563 return Err(Error::DatabaseError(e.to_string()));
2564 }
2565
2566 self.0.1.remove(format!("atto.post:{}", id)).await;
2567
2568 Ok(())
2570 }
2571
2572 pub async fn update_post_content(&self, id: usize, user: User, x: String) -> Result<()> {
2573 let mut y = self.get_post_by_id(id).await?;
2574
2575 if user.id != y.owner {
2576 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2577 return Err(Error::NotAllowed);
2578 } else {
2579 self.create_audit_log_entry(AuditLogEntry::new(
2580 user.id,
2581 format!("invoked `update_post_content` with x value `{id}`"),
2582 ))
2583 .await?
2584 }
2585 }
2586
2587 if x.len() < 2 {
2589 return Err(Error::DataTooShort("content".to_string()));
2590 } else if x.len() > 4096 {
2591 return Err(Error::DataTooLong("content".to_string()));
2592 }
2593
2594 let conn = match self.0.connect().await {
2596 Ok(c) => c,
2597 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2598 };
2599
2600 let res = execute!(
2601 &conn,
2602 "UPDATE posts SET content = $1 WHERE id = $2",
2603 params![&x, &(id as i64)]
2604 );
2605
2606 if let Err(e) = res {
2607 return Err(Error::DatabaseError(e.to_string()));
2608 }
2609
2610 y.context.edited = unix_epoch_timestamp();
2612 self.update_post_context(id, user, y.context).await?;
2613
2614 Ok(())
2616 }
2617
2618 pub async fn update_post_title(&self, id: usize, user: User, x: String) -> Result<()> {
2619 let mut y = self.get_post_by_id(id).await?;
2620
2621 if user.id != y.owner {
2622 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2623 return Err(Error::NotAllowed);
2624 } else {
2625 self.create_audit_log_entry(AuditLogEntry::new(
2626 user.id,
2627 format!("invoked `update_post_title` with x value `{id}`"),
2628 ))
2629 .await?
2630 }
2631 }
2632
2633 let community = self.get_community_by_id(y.community).await?;
2634
2635 if !community.context.enable_titles {
2636 return Err(Error::MiscError(
2637 "Community does not allow titles".to_string(),
2638 ));
2639 }
2640
2641 if x.len() < 2 && community.context.require_titles {
2642 return Err(Error::DataTooShort("title".to_string()));
2643 } else if x.len() > 128 {
2644 return Err(Error::DataTooLong("title".to_string()));
2645 }
2646
2647 let conn = match self.0.connect().await {
2649 Ok(c) => c,
2650 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2651 };
2652
2653 let res = execute!(
2654 &conn,
2655 "UPDATE posts SET title = $1 WHERE id = $2",
2656 params![&x, &(id as i64)]
2657 );
2658
2659 if let Err(e) = res {
2660 return Err(Error::DatabaseError(e.to_string()));
2661 }
2662
2663 y.context.edited = unix_epoch_timestamp();
2665 self.update_post_context(id, user, y.context).await?;
2666
2667 Ok(())
2669 }
2670
2671 auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2672 auto_method!(incr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2673 auto_method!(decr_post_likes() -> "UPDATE posts SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
2674 auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
2675
2676 auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2677 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);
2678
2679 auto_method!(incr_post_views() -> "UPDATE posts SET views = views + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2680 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);
2681}