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 }
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 && !ua.permissions.check(FinePermission::MANAGE_POSTS)
189 {
190 if self
191 .get_userfollow_by_initiator_receiver(owner.id, ua.id)
192 .await
193 .is_err()
194 {
195 return (!post.content.is_empty(), None);
197 }
198 }
199 } else {
200 return (!post.content.is_empty(), None);
202 }
203 }
204
205 x.mark_as_repost();
207 (
208 true,
209 Some((
210 match self.get_user_by_id(x.owner).await {
211 Ok(ua) => ua,
212 Err(_) => return (true, None),
213 },
214 x,
215 )),
216 )
217 } else {
218 (true, None)
219 }
220 } else {
221 (true, None)
222 }
223 }
224
225 pub async fn get_post_question(
227 &self,
228 post: &Post,
229 ignore_users: &[usize],
230 seen_questions: &mut HashMap<usize, FullQuestion>,
231 ) -> Result<Option<FullQuestion>> {
232 if post.context.answering != 0 {
233 if let Some(q) = seen_questions.get(&post.context.answering) {
234 return Ok(Some(q.to_owned()));
235 }
236
237 let question = self.get_question_by_id(post.context.answering).await?;
239
240 if ignore_users.contains(&question.owner) {
241 return Ok(None);
242 }
243
244 let user = if question.owner == 0 {
245 User::anonymous()
246 } else {
247 self.get_user_by_id_with_void(question.owner).await?
248 };
249
250 let asking_about = self.get_question_asking_about(&question).await?;
251 let full_question = (question, user, asking_about);
252
253 seen_questions.insert(post.context.answering, full_question.to_owned());
254 Ok(Some(full_question))
255 } else {
256 Ok(None)
257 }
258 }
259
260 pub async fn get_post_poll(
265 &self,
266 post: &Post,
267 user: &Option<User>,
268 ) -> Result<Option<(Poll, bool, bool)>> {
269 let user = if let Some(ua) = user {
270 ua
271 } else {
272 return Ok(None);
273 };
274
275 if post.poll_id != 0 {
276 Ok(Some(match self.get_poll_by_id(post.poll_id).await {
277 Ok(p) => {
278 let expired = unix_epoch_timestamp() - p.created > p.expires;
279 (
280 p,
281 self.get_pollvote_by_owner_poll(user.id, post.poll_id)
282 .await
283 .is_ok(),
284 expired,
285 )
286 }
287 Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())),
288 }))
289 } else {
290 Ok(None)
291 }
292 }
293
294 pub async fn get_post_stack(
299 &self,
300 seen_stacks: &mut HashMap<usize, UserStack>,
301 post: &Post,
302 as_user_id: usize,
303 ) -> (bool, Option<UserStack>) {
304 if post.stack != 0 {
305 if let Some(s) = seen_stacks.get(&post.stack) {
306 (
307 (s.owner == as_user_id) | s.users.contains(&as_user_id),
308 Some(s.to_owned()),
309 )
310 } else {
311 let s = match self.get_stack_by_id(post.stack).await {
312 Ok(s) => s,
313 Err(_) => return (true, None),
314 };
315
316 seen_stacks.insert(s.id, s.to_owned());
317 (
318 (s.owner == as_user_id) | s.users.contains(&as_user_id),
319 Some(s.to_owned()),
320 )
321 }
322 } else {
323 (true, None)
324 }
325 }
326
327 pub async fn fill_posts(
329 &self,
330 posts: Vec<Post>,
331 ignore_users: &[usize],
332 user: &Option<User>,
333 ) -> Result<
334 Vec<(
335 Post,
336 User,
337 Option<(User, Post)>,
338 Option<(Question, User, Option<(User, Post)>)>,
339 Option<(Poll, bool, bool)>,
340 Option<UserStack>,
341 )>,
342 > {
343 let mut out = Vec::new();
344
345 let mut users: HashMap<usize, User> = HashMap::new();
346 let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
347 let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
348 let mut seen_questions: HashMap<usize, FullQuestion> = HashMap::new();
349 let mut replying_posts: HashMap<usize, Post> = HashMap::new();
350
351 for post in posts {
352 if post.is_deleted {
353 continue;
354 }
355
356 let owner = post.owner;
357
358 if let Some(ua) = users.get(&owner) {
359 if ua.settings.require_account && user.is_none() {
361 continue;
362 }
363
364 let (can_view, stack) = self
366 .get_post_stack(
367 &mut seen_stacks,
368 &post,
369 if let Some(ua) = user { ua.id } else { 0 },
370 )
371 .await;
372
373 if !can_view {
374 continue;
375 }
376
377 let (can_view, reposting) =
379 self.get_post_reposting(&post, ignore_users, user).await;
380
381 if !can_view {
382 continue;
383 }
384
385 out.push((
387 post.clone(),
388 ua.clone(),
389 reposting,
390 self.get_post_question(&post, ignore_users, &mut seen_questions)
391 .await?,
392 self.get_post_poll(&post, user).await?,
393 stack,
394 ));
395 } else {
396 let ua = self.get_user_by_id(owner).await?;
397
398 if ua.settings.require_account && user.is_none() {
399 continue;
400 }
401
402 if (ua.permissions.check_banned()
403 | ignore_users.contains(&owner)
404 | ua.is_deactivated)
405 && !ua.permissions.check(FinePermission::MANAGE_POSTS)
406 {
407 continue;
408 }
409
410 if ua.settings.private_profile {
412 if let Some(ua1) = user {
415 if ua1.id == 0 {
416 continue;
417 }
418
419 if ua1.id != ua.id && !ua1.permissions.check(FinePermission::MANAGE_POSTS) {
420 if let Some(is_following) =
421 seen_user_follow_statuses.get(&(ua.id, ua1.id))
422 {
423 if !is_following && ua.id != ua1.id {
424 private_post_replying!(post, replying_posts, ua1, self);
425 }
426 } else {
427 if self
428 .get_userfollow_by_initiator_receiver(ua.id, ua1.id)
429 .await
430 .is_err()
431 && ua.id != ua1.id
432 {
433 seen_user_follow_statuses.insert((ua.id, ua1.id), false);
435 private_post_replying!(post, replying_posts, ua1, self);
436 }
437
438 seen_user_follow_statuses.insert((ua.id, ua1.id), true);
439 }
440 }
441 } else {
442 continue;
444 }
445 }
446
447 let (can_view, stack) = self
449 .get_post_stack(
450 &mut seen_stacks,
451 &post,
452 if let Some(ua) = user { ua.id } else { 0 },
453 )
454 .await;
455
456 if !can_view {
457 continue;
458 }
459
460 let (can_view, reposting) =
462 self.get_post_reposting(&post, ignore_users, user).await;
463
464 if !can_view {
465 continue;
466 }
467
468 users.insert(owner, ua.clone());
470 out.push((
471 post.clone(),
472 ua,
473 reposting,
474 self.get_post_question(&post, ignore_users, &mut seen_questions)
475 .await?,
476 self.get_post_poll(&post, user).await?,
477 stack,
478 ));
479 }
480 }
481
482 Ok(out)
483 }
484
485 pub async fn fill_posts_with_community(
487 &self,
488 posts: Vec<Post>,
489 user_id: usize,
490 ignore_users: &[usize],
491 user: &Option<User>,
492 ) -> Result<Vec<FullPost>> {
493 let mut out = Vec::new();
494
495 let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new();
496 let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
497 let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
498 let mut seen_questions: HashMap<usize, FullQuestion> = HashMap::new();
499 let mut replying_posts: HashMap<usize, Post> = HashMap::new();
500 let mut memberships: HashMap<usize, CommunityMembership> = HashMap::new();
501
502 for post in posts {
503 if post.is_deleted {
504 continue;
505 }
506
507 let owner = post.owner;
508 let community = post.community;
509
510 if let Some((ua, community)) = seen_before.get(&(owner, community)) {
511 if ua.settings.require_account && user.is_none() {
512 continue;
513 }
514
515 if community.read_access == CommunityReadAccess::Joined {
517 if let Some(user) = user {
518 if let Some(membership) = memberships.get(&community.id) {
519 if !membership.role.check(CommunityPermission::MEMBER) {
520 continue;
521 }
522 } else {
523 if let Ok(membership) = self
524 .get_membership_by_owner_community_no_void(user.id, community.id)
525 .await
526 {
527 if !membership.role.check(CommunityPermission::MEMBER) {
528 continue;
529 }
530 } else {
531 continue;
532 }
533 }
534 } else {
535 continue;
536 }
537 }
538
539 let (can_view, stack) = self
541 .get_post_stack(
542 &mut seen_stacks,
543 &post,
544 if let Some(ua) = user { ua.id } else { 0 },
545 )
546 .await;
547
548 if !can_view {
549 continue;
550 }
551
552 let (can_view, reposting) =
554 self.get_post_reposting(&post, ignore_users, user).await;
555
556 if !can_view {
557 continue;
558 }
559
560 out.push((
562 post.clone(),
563 ua.clone(),
564 community.to_owned(),
565 reposting,
566 self.get_post_question(&post, ignore_users, &mut seen_questions)
567 .await?,
568 self.get_post_poll(&post, user).await?,
569 stack,
570 ));
571 } else {
572 let ua = self.get_user_by_id(owner).await?;
573
574 if ua.settings.require_account && user.is_none() {
575 continue;
576 }
577
578 if ua.permissions.check_banned() | ignore_users.contains(&owner)
579 && !ua.permissions.check(FinePermission::MANAGE_POSTS)
580 {
581 continue;
582 }
583
584 let community = self.get_community_by_id(community).await?;
586 if community.read_access == CommunityReadAccess::Joined {
587 if let Some(user) = user {
588 if let Some(membership) = memberships.get(&community.id) {
589 if !membership.role.check(CommunityPermission::MEMBER) {
590 continue;
591 }
592 } else {
593 if let Ok(membership) = self
594 .get_membership_by_owner_community_no_void(user.id, community.id)
595 .await
596 {
597 memberships.insert(owner, membership.clone());
598 if !membership.role.check(CommunityPermission::MEMBER) {
599 continue;
600 }
601 } else {
602 continue;
603 }
604 }
605 } else {
606 continue;
607 }
608 }
609
610 if ua.settings.private_profile && ua.id != user_id {
612 if user_id == 0 {
613 continue;
614 }
615
616 if user_id != ua.id {
617 if let Some(is_following) = seen_user_follow_statuses.get(&(ua.id, user_id))
618 {
619 if !is_following {
620 private_post_replying!(post, replying_posts, id = user_id, self);
621 }
622 } else {
623 if self
624 .get_userfollow_by_initiator_receiver(ua.id, user_id)
625 .await
626 .is_err()
627 {
628 seen_user_follow_statuses.insert((ua.id, user_id), false);
630 private_post_replying!(post, replying_posts, id = user_id, self);
631 }
632
633 seen_user_follow_statuses.insert((ua.id, user_id), true);
634 }
635 }
636 }
637
638 let (can_view, stack) = self
640 .get_post_stack(
641 &mut seen_stacks,
642 &post,
643 if let Some(ua) = user { ua.id } else { 0 },
644 )
645 .await;
646
647 if !can_view {
648 continue;
649 }
650
651 let (can_view, reposting) =
653 self.get_post_reposting(&post, ignore_users, user).await;
654
655 if !can_view {
656 continue;
657 }
658
659 seen_before.insert((owner, community.id), (ua.clone(), community.clone()));
661 out.push((
662 post.clone(),
663 ua,
664 community,
665 reposting,
666 self.get_post_question(&post, ignore_users, &mut seen_questions)
667 .await?,
668 self.get_post_poll(&post, user).await?,
669 stack,
670 ));
671 }
672 }
673
674 Ok(out)
675 }
676
677 pub fn posts_muted_phrase_filter(
679 &self,
680 posts: &Vec<FullPost>,
681 muted: Option<&Vec<String>>,
682 ) -> Vec<FullPost> {
683 let muted = match muted {
686 Some(m) => m,
687 None => return posts.to_owned(),
688 };
689
690 let mut out: Vec<FullPost> = Vec::new();
691
692 for mut post in posts.clone() {
693 for phrase in muted {
694 if phrase.is_empty() {
695 continue;
696 }
697
698 if post
699 .0
700 .content
701 .to_lowercase()
702 .contains(&phrase.to_lowercase())
703 {
704 post.0.context.content_warning = "Contains muted phrase".to_string();
705 break;
706 }
707
708 if let Some(ref mut reposting) = post.3 {
709 if reposting
710 .1
711 .content
712 .to_lowercase()
713 .contains(&phrase.to_lowercase())
714 {
715 reposting.1.context.content_warning = "Contains muted phrase".to_string();
716 break;
717 }
718 }
719 }
720
721 out.push(post);
722 }
723
724 out
725 }
726
727 pub fn posts_owner_filter(&self, posts: &Vec<FullPost>) -> Vec<FullPost> {
729 let mut out: Vec<FullPost> = Vec::new();
730
731 for mut post in posts.clone() {
732 post.1.clean();
733
734 if let Some((ref mut x, _)) = post.3 {
736 x.clean();
737 }
738
739 if let Some((_, ref mut x, ref mut y)) = post.4 {
741 x.clean();
742
743 if y.is_some() {
744 y.as_mut().unwrap().0.clean();
745 }
746 }
747
748 out.push(post);
750 }
751
752 out
753 }
754
755 pub async fn get_posts_by_user(
762 &self,
763 id: usize,
764 batch: usize,
765 page: usize,
766 ) -> Result<Vec<Post>> {
767 let conn = match self.0.connect().await {
768 Ok(c) => c,
769 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
770 };
771
772 let res = query_rows!(
773 &conn,
774 "SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
775 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
776 |x| { Self::get_post_from_row(x) }
777 );
778
779 if res.is_err() {
780 return Err(Error::GeneralNotFound("post".to_string()));
781 }
782
783 Ok(res.unwrap())
784 }
785
786 pub async fn get_responses_by_user(
793 &self,
794 id: usize,
795 batch: usize,
796 page: usize,
797 ) -> Result<Vec<Post>> {
798 let conn = match self.0.connect().await {
799 Ok(c) => c,
800 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
801 };
802
803 let res = query_rows!(
804 &conn,
805 "SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 AND NOT context::jsonb->>'answering' = '0' ORDER BY created DESC LIMIT $2 OFFSET $3",
806 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
807 |x| { Self::get_post_from_row(x) }
808 );
809
810 if res.is_err() {
811 return Err(Error::GeneralNotFound("post".to_string()));
812 }
813
814 Ok(res.unwrap())
815 }
816
817 pub async fn get_replies_by_user(
824 &self,
825 id: usize,
826 batch: usize,
827 page: usize,
828 user: &Option<User>,
829 ) -> Result<Vec<Post>> {
830 let conn = match self.0.connect().await {
831 Ok(c) => c,
832 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
833 };
834
835 let mut hide_nsfw: bool = true;
837
838 if let Some(ua) = user {
839 hide_nsfw = !ua.settings.show_nsfw;
840 }
841
842 let res = query_rows!(
844 &conn,
845 &format!(
846 "SELECT * FROM posts WHERE owner = $1 AND NOT replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $2 OFFSET $3",
847 if hide_nsfw {
848 "AND NOT (context::json->>'is_nsfw')::boolean"
849 } else {
850 ""
851 }
852 ),
853 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
854 |x| { Self::get_post_from_row(x) }
855 );
856
857 if res.is_err() {
858 return Err(Error::GeneralNotFound("post".to_string()));
859 }
860
861 Ok(res.unwrap())
862 }
863
864 pub async fn get_media_posts_by_user(
871 &self,
872 id: usize,
873 batch: usize,
874 page: usize,
875 user: &Option<User>,
876 ) -> Result<Vec<Post>> {
877 let conn = match self.0.connect().await {
878 Ok(c) => c,
879 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
880 };
881
882 let mut hide_nsfw: bool = true;
884
885 if let Some(ua) = user {
886 hide_nsfw = !ua.settings.show_nsfw;
887 }
888
889 let res = query_rows!(
891 &conn,
892 &format!(
893 "SELECT * FROM posts WHERE owner = $1 AND NOT uploads = '[]' AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $2 OFFSET $3",
894 if hide_nsfw {
895 "AND NOT (context::json->>'is_nsfw')::boolean"
896 } else {
897 ""
898 }
899 ),
900 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
901 |x| { Self::get_post_from_row(x) }
902 );
903
904 if res.is_err() {
905 return Err(Error::GeneralNotFound("post".to_string()));
906 }
907
908 Ok(res.unwrap())
909 }
910
911 pub async fn get_posts_by_user_searched(
920 &self,
921 id: usize,
922 batch: usize,
923 page: usize,
924 text_query: &str,
925 user: &Option<&User>,
926 ) -> Result<Vec<Post>> {
927 let conn = match self.0.connect().await {
928 Ok(c) => c,
929 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
930 };
931
932 let mut hide_nsfw: bool = true;
934
935 if let Some(ua) = user {
936 hide_nsfw = !ua.settings.show_nsfw;
937 }
938
939 let res = query_rows!(
941 &conn,
942 &format!(
943 "SELECT * FROM posts WHERE owner = $1 AND tsvector_content @@ to_tsquery($2) {} AND is_deleted = 0 ORDER BY created DESC LIMIT $3 OFFSET $4",
944 if hide_nsfw {
945 "AND NOT (context::json->>'is_nsfw')::boolean"
946 } else {
947 ""
948 }
949 ),
950 params![
951 &(id as i64),
952 &text_query,
953 &(batch as i64),
954 &((page * batch) as i64)
955 ],
956 |x| { Self::get_post_from_row(x) }
957 );
958
959 if res.is_err() {
960 return Err(Error::GeneralNotFound("post".to_string()));
961 }
962
963 Ok(res.unwrap())
964 }
965
966 pub async fn get_posts_searched(
973 &self,
974 batch: usize,
975 page: usize,
976 text_query: &str,
977 ) -> Result<Vec<Post>> {
978 let conn = match self.0.connect().await {
979 Ok(c) => c,
980 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
981 };
982
983 let res = query_rows!(
985 &conn,
986 "SELECT * FROM posts WHERE tsvector_content @@ to_tsquery($1) AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
987 params![&text_query, &(batch as i64), &((page * batch) as i64)],
988 |x| { Self::get_post_from_row(x) }
989 );
990
991 if res.is_err() {
992 return Err(Error::GeneralNotFound("post".to_string()));
993 }
994
995 Ok(res.unwrap())
996 }
997
998 pub async fn get_posts_by_user_tag(
1006 &self,
1007 id: usize,
1008 tag: &str,
1009 batch: usize,
1010 page: usize,
1011 ) -> Result<Vec<Post>> {
1012 let conn = match self.0.connect().await {
1013 Ok(c) => c,
1014 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1015 };
1016
1017 let res = query_rows!(
1018 &conn,
1019 "SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 AND is_deleted = 0 ORDER BY created DESC LIMIT $3 OFFSET $4",
1020 params![
1021 &(id as i64),
1022 &format!("%\"{tag}\"%"),
1023 &(batch as i64),
1024 &((page * batch) as i64)
1025 ],
1026 |x| { Self::get_post_from_row(x) }
1027 );
1028
1029 if res.is_err() {
1030 return Err(Error::GeneralNotFound("post".to_string()));
1031 }
1032
1033 Ok(res.unwrap())
1034 }
1035
1036 pub async fn get_responses_by_user_tag(
1045 &self,
1046 id: usize,
1047 tag: &str,
1048 batch: usize,
1049 page: usize,
1050 ) -> Result<Vec<Post>> {
1051 let conn = match self.0.connect().await {
1052 Ok(c) => c,
1053 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1054 };
1055
1056 let res = query_rows!(
1057 &conn,
1058 "SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 AND is_deleted = 0 AND NOT context::jsonb->>'answering' = '0' ORDER BY created DESC LIMIT $3 OFFSET $4",
1059 params![
1060 &(id as i64),
1061 &format!("%\"{tag}\"%"),
1062 &(batch as i64),
1063 &((page * batch) as i64)
1064 ],
1065 |x| { Self::get_post_from_row(x) }
1066 );
1067
1068 if res.is_err() {
1069 return Err(Error::GeneralNotFound("post".to_string()));
1070 }
1071
1072 Ok(res.unwrap())
1073 }
1074
1075 pub async fn get_posts_by_community(
1082 &self,
1083 id: usize,
1084 batch: usize,
1085 page: usize,
1086 user: &Option<User>,
1087 ) -> Result<Vec<Post>> {
1088 let conn = match self.0.connect().await {
1089 Ok(c) => c,
1090 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1091 };
1092
1093 let mut hide_nsfw: bool = true;
1095
1096 if let Some(ua) = user {
1097 hide_nsfw = !ua.settings.show_nsfw;
1098 }
1099
1100 let res = query_rows!(
1102 &conn,
1103 &format!(
1104 "SELECT * FROM posts WHERE community = $1 AND replying_to = 0 AND NOT context LIKE '%\"is_pinned\":true%' AND is_deleted = 0 {} ORDER BY created DESC LIMIT $2 OFFSET $3",
1105 if hide_nsfw {
1106 "AND NOT (context::json->>'is_nsfw')::boolean"
1107 } else {
1108 ""
1109 }
1110 ),
1111 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
1112 |x| { Self::get_post_from_row(x) }
1113 );
1114
1115 if res.is_err() {
1116 return Err(Error::GeneralNotFound("post".to_string()));
1117 }
1118
1119 Ok(res.unwrap())
1120 }
1121
1122 pub async fn get_posts_by_community_topic(
1130 &self,
1131 id: usize,
1132 topic: usize,
1133 batch: usize,
1134 page: usize,
1135 user: &Option<User>,
1136 ) -> Result<Vec<Post>> {
1137 let conn = match self.0.connect().await {
1138 Ok(c) => c,
1139 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1140 };
1141
1142 let mut hide_nsfw: bool = true;
1144
1145 if let Some(ua) = user {
1146 hide_nsfw = !ua.settings.show_nsfw;
1147 }
1148
1149 let res = query_rows!(
1151 &conn,
1152 &format!(
1153 "SELECT * FROM posts WHERE community = $1 AND topic = $2 AND replying_to = 0 AND NOT context LIKE '%\"is_pinned\":true%' AND is_deleted = 0 {} ORDER BY created DESC LIMIT $3 OFFSET $4",
1154 if hide_nsfw {
1155 "AND NOT (context::json->>'is_nsfw')::boolean"
1156 } else {
1157 ""
1158 }
1159 ),
1160 &[
1161 &(id as i64),
1162 &(topic as i64),
1163 &(batch as i64),
1164 &((page * batch) as i64)
1165 ],
1166 |x| { Self::get_post_from_row(x) }
1167 );
1168
1169 if res.is_err() {
1170 return Err(Error::GeneralNotFound("post".to_string()));
1171 }
1172
1173 Ok(res.unwrap())
1174 }
1175
1176 pub async fn get_posts_by_stack(
1183 &self,
1184 id: usize,
1185 batch: usize,
1186 page: usize,
1187 ) -> Result<Vec<Post>> {
1188 let conn = match self.0.connect().await {
1189 Ok(c) => c,
1190 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1191 };
1192
1193 let res = query_rows!(
1194 &conn,
1195 "SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
1196 &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
1197 |x| { Self::get_post_from_row(x) }
1198 );
1199
1200 if res.is_err() {
1201 return Err(Error::GeneralNotFound("post".to_string()));
1202 }
1203
1204 Ok(res.unwrap())
1205 }
1206
1207 pub async fn get_pinned_posts_by_community(&self, id: usize) -> Result<Vec<Post>> {
1212 let conn = match self.0.connect().await {
1213 Ok(c) => c,
1214 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1215 };
1216
1217 let res = query_rows!(
1218 &conn,
1219 "SELECT * FROM posts WHERE community = $1 AND context LIKE '%\"is_pinned\":true%' ORDER BY created DESC",
1220 &[&(id as i64),],
1221 |x| { Self::get_post_from_row(x) }
1222 );
1223
1224 if res.is_err() {
1225 return Err(Error::GeneralNotFound("post".to_string()));
1226 }
1227
1228 Ok(res.unwrap())
1229 }
1230
1231 pub async fn get_pinned_posts_by_community_topic(
1237 &self,
1238 id: usize,
1239 topic: usize,
1240 ) -> Result<Vec<Post>> {
1241 let conn = match self.0.connect().await {
1242 Ok(c) => c,
1243 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1244 };
1245
1246 let res = query_rows!(
1247 &conn,
1248 "SELECT * FROM posts WHERE community = $1 AND topic = $2 AND context LIKE '%\"is_pinned\":true%' ORDER BY created DESC",
1249 &[&(id as i64), &(topic as i64)],
1250 |x| { Self::get_post_from_row(x) }
1251 );
1252
1253 if res.is_err() {
1254 return Err(Error::GeneralNotFound("post".to_string()));
1255 }
1256
1257 Ok(res.unwrap())
1258 }
1259
1260 pub async fn get_pinned_posts_by_user(&self, id: usize) -> Result<Vec<Post>> {
1265 let conn = match self.0.connect().await {
1266 Ok(c) => c,
1267 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1268 };
1269
1270 let res = query_rows!(
1271 &conn,
1272 "SELECT * FROM posts WHERE owner = $1 AND context LIKE '%\"is_profile_pinned\":true%' ORDER BY created DESC",
1273 &[&(id as i64),],
1274 |x| { Self::get_post_from_row(x) }
1275 );
1276
1277 if res.is_err() {
1278 return Err(Error::GeneralNotFound("post".to_string()));
1279 }
1280
1281 Ok(res.unwrap())
1282 }
1283
1284 pub async fn get_posts_by_question(
1291 &self,
1292 id: usize,
1293 batch: usize,
1294 page: usize,
1295 ) -> Result<Vec<Post>> {
1296 let conn = match self.0.connect().await {
1297 Ok(c) => c,
1298 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1299 };
1300
1301 let res = query_rows!(
1302 &conn,
1303 "SELECT * FROM posts WHERE context LIKE $1 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
1304 params![
1305 &format!("%\"answering\":{id}%"),
1306 &(batch as i64),
1307 &((page * batch) as i64)
1308 ],
1309 |x| { Self::get_post_from_row(x) }
1310 );
1311
1312 if res.is_err() {
1313 return Err(Error::GeneralNotFound("post".to_string()));
1314 }
1315
1316 Ok(res.unwrap())
1317 }
1318
1319 pub async fn get_post_by_owner_question(&self, owner: usize, question: usize) -> Result<Post> {
1325 let conn = match self.0.connect().await {
1326 Ok(c) => c,
1327 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1328 };
1329
1330 let res = query_row!(
1331 &conn,
1332 "SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 AND is_deleted = 0 LIMIT 1",
1333 params![&format!("%\"answering\":{question}%"), &(owner as i64),],
1334 |x| { Ok(Self::get_post_from_row(x)) }
1335 );
1336
1337 if res.is_err() {
1338 return Err(Error::GeneralNotFound("post".to_string()));
1339 }
1340
1341 Ok(res.unwrap())
1342 }
1343
1344 pub async fn get_quoting_posts_by_quoting(
1354 &self,
1355 id: usize,
1356 batch: usize,
1357 page: usize,
1358 ) -> Result<Vec<Post>> {
1359 let conn = match self.0.connect().await {
1360 Ok(c) => c,
1361 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1362 };
1363
1364 let res = query_rows!(
1365 &conn,
1366 "SELECT * FROM posts WHERE NOT content = '' AND context LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
1367 params![
1368 &format!("%\"reposting\":{id}%"),
1369 &(batch as i64),
1370 &((page * batch) as i64)
1371 ],
1372 |x| { Self::get_post_from_row(x) }
1373 );
1374
1375 if res.is_err() {
1376 return Err(Error::GeneralNotFound("post".to_string()));
1377 }
1378
1379 Ok(res.unwrap())
1380 }
1381
1382 pub async fn get_reposts_by_quoting(
1392 &self,
1393 id: usize,
1394 batch: usize,
1395 page: usize,
1396 ) -> Result<Vec<Post>> {
1397 let conn = match self.0.connect().await {
1398 Ok(c) => c,
1399 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1400 };
1401
1402 let res = query_rows!(
1403 &conn,
1404 "SELECT * FROM posts WHERE content = '' AND context LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
1405 params![
1406 &format!("%\"reposting\":{id}%"),
1407 &(batch as i64),
1408 &((page * batch) as i64)
1409 ],
1410 |x| { Self::get_post_from_row(x) }
1411 );
1412
1413 if res.is_err() {
1414 return Err(Error::GeneralNotFound("post".to_string()));
1415 }
1416
1417 Ok(res.unwrap())
1418 }
1419
1420 pub async fn get_popular_posts(
1427 &self,
1428 batch: usize,
1429 page: usize,
1430 cutoff: usize,
1431 ) -> Result<Vec<Post>> {
1432 let conn = match self.0.connect().await {
1433 Ok(c) => c,
1434 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1435 };
1436
1437 let res = query_rows!(
1438 &conn,
1439 "SELECT * FROM posts WHERE replying_to = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2 ORDER BY likes - dislikes DESC, created ASC LIMIT $3 OFFSET $4",
1440 &[
1441 &(unix_epoch_timestamp() as i64),
1442 &(cutoff as i64),
1443 &(batch as i64),
1444 &((page * batch) as i64)
1445 ],
1446 |x| { Self::get_post_from_row(x) }
1447 );
1448
1449 if res.is_err() {
1450 return Err(Error::GeneralNotFound("post".to_string()));
1451 }
1452
1453 Ok(res.unwrap())
1454 }
1455
1456 pub async fn get_latest_posts(
1462 &self,
1463 batch: usize,
1464 page: usize,
1465 as_user: &Option<User>,
1466 before_time: usize,
1467 ) -> Result<Vec<Post>> {
1468 let hide_answers: bool = if let Some(user) = as_user {
1469 user.settings.all_timeline_hide_answers
1470 } else {
1471 false
1472 };
1473
1474 let mut hide_nsfw: bool = true;
1476
1477 if let Some(ua) = as_user {
1478 hide_nsfw = !ua.settings.show_nsfw;
1479 }
1480
1481 let conn = match self.0.connect().await {
1483 Ok(c) => c,
1484 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1485 };
1486
1487 let res = query_rows!(
1488 &conn,
1489 &format!(
1490 "SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1491 if before_time > 0 {
1492 format!(" AND created < {before_time}")
1493 } else {
1494 String::new()
1495 },
1496 if hide_nsfw {
1497 " AND NOT context LIKE '%\"is_nsfw\":true%'"
1498 } else {
1499 ""
1500 },
1501 if hide_answers {
1502 " AND context::jsonb->>'answering' = '0'"
1503 } else {
1504 ""
1505 }
1506 ),
1507 &[&(batch as i64), &((page * batch) as i64)],
1508 |x| { Self::get_post_from_row(x) }
1509 );
1510
1511 if res.is_err() {
1512 return Err(Error::GeneralNotFound("post".to_string()));
1513 }
1514
1515 Ok(res.unwrap())
1516 }
1517
1518 pub async fn get_latest_forum_posts(
1524 &self,
1525 batch: usize,
1526 page: usize,
1527 as_user: &Option<User>,
1528 before_time: usize,
1529 ) -> Result<Vec<Post>> {
1530 let mut hide_nsfw: bool = true;
1532
1533 if let Some(ua) = as_user {
1534 hide_nsfw = !ua.settings.show_nsfw;
1535 }
1536
1537 let conn = match self.0.connect().await {
1539 Ok(c) => c,
1540 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1541 };
1542
1543 let res = query_rows!(
1544 &conn,
1545 &format!(
1546 "SELECT * FROM posts WHERE replying_to = 0{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1547 if before_time > 0 {
1548 format!(" AND created < {before_time}")
1549 } else {
1550 String::new()
1551 },
1552 if hide_nsfw {
1553 " AND NOT context LIKE '%\"is_nsfw\":true%'"
1554 } else {
1555 ""
1556 }
1557 ),
1558 &[&(batch as i64), &((page * batch) as i64)],
1559 |x| { Self::get_post_from_row(x) }
1560 );
1561
1562 if res.is_err() {
1563 return Err(Error::GeneralNotFound("post".to_string()));
1564 }
1565
1566 Ok(res.unwrap())
1567 }
1568
1569 pub async fn get_posts_from_user_communities(
1576 &self,
1577 id: usize,
1578 batch: usize,
1579 page: usize,
1580 user: &User,
1581 ) -> Result<Vec<Post>> {
1582 let memberships = self.get_memberships_by_owner(id).await?;
1583 let mut memberships = memberships.iter();
1584 let first = match memberships.next() {
1585 Some(f) => f,
1586 None => return Ok(Vec::new()),
1587 };
1588
1589 let mut query_string: String = String::new();
1590
1591 for membership in memberships {
1592 query_string.push_str(&format!(" OR community = {}", membership.community));
1593 }
1594
1595 let hide_nsfw: bool = !user.settings.show_nsfw;
1597
1598 let conn = match self.0.connect().await {
1600 Ok(c) => c,
1601 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1602 };
1603
1604 let res = query_rows!(
1605 &conn,
1606 &format!(
1607 "SELECT * FROM posts WHERE (community = {} {query_string}){} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1608 first.community,
1609 if hide_nsfw {
1610 " AND NOT context LIKE '%\"is_nsfw\":true%'"
1611 } else {
1612 ""
1613 },
1614 ),
1615 &[&(batch as i64), &((page * batch) as i64)],
1616 |x| { Self::get_post_from_row(x) }
1617 );
1618
1619 if res.is_err() {
1620 return Err(Error::GeneralNotFound("post".to_string()));
1621 }
1622
1623 Ok(res.unwrap())
1624 }
1625
1626 pub async fn get_posts_from_user_following(
1633 &self,
1634 id: usize,
1635 batch: usize,
1636 page: usize,
1637 ) -> Result<Vec<Post>> {
1638 let following = self.get_userfollows_by_initiator_all(id).await?;
1639 let mut following = following.iter();
1640 let first = match following.next() {
1641 Some(f) => f,
1642 None => return Ok(Vec::new()),
1643 };
1644
1645 let mut query_string: String = String::new();
1646
1647 for user in following {
1648 query_string.push_str(&format!(" OR owner = {}", user.receiver));
1649 }
1650
1651 let conn = match self.0.connect().await {
1653 Ok(c) => c,
1654 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1655 };
1656
1657 let res = query_rows!(
1658 &conn,
1659 &format!(
1660 "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
1661 first.receiver
1662 ),
1663 &[&(batch as i64), &((page * batch) as i64)],
1664 |x| { Self::get_post_from_row(x) }
1665 );
1666
1667 if res.is_err() {
1668 return Err(Error::GeneralNotFound("post".to_string()));
1669 }
1670
1671 Ok(res.unwrap())
1672 }
1673
1674 pub async fn get_posts_from_stack(
1681 &self,
1682 id: usize,
1683 batch: usize,
1684 page: usize,
1685 sort: StackSort,
1686 ) -> Result<Vec<Post>> {
1687 let users = self.get_stack_by_id(id).await?.users;
1688 let mut users = users.iter();
1689
1690 let first = match users.next() {
1691 Some(f) => f,
1692 None => return Ok(Vec::new()),
1693 };
1694
1695 let mut query_string: String = String::new();
1696
1697 for user in users {
1698 query_string.push_str(&format!(" OR owner = {}", user));
1699 }
1700
1701 let conn = match self.0.connect().await {
1703 Ok(c) => c,
1704 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1705 };
1706
1707 let res = query_rows!(
1708 &conn,
1709 &format!(
1710 "SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY {} DESC LIMIT $1 OFFSET $2",
1711 first,
1712 if sort == StackSort::Created {
1713 "created"
1714 } else {
1715 "likes"
1716 }
1717 ),
1718 &[&(batch as i64), &((page * batch) as i64)],
1719 |x| { Self::get_post_from_row(x) }
1720 );
1721
1722 if res.is_err() {
1723 return Err(Error::GeneralNotFound("post".to_string()));
1724 }
1725
1726 Ok(res.unwrap())
1727 }
1728
1729 pub async fn check_can_post(&self, community: &Community, uid: usize) -> bool {
1731 match community.write_access {
1732 CommunityWriteAccess::Owner => uid == community.owner,
1733 CommunityWriteAccess::Joined => {
1734 match self
1735 .get_membership_by_owner_community(uid, community.id)
1736 .await
1737 {
1738 Ok(m) => m.role.check_member(),
1739 Err(_) => false,
1740 }
1741 }
1742 _ => true,
1743 }
1744 }
1745
1746 pub async fn check_can_post_with_access(
1748 &self,
1749 community: &Community,
1750 access: &CommunityWriteAccess,
1751 uid: usize,
1752 ) -> bool {
1753 match *access {
1754 CommunityWriteAccess::Owner => uid == community.owner,
1755 CommunityWriteAccess::Joined => {
1756 match self
1757 .get_membership_by_owner_community(uid, community.id)
1758 .await
1759 {
1760 Ok(m) => m.role.check_member(),
1761 Err(_) => false,
1762 }
1763 }
1764 _ => true,
1765 }
1766 }
1767
1768 pub async fn create_post(&self, mut data: Post) -> Result<usize> {
1773 for ban in &self.0.0.banned_data {
1775 match ban {
1776 StringBan::String(x) => {
1777 if data.content.contains(x) {
1778 return Ok(0);
1779 }
1780 }
1781 StringBan::Unicode(x) => {
1782 if data.content.contains(&match char::from_u32(x.to_owned()) {
1783 Some(c) => c.to_string(),
1784 None => continue,
1785 }) {
1786 return Ok(0);
1787 }
1788 }
1789 }
1790 }
1791
1792 if data.stack != 0 {
1794 let stack = self.get_stack_by_id(data.stack).await?;
1795
1796 if stack.mode != StackMode::Circle {
1797 return Err(Error::MiscError(
1798 "You must use a \"Circle\" stack for this".to_string(),
1799 ));
1800 }
1801
1802 if stack.owner != data.owner && !stack.users.contains(&data.owner) {
1803 return Err(Error::NotAllowed);
1804 }
1805 }
1806
1807 let community = if data.stack != 0 {
1809 data.community = self.0.0.town_square;
1811 self.get_community_by_id(self.0.0.town_square).await?
1812 } else {
1813 self.get_community_by_id(data.community).await?
1815 };
1816
1817 if community.is_forum {
1819 if data.topic == 0 {
1820 return Err(Error::MiscError(
1821 "Topic is required for this community".to_string(),
1822 ));
1823 }
1824
1825 if let Some(topic) = community.topics.get(&data.topic) {
1826 if !self
1828 .check_can_post_with_access(&community, &topic.write_access, data.owner)
1829 .await
1830 {
1831 return Err(Error::NotAllowed);
1832 }
1833 } else {
1834 return Err(Error::GeneralNotFound("topic".to_string()));
1835 }
1836 } else if data.topic != 0 {
1837 return Err(Error::DoesNotSupportField("Community".to_string()));
1838 }
1839
1840 let mut owner = self.get_user_by_id(data.owner).await?;
1842
1843 let is_reposting = if let Some(ref repost) = data.context.repost {
1845 repost.reposting.is_some()
1846 } else {
1847 false
1848 };
1849
1850 if !is_reposting {
1851 if data.content.len() < 2 && data.uploads.len() == 0 {
1852 return Err(Error::DataTooShort("content".to_string()));
1853 } else if data.content.len() > 4096 {
1854 return Err(Error::DataTooLong("content".to_string()));
1855 }
1856
1857 if !community.context.enable_titles {
1859 if !data.title.is_empty() {
1860 return Err(Error::MiscError(
1861 "Community does not allow titles".to_string(),
1862 ));
1863 }
1864 } else if data.replying_to.is_none() {
1865 if data.title.trim().len() < 2 && community.context.require_titles {
1866 return Err(Error::DataTooShort("title".to_string()));
1867 } else if data.title.len() > 128 {
1868 return Err(Error::DataTooLong("title".to_string()));
1869 }
1870
1871 self.add_achievement(
1873 &mut owner,
1874 AchievementName::CreatePostWithTitle.into(),
1875 true,
1876 )
1877 .await?;
1878 }
1879 }
1880
1881 if !self.check_can_post(&community, data.owner).await {
1883 return Err(Error::NotAllowed);
1884 }
1885
1886 data.context.is_nsfw = community.context.is_nsfw;
1888
1889 if data.context.answering != 0 {
1891 let question = self.get_question_by_id(data.context.answering).await?;
1892
1893 if self
1895 .get_post_by_owner_question(owner.id, question.id)
1896 .await
1897 .is_ok()
1898 {
1899 return Err(Error::MiscError(
1900 "You've already answered this question".to_string(),
1901 ));
1902 }
1903
1904 if !question.is_global {
1905 self.delete_request(question.id, question.id, &owner, false)
1906 .await?;
1907 } else {
1908 self.incr_question_answer_count(data.context.answering)
1909 .await?;
1910 }
1911
1912 if (question.owner != data.owner)
1915 && (question.owner != 0)
1916 && (!owner.settings.private_profile
1917 | self
1918 .get_userfollow_by_initiator_receiver(data.owner, question.owner)
1919 .await
1920 .is_ok())
1921 {
1922 self.create_notification(Notification::new(
1923 "Your question has received a new answer!".to_string(),
1924 format!(
1925 "[@{}](/api/v1/auth/user/find/{}) has answered your [question](/question/{}).",
1926 owner.username, owner.id, question.id
1927 ),
1928 question.owner,
1929 ))
1930 .await?;
1931 }
1932
1933 if question.context.is_nsfw {
1935 data.context.is_nsfw = question.context.is_nsfw;
1936 }
1937 }
1938
1939 let reposting = if let Some(ref repost) = data.context.repost {
1941 if let Some(id) = repost.reposting {
1942 Some(self.get_post_by_id(id).await?)
1943 } else {
1944 None
1945 }
1946 } else {
1947 None
1948 };
1949
1950 if let Some(ref rt) = reposting {
1951 if rt.stack != data.stack && rt.stack != 0 {
1952 return Err(Error::MiscError("Cannot repost out of stack".to_string()));
1953 }
1954
1955 if data.content.is_empty() {
1956 data.context.reposts_enabled = false;
1958 data.context.reactions_enabled = false;
1959 data.context.comments_enabled = false;
1960 }
1961
1962 if rt.context.is_nsfw {
1964 data.context.is_nsfw = true;
1965 }
1966
1967 if !rt.context.reposts_enabled {
1978 return Err(Error::MiscError("Post has reposts disabled".to_string()));
1979 }
1980
1981 if self
1983 .get_userblock_by_initiator_receiver(rt.owner, data.owner)
1984 .await
1985 .is_ok()
1986 | self
1987 .get_user_stack_blocked_users(rt.owner)
1988 .await
1989 .contains(&data.owner)
1990 {
1991 return Err(Error::NotAllowed);
1992 }
1993
1994 if owner.id != rt.owner && !owner.settings.private_profile && data.stack == 0 {
1997 self.create_notification(
1998 Notification::new(
1999 format!(
2000 "[@{}](/api/v1/auth/user/find/{}) has [quoted](/post/{}) your [post](/post/{})",
2001 owner.username,
2002 owner.id,
2003 data.id,
2004 rt.id
2005 ),
2006 if data.content.is_empty() {
2007 String::new()
2008 } else {
2009 format!("\"{}\"", data.content)
2010 },
2011 rt.owner
2012 )
2013 )
2014 .await?;
2015 }
2016
2017 self.add_achievement(&mut owner, AchievementName::CreateRepost.into(), true)
2019 .await?;
2020 }
2021
2022 let replying_to = if let Some(id) = data.replying_to {
2024 Some(self.get_post_by_id(id).await?)
2025 } else {
2026 None
2027 };
2028
2029 if let Some(ref rt) = replying_to {
2030 if !rt.context.comments_enabled {
2031 return Err(Error::MiscError("Post has comments disabled".to_string()));
2032 }
2033
2034 if self
2036 .get_userblock_by_initiator_receiver(rt.owner, data.owner)
2037 .await
2038 .is_ok()
2039 | self
2040 .get_user_stack_blocked_users(rt.owner)
2041 .await
2042 .contains(&data.owner)
2043 {
2044 return Err(Error::NotAllowed);
2045 }
2046 }
2047
2048 let mut already_notified: HashMap<String, User> = HashMap::new();
2050 for username in User::parse_mentions(&data.content) {
2051 let user = {
2052 if let Some(ua) = already_notified.get(&username) {
2053 ua.to_owned()
2054 } else {
2055 let user = self.get_user_by_username(&username).await?;
2056
2057 if self
2059 .get_userblock_by_initiator_receiver(user.id, data.owner)
2060 .await
2061 .is_ok()
2062 | self
2063 .get_user_stack_blocked_users(user.id)
2064 .await
2065 .contains(&data.owner)
2066 {
2067 return Err(Error::NotAllowed);
2068 }
2069
2070 if user.settings.private_profile {
2072 if self
2073 .get_userfollow_by_initiator_receiver(user.id, data.owner)
2074 .await
2075 .is_err()
2076 {
2077 return Err(Error::NotAllowed);
2078 }
2079 }
2080
2081 self.create_notification(Notification::new(
2083 "You've been mentioned in a post!".to_string(),
2084 format!(
2085 "[@{}](/api/v1/auth/user/find/{}) has mentioned you in their [post](/post/{}).",
2086 owner.username, owner.id, data.id
2087 ),
2088 user.id,
2089 ))
2090 .await?;
2091
2092 already_notified.insert(username.to_owned(), user.clone());
2094 user
2095 }
2096 };
2097
2098 data.content = data.content.replace(
2099 &format!("@{username}"),
2100 &format!("[@{username}](/api/v1/auth/user/find/{})", user.id),
2101 );
2102 }
2103
2104 if owner.settings.auto_unlist {
2106 data.context.is_nsfw = true;
2107 }
2108
2109 if owner.settings.auto_full_unlist {
2110 data.context.full_unlist = true;
2111 }
2112
2113 let conn = match self.0.connect().await {
2115 Ok(c) => c,
2116 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2117 };
2118
2119 let replying_to_id = data.replying_to.unwrap_or(0).to_string();
2120
2121 let res = execute!(
2122 &conn,
2123 "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13, $14, $15, $16, $17)",
2124 params![
2125 &(data.id as i64),
2126 &(data.created as i64),
2127 &data.content,
2128 &(data.owner as i64),
2129 &(data.community as i64),
2130 &serde_json::to_string(&data.context).unwrap(),
2131 &if replying_to_id != "0" {
2132 replying_to_id.parse::<i64>().unwrap()
2133 } else {
2134 0_i64
2135 },
2136 &0_i32,
2137 &0_i32,
2138 &0_i32,
2139 &serde_json::to_string(&data.uploads).unwrap(),
2140 &{ if data.is_deleted { 1 } else { 0 } },
2141 &(data.poll_id as i64),
2142 &data.title,
2143 &{ if data.is_open { 1 } else { 0 } },
2144 &(data.stack as i64),
2145 &(data.topic as i64),
2146 ]
2147 );
2148
2149 if let Err(e) = res {
2150 return Err(Error::DatabaseError(e.to_string()));
2151 }
2152
2153 if let Some(rt) = replying_to {
2155 self.incr_post_comments(rt.id).await.unwrap();
2156
2157 if data.owner != rt.owner {
2159 let owner = self.get_user_by_id(data.owner).await?;
2160
2161 if !owner.settings.private_profile
2165 | self
2166 .get_userfollow_by_initiator_receiver(data.owner, rt.owner)
2167 .await
2168 .is_ok()
2169 {
2170 self.create_notification(Notification::new(
2171 "Your post has received a new comment!".to_string(),
2172 format!(
2173 "[@{}](/api/v1/auth/user/find/{}) has commented on your [post](/post/{}).",
2174 owner.username, owner.id, rt.id
2175 ),
2176 rt.owner,
2177 ))
2178 .await?;
2179 }
2180
2181 if !rt.context.comments_enabled {
2182 return Err(Error::NotAllowed);
2183 }
2184 }
2185 }
2186
2187 self.incr_user_post_count(data.owner).await?;
2189
2190 self.incr_community_post_count(data.community).await?;
2192
2193 Ok(data.id)
2195 }
2196
2197 pub async fn delete_post(&self, id: usize, user: User) -> Result<()> {
2198 let y = self.get_post_by_id(id).await?;
2199
2200 let user_membership = self
2201 .get_membership_by_owner_community(user.id, y.community)
2202 .await?;
2203
2204 if (user.id != y.owner)
2205 && !user_membership
2206 .role
2207 .check(CommunityPermission::MANAGE_POSTS)
2208 {
2209 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2210 return Err(Error::NotAllowed);
2211 } else {
2212 self.create_audit_log_entry(AuditLogEntry::new(
2213 user.id,
2214 format!("invoked `delete_post` with x value `{id}`"),
2215 ))
2216 .await?
2217 }
2218 }
2219
2220 let conn = match self.0.connect().await {
2221 Ok(c) => c,
2222 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2223 };
2224
2225 let res = execute!(&conn, "DELETE FROM posts WHERE id = $1", &[&(id as i64)]);
2226
2227 if let Err(e) = res {
2228 return Err(Error::DatabaseError(e.to_string()));
2229 }
2230
2231 self.0.1.remove(format!("atto.post:{}", id)).await;
2232
2233 if let Some(replying_to) = y.replying_to {
2235 if replying_to != 0 {
2236 self.decr_post_comments(replying_to).await.unwrap();
2237 }
2238 }
2239
2240 let owner = self.get_user_by_id(y.owner).await?;
2242
2243 if owner.post_count > 0 {
2244 self.decr_user_post_count(y.owner).await?;
2245 }
2246
2247 let community = self.get_community_by_id_no_void(y.community).await?;
2249
2250 if community.post_count > 0 {
2251 self.decr_community_post_count(y.community).await?;
2252 }
2253
2254 if y.context.answering != 0 {
2256 let question = self.get_question_by_id(y.context.answering).await?;
2257
2258 if question.is_global {
2259 self.decr_question_answer_count(y.context.answering).await?;
2260 }
2261 }
2262
2263 for upload in y.uploads {
2265 self.delete_upload(upload).await?;
2266 }
2267
2268 if y.poll_id != 0 {
2270 self.delete_poll(y.poll_id, &user).await?;
2271 }
2272
2273 if y.context.answering != 0 {
2275 let question = self.get_question_by_id(y.context.answering).await?;
2276
2277 if !question.is_global {
2278 self.delete_question(question.id, &user).await?;
2279 }
2280 }
2281
2282 Ok(())
2284 }
2285
2286 pub async fn fake_delete_post(&self, id: usize, user: User, is_deleted: bool) -> Result<()> {
2287 let y = self.get_post_by_id(id).await?;
2288
2289 let user_membership = self
2290 .get_membership_by_owner_community(user.id, y.community)
2291 .await?;
2292
2293 if (user.id != y.owner)
2294 && !user_membership
2295 .role
2296 .check(CommunityPermission::MANAGE_POSTS)
2297 {
2298 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2299 return Err(Error::NotAllowed);
2300 } else {
2301 self.create_audit_log_entry(AuditLogEntry::new(
2302 user.id,
2303 format!("invoked `fake_delete_post` with x value `{id}`"),
2304 ))
2305 .await?
2306 }
2307 }
2308
2309 let conn = match self.0.connect().await {
2310 Ok(c) => c,
2311 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2312 };
2313
2314 let res = execute!(
2315 &conn,
2316 "UPDATE posts SET is_deleted = $1 WHERE id = $2",
2317 params![&if is_deleted { 1 } else { 0 }, &(id as i64)]
2318 );
2319
2320 if let Err(e) = res {
2321 return Err(Error::DatabaseError(e.to_string()));
2322 }
2323
2324 self.0.1.remove(format!("atto.post:{}", id)).await;
2325
2326 if is_deleted {
2327 if let Some(replying_to) = y.replying_to {
2329 if replying_to != 0 {
2330 self.decr_post_comments(replying_to).await.unwrap();
2331 }
2332 }
2333
2334 let owner = self.get_user_by_id(y.owner).await?;
2336
2337 if owner.post_count > 0 {
2338 self.decr_user_post_count(y.owner).await?;
2339 }
2340
2341 let community = self.get_community_by_id_no_void(y.community).await?;
2343
2344 if community.post_count > 0 {
2345 self.decr_community_post_count(y.community).await?;
2346 }
2347
2348 if y.context.answering != 0 {
2350 let question = self.get_question_by_id(y.context.answering).await?;
2351
2352 if question.is_global {
2353 self.decr_question_answer_count(y.context.answering).await?;
2354 }
2355 }
2356
2357 for upload in y.uploads {
2359 self.delete_upload(upload).await?;
2360 }
2361
2362 if y.context.answering != 0 {
2364 let question = self.get_question_by_id(y.context.answering).await?;
2365
2366 if !question.is_global {
2367 self.delete_question(question.id, &user).await?;
2368 }
2369 }
2370 } else {
2371 if let Some(replying_to) = y.replying_to {
2373 self.incr_post_comments(replying_to).await.unwrap();
2374 }
2375
2376 self.incr_user_post_count(y.owner).await?;
2378
2379 self.incr_community_post_count(y.community).await?;
2381
2382 if y.context.answering != 0 {
2384 let question = self.get_question_by_id(y.context.answering).await?;
2385
2386 if question.is_global {
2387 self.incr_question_answer_count(y.context.answering).await?;
2388 }
2389 }
2390
2391 }
2393
2394 Ok(())
2396 }
2397
2398 pub async fn update_post_is_open(&self, id: usize, user: User, is_open: bool) -> Result<()> {
2399 let y = self.get_post_by_id(id).await?;
2400
2401 let community = self.get_community_by_id(y.community).await?;
2403
2404 if !community.is_forge {
2405 return Err(Error::MiscError(
2406 "This community does not support this".to_string(),
2407 ));
2408 }
2409
2410 let user_membership = self
2412 .get_membership_by_owner_community(user.id, y.community)
2413 .await?;
2414
2415 if (user.id != y.owner)
2416 && !user_membership
2417 .role
2418 .check(CommunityPermission::MANAGE_POSTS)
2419 {
2420 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2421 return Err(Error::NotAllowed);
2422 } else {
2423 self.create_audit_log_entry(AuditLogEntry::new(
2424 user.id,
2425 format!("invoked `update_post_is_open` with x value `{id}`"),
2426 ))
2427 .await?
2428 }
2429 }
2430
2431 let conn = match self.0.connect().await {
2433 Ok(c) => c,
2434 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2435 };
2436
2437 let res = execute!(
2438 &conn,
2439 "UPDATE posts SET is_open = $1 WHERE id = $2",
2440 params![&if is_open { 1 } else { 0 }, &(id as i64)]
2441 );
2442
2443 if let Err(e) = res {
2444 return Err(Error::DatabaseError(e.to_string()));
2445 }
2446
2447 self.0.1.remove(format!("atto.post:{}", id)).await;
2448 Ok(())
2449 }
2450
2451 pub async fn update_post_context(
2452 &self,
2453 id: usize,
2454 user: User,
2455 mut x: PostContext,
2456 ) -> Result<()> {
2457 let y = self.get_post_by_id(id).await?;
2458 x.repost = y.context.repost; x.answering = y.context.answering; let user_membership = self
2462 .get_membership_by_owner_community(user.id, y.community)
2463 .await?;
2464
2465 if (user.id != y.owner)
2466 && !user_membership
2467 .role
2468 .check(CommunityPermission::MANAGE_POSTS)
2469 {
2470 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2471 return Err(Error::NotAllowed);
2472 } else {
2473 self.create_audit_log_entry(AuditLogEntry::new(
2474 user.id,
2475 format!("invoked `update_post_context` with x value `{id}`"),
2476 ))
2477 .await?
2478 }
2479 }
2480
2481 if x.is_pinned != y.context.is_pinned
2483 && !user_membership.role.check(CommunityPermission::MANAGE_PINS)
2484 {
2485 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2488 return Err(Error::NotAllowed);
2489 } else {
2490 self.create_audit_log_entry(AuditLogEntry::new(
2491 user.id,
2492 format!("invoked `update_post_context(pinned)` with x value `{id}`"),
2493 ))
2494 .await?
2495 }
2496 }
2497
2498 if (x.is_profile_pinned != y.context.is_profile_pinned) && (user.id != y.owner) {
2500 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2501 return Err(Error::NotAllowed);
2502 } else {
2503 self.create_audit_log_entry(AuditLogEntry::new(
2504 user.id,
2505 format!("invoked `update_post_context(profile_pinned)` with x value `{id}`"),
2506 ))
2507 .await?
2508 }
2509 }
2510
2511 if user.settings.auto_unlist {
2513 x.is_nsfw = true;
2514 }
2515
2516 if user.settings.auto_full_unlist {
2517 x.full_unlist = true;
2518 }
2519
2520 let conn = match self.0.connect().await {
2522 Ok(c) => c,
2523 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2524 };
2525
2526 let res = execute!(
2527 &conn,
2528 "UPDATE posts SET context = $1 WHERE id = $2",
2529 params![&serde_json::to_string(&x).unwrap(), &(id as i64)]
2530 );
2531
2532 if let Err(e) = res {
2533 return Err(Error::DatabaseError(e.to_string()));
2534 }
2535
2536 self.0.1.remove(format!("atto.post:{}", id)).await;
2537
2538 Ok(())
2540 }
2541
2542 pub async fn update_post_content(&self, id: usize, user: User, x: String) -> Result<()> {
2543 let mut y = self.get_post_by_id(id).await?;
2544
2545 if user.id != y.owner {
2546 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2547 return Err(Error::NotAllowed);
2548 } else {
2549 self.create_audit_log_entry(AuditLogEntry::new(
2550 user.id,
2551 format!("invoked `update_post_content` with x value `{id}`"),
2552 ))
2553 .await?
2554 }
2555 }
2556
2557 if x.len() < 2 {
2559 return Err(Error::DataTooShort("content".to_string()));
2560 } else if x.len() > 4096 {
2561 return Err(Error::DataTooLong("content".to_string()));
2562 }
2563
2564 let conn = match self.0.connect().await {
2566 Ok(c) => c,
2567 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2568 };
2569
2570 let res = execute!(
2571 &conn,
2572 "UPDATE posts SET content = $1 WHERE id = $2",
2573 params![&x, &(id as i64)]
2574 );
2575
2576 if let Err(e) = res {
2577 return Err(Error::DatabaseError(e.to_string()));
2578 }
2579
2580 y.context.edited = unix_epoch_timestamp();
2582 self.update_post_context(id, user, y.context).await?;
2583
2584 Ok(())
2586 }
2587
2588 pub async fn update_post_title(&self, id: usize, user: User, x: String) -> Result<()> {
2589 let mut y = self.get_post_by_id(id).await?;
2590
2591 if user.id != y.owner {
2592 if !user.permissions.check(FinePermission::MANAGE_POSTS) {
2593 return Err(Error::NotAllowed);
2594 } else {
2595 self.create_audit_log_entry(AuditLogEntry::new(
2596 user.id,
2597 format!("invoked `update_post_title` with x value `{id}`"),
2598 ))
2599 .await?
2600 }
2601 }
2602
2603 let community = self.get_community_by_id(y.community).await?;
2604
2605 if !community.context.enable_titles {
2606 return Err(Error::MiscError(
2607 "Community does not allow titles".to_string(),
2608 ));
2609 }
2610
2611 if x.len() < 2 && community.context.require_titles {
2612 return Err(Error::DataTooShort("title".to_string()));
2613 } else if x.len() > 128 {
2614 return Err(Error::DataTooLong("title".to_string()));
2615 }
2616
2617 let conn = match self.0.connect().await {
2619 Ok(c) => c,
2620 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
2621 };
2622
2623 let res = execute!(
2624 &conn,
2625 "UPDATE posts SET title = $1 WHERE id = $2",
2626 params![&x, &(id as i64)]
2627 );
2628
2629 if let Err(e) = res {
2630 return Err(Error::DatabaseError(e.to_string()));
2631 }
2632
2633 y.context.edited = unix_epoch_timestamp();
2635 self.update_post_context(id, user, y.context).await?;
2636
2637 Ok(())
2639 }
2640
2641 auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2642 auto_method!(incr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2643 auto_method!(decr_post_likes() -> "UPDATE posts SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
2644 auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
2645
2646 auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
2647 auto_method!(decr_post_comments()@get_post_by_id -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr=comment_count);
2648}