1use std::collections::HashMap;
2use crate::config::StringBan;
3use crate::model::groups::GroupRole;
4use crate::model::{
5 Error, Result,
6 auth::User,
7 posts::{Post, PostContext},
8 permissions::FinePermission,
9 moderation::AuditLogEntry,
10 auth::{AchievementName, Notification},
11 id::Id,
12};
13use tritools::time::unix_epoch_timestamp;
14use crate::{auto_method, DataManager};
15use oiseau::{PostgresRow, execute, get, query_row, query_rows, params, cache::Cache};
16
17impl DataManager {
18 pub(crate) fn get_post_from_row(x: &PostgresRow) -> Post {
20 Post {
21 id: Id::deserialize(&get!(x->0(String))),
22 created: get!(x->1(i64)) as u128,
23 content: get!(x->2(String)),
24 owner: Id::deserialize(&get!(x->3(String))),
25 context: serde_json::from_str(&get!(x->4(String))).unwrap(),
26 replying_to: x
27 .get::<usize, Option<String>>(5)
28 .map(|x| Id::deserialize(&x)),
29 likes: get!(x->6(i32)) as isize,
31 dislikes: get!(x->7(i32)) as isize,
32 comment_count: get!(x->8(i32)) as usize,
34 uploads: serde_json::from_str(&get!(x->9(String))).unwrap(),
36 is_deleted: get!(x->10(i32)) as i8 == 1,
37 poll_id: x
39 .get::<usize, Option<String>>(12)
40 .map(|x| Id::deserialize(&x)),
41 views: get!(x->13(i32)) as usize,
42 group: x
43 .get::<usize, Option<String>>(14)
44 .map(|x| Id::deserialize(&x)),
45 limited_reach: get!(x->15(i32)) == 1,
46 }
47 }
48
49 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:{}");
50
51 pub async fn get_replies_by_post(
58 &self,
59 id: &Id,
60 batch: usize,
61 page: usize,
62 ) -> Result<Vec<Post>> {
63 let conn = match self.0.connect().await {
64 Ok(c) => c,
65 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
66 };
67
68 let res = query_rows!(
69 &conn,
70 "SELECT * FROM posts WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
71 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
72 |x| { Self::get_post_from_row(x) }
73 );
74
75 if res.is_err() {
76 return Err(Error::GeneralNotFound("post".to_string()));
77 }
78
79 Ok(res.unwrap())
80 }
81
82 pub async fn get_posts_by_user(&self, id: &Id, batch: usize, page: usize) -> Result<Vec<Post>> {
89 let conn = match self.0.connect().await {
90 Ok(c) => c,
91 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
92 };
93
94 let res = query_rows!(
95 &conn,
96 "SELECT * FROM posts WHERE owner = $1 AND replying_to IS NULL AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
97 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
98 |x| { Self::get_post_from_row(x) }
99 );
100
101 if res.is_err() {
102 return Err(Error::GeneralNotFound("post".to_string()));
103 }
104
105 Ok(res.unwrap())
106 }
107
108 pub async fn get_posts_by_group(
115 &self,
116 id: &Id,
117 batch: usize,
118 page: usize,
119 ) -> Result<Vec<Post>> {
120 let conn = match self.0.connect().await {
121 Ok(c) => c,
122 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
123 };
124
125 let res = query_rows!(
126 &conn,
127 "SELECT * FROM posts WHERE group_id = $1 AND replying_to IS NULL AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
128 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
129 |x| { Self::get_post_from_row(x) }
130 );
131
132 if res.is_err() {
133 return Err(Error::GeneralNotFound("post".to_string()));
134 }
135
136 Ok(res.unwrap())
137 }
138
139 pub async fn get_popular_posts_by_user(
146 &self,
147 id: &Id,
148 batch: usize,
149 page: usize,
150 ) -> Result<Vec<Post>> {
151 let conn = match self.0.connect().await {
152 Ok(c) => c,
153 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
154 };
155
156 let res = query_rows!(
157 &conn,
158 "SELECT * FROM posts WHERE owner = $1 AND replying_to IS NULL AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 ORDER BY (likes - dislikes) DESC, created DESC LIMIT $2 OFFSET $3",
159 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
160 |x| { Self::get_post_from_row(x) }
161 );
162
163 if res.is_err() {
164 return Err(Error::GeneralNotFound("post".to_string()));
165 }
166
167 Ok(res.unwrap())
168 }
169
170 pub async fn get_responses_by_user(
177 &self,
178 id: &Id,
179 batch: usize,
180 page: usize,
181 ) -> Result<Vec<Post>> {
182 let conn = match self.0.connect().await {
183 Ok(c) => c,
184 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
185 };
186
187 let res = query_rows!(
188 &conn,
189 "SELECT * FROM posts WHERE owner = $1 AND replying_to IS NULL AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 AND NOT context LIKE '%\"answering\":null%' ORDER BY created DESC LIMIT $2 OFFSET $3",
190 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
191 |x| { Self::get_post_from_row(x) }
192 );
193
194 if res.is_err() {
195 return Err(Error::GeneralNotFound("post".to_string()));
196 }
197
198 Ok(res.unwrap())
199 }
200
201 pub async fn get_replies_by_user(
208 &self,
209 id: &Id,
210 batch: usize,
211 page: usize,
212 user: &Option<User>,
213 ) -> Result<Vec<Post>> {
214 let conn = match self.0.connect().await {
215 Ok(c) => c,
216 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
217 };
218
219 let mut hide_nsfw: bool = true;
221
222 if let Some(ua) = user {
223 hide_nsfw = !ua.settings.show_nsfw;
224 }
225
226 let res = query_rows!(
228 &conn,
229 &format!(
230 "SELECT * FROM posts WHERE owner = $1 AND NOT replying_to IS NULL AND NOT (context::json->>'is_profile_pinned')::boolean {} ORDER BY created DESC LIMIT $2 OFFSET $3",
231 if hide_nsfw {
232 "AND NOT (context::json->>'is_nsfw')::boolean"
233 } else {
234 ""
235 }
236 ),
237 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
238 |x| { Self::get_post_from_row(x) }
239 );
240
241 if res.is_err() {
242 return Err(Error::GeneralNotFound("post".to_string()));
243 }
244
245 Ok(res.unwrap())
246 }
247
248 pub async fn get_media_posts_by_user(
255 &self,
256 id: &Id,
257 batch: usize,
258 page: usize,
259 user: &Option<User>,
260 ) -> Result<Vec<Post>> {
261 let conn = match self.0.connect().await {
262 Ok(c) => c,
263 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
264 };
265
266 let mut hide_nsfw: bool = true;
268
269 if let Some(ua) = user {
270 hide_nsfw = !ua.settings.show_nsfw;
271 }
272
273 let res = query_rows!(
275 &conn,
276 &format!(
277 "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",
278 if hide_nsfw {
279 "AND NOT (context::json->>'is_nsfw')::boolean"
280 } else {
281 ""
282 }
283 ),
284 &[&id.printable(), &(batch as i64), &((page * batch) as i64)],
285 |x| { Self::get_post_from_row(x) }
286 );
287
288 if res.is_err() {
289 return Err(Error::GeneralNotFound("post".to_string()));
290 }
291
292 Ok(res.unwrap())
293 }
294
295 pub async fn get_posts_by_user_searched(
304 &self,
305 id: &Id,
306 text_query: &str,
307 batch: usize,
308 page: usize,
309 user: &Option<&User>,
310 ) -> Result<Vec<Post>> {
311 let conn = match self.0.connect().await {
312 Ok(c) => c,
313 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
314 };
315
316 let mut hide_nsfw: bool = true;
318
319 if let Some(ua) = user {
320 hide_nsfw = !ua.settings.show_nsfw;
321 }
322
323 let res = query_rows!(
325 &conn,
326 &format!(
327 "SELECT * FROM posts WHERE owner = $1 AND tsvector_content @@ to_tsquery($2) {} AND is_deleted = 0 ORDER BY created DESC LIMIT $3 OFFSET $4",
328 if hide_nsfw {
329 "AND NOT (context::json->>'is_nsfw')::boolean"
330 } else {
331 ""
332 }
333 ),
334 params![
335 &id.printable(),
336 &text_query,
337 &(batch as i64),
338 &((page * batch) as i64)
339 ],
340 |x| { Self::get_post_from_row(x) }
341 );
342
343 if res.is_err() {
344 return Err(Error::GeneralNotFound("post".to_string()));
345 }
346
347 Ok(res.unwrap())
348 }
349
350 pub async fn get_posts_searched(
357 &self,
358 text_query: &str,
359 batch: usize,
360 page: usize,
361 ) -> Result<Vec<Post>> {
362 let conn = match self.0.connect().await {
363 Ok(c) => c,
364 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
365 };
366
367 let res = query_rows!(
369 &conn,
370 "SELECT * FROM posts WHERE tsvector_content @@ to_tsquery($1) AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
371 params![
372 &text_query.replace(" ", " & "),
373 &(batch as i64),
374 &((page * batch) as i64)
375 ],
376 |x| { Self::get_post_from_row(x) }
377 );
378
379 if res.is_err() {
380 return Err(Error::GeneralNotFound("post".to_string()));
381 }
382
383 Ok(res.unwrap())
384 }
385
386 pub async fn get_posts_by_user_tag(
394 &self,
395 id: &Id,
396 tag: &str,
397 batch: usize,
398 page: usize,
399 ) -> Result<Vec<Post>> {
400 let conn = match self.0.connect().await {
401 Ok(c) => c,
402 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
403 };
404
405 let res = query_rows!(
406 &conn,
407 "SELECT * FROM posts WHERE owner = $1 AND context::json->>'tags' LIKE $2 AND is_deleted = 0 ORDER BY created DESC LIMIT $3 OFFSET $4",
408 params![
409 &id.printable(),
410 &format!("%\"{tag}\"%"),
411 &(batch as i64),
412 &((page * batch) as i64)
413 ],
414 |x| { Self::get_post_from_row(x) }
415 );
416
417 if res.is_err() {
418 return Err(Error::GeneralNotFound("post".to_string()));
419 }
420
421 Ok(res.unwrap())
422 }
423
424 pub async fn get_responses_by_user_tag(
433 &self,
434 id: &Id,
435 tag: &str,
436 batch: usize,
437 page: usize,
438 ) -> Result<Vec<Post>> {
439 let conn = match self.0.connect().await {
440 Ok(c) => c,
441 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
442 };
443
444 let res = query_rows!(
445 &conn,
446 "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",
447 params![
448 &id.printable(),
449 &format!("%\"{tag}\"%"),
450 &(batch as i64),
451 &((page * batch) as i64)
452 ],
453 |x| { Self::get_post_from_row(x) }
454 );
455
456 if res.is_err() {
457 return Err(Error::GeneralNotFound("post".to_string()));
458 }
459
460 Ok(res.unwrap())
461 }
462
463 pub async fn get_pinned_posts_by_user(&self, id: &Id) -> Result<Vec<Post>> {
468 let conn = match self.0.connect().await {
469 Ok(c) => c,
470 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
471 };
472
473 let res = query_rows!(
474 &conn,
475 "SELECT * FROM posts WHERE owner = $1 AND context LIKE '%\"is_profile_pinned\":true%' ORDER BY created DESC",
476 &[&id.printable()],
477 |x| { Self::get_post_from_row(x) }
478 );
479
480 if res.is_err() {
481 return Err(Error::GeneralNotFound("post".to_string()));
482 }
483
484 Ok(res.unwrap())
485 }
486
487 pub async fn get_posts_by_question(
494 &self,
495 id: &Id,
496 batch: usize,
497 page: usize,
498 ) -> Result<Vec<Post>> {
499 let conn = match self.0.connect().await {
500 Ok(c) => c,
501 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
502 };
503
504 let res = query_rows!(
505 &conn,
506 "SELECT * FROM posts WHERE context LIKE $1 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
507 params![
508 &format!("%\"answering\":\"{id}\"%"),
509 &(batch as i64),
510 &((page * batch) as i64)
511 ],
512 |x| { Self::get_post_from_row(x) }
513 );
514
515 if res.is_err() {
516 return Err(Error::GeneralNotFound("post".to_string()));
517 }
518
519 Ok(res.unwrap())
520 }
521
522 pub async fn get_post_by_owner_question(&self, owner: &Id, question: &Id) -> Result<Post> {
528 let conn = match self.0.connect().await {
529 Ok(c) => c,
530 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
531 };
532
533 let res = query_row!(
534 &conn,
535 "SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 AND is_deleted = 0 LIMIT 1",
536 params![
537 &format!("%\"answering\":\"{question}\"%"),
538 &owner.printable(),
539 ],
540 |x| { Ok(Self::get_post_from_row(x)) }
541 );
542
543 if res.is_err() {
544 return Err(Error::GeneralNotFound("post".to_string()));
545 }
546
547 Ok(res.unwrap())
548 }
549
550 pub async fn get_quoting_posts_by_quoting(
560 &self,
561 id: &Id,
562 batch: usize,
563 page: usize,
564 ) -> Result<Vec<Post>> {
565 let conn = match self.0.connect().await {
566 Ok(c) => c,
567 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
568 };
569
570 let res = query_rows!(
571 &conn,
572 "SELECT * FROM posts WHERE NOT content = '' AND context LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
573 params![
574 &format!("%\"reposting\":{id}%"),
575 &(batch as i64),
576 &((page * batch) as i64)
577 ],
578 |x| { Self::get_post_from_row(x) }
579 );
580
581 if res.is_err() {
582 return Err(Error::GeneralNotFound("post".to_string()));
583 }
584
585 Ok(res.unwrap())
586 }
587
588 pub async fn get_reposts_by_quoting(
598 &self,
599 id: &Id,
600 batch: usize,
601 page: usize,
602 ) -> Result<Vec<Post>> {
603 let conn = match self.0.connect().await {
604 Ok(c) => c,
605 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
606 };
607
608 let res = query_rows!(
609 &conn,
610 "SELECT * FROM posts WHERE content = '' AND context LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
611 params![
612 &format!("%\"reposting\":{id}%"),
613 &(batch as i64),
614 &((page * batch) as i64)
615 ],
616 |x| { Self::get_post_from_row(x) }
617 );
618
619 if res.is_err() {
620 return Err(Error::GeneralNotFound("post".to_string()));
621 }
622
623 Ok(res.unwrap())
624 }
625
626 pub async fn get_popular_posts(
633 &self,
634 batch: usize,
635 before: usize,
636 cutoff: usize,
637 ) -> Result<Vec<Post>> {
638 let conn = match self.0.connect().await {
639 Ok(c) => c,
640 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
641 };
642
643 let res = query_rows!(
644 &conn,
645 &format!(
646 "SELECT * FROM posts WHERE replying_to IS NULL AND limited_reach = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2{} ORDER BY (likes - dislikes) DESC, created ASC LIMIT $3",
647 if before > 0 {
648 format!(" AND created < {before}")
649 } else {
650 String::new()
651 }
652 ),
653 &[
654 &(unix_epoch_timestamp() as i64),
655 &(cutoff as i64),
656 &(batch as i64),
657 ],
658 |x| { Self::get_post_from_row(x) }
659 );
660
661 if let Err(e) = res {
662 return Err(Error::DatabaseError(e.to_string()));
663 }
664
665 Ok(res.unwrap())
666 }
667
668 pub async fn get_latest_posts(
674 &self,
675 as_user: &Option<&User>,
676 batch: usize,
677 page: usize,
678 ) -> Result<Vec<Post>> {
679 let hide_answers: bool = if let Some(user) = as_user {
680 user.settings.all_timeline_hide_answers
681 } else {
682 false
683 };
684
685 let mut hide_nsfw: bool = true;
687 let mut can_view_deleted: bool = false;
688
689 if let Some(ua) = as_user {
690 hide_nsfw = !ua.settings.show_nsfw;
691 can_view_deleted = ua.permissions.check(FinePermission::ManagePosts);
692 }
693
694 let conn = match self.0.connect().await {
696 Ok(c) => c,
697 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
698 };
699
700 let res = query_rows!(
701 &conn,
702 &format!(
703 "SELECT * FROM posts WHERE replying_to IS NULL AND limited_reach = 0 AND group_id IS NULL{}{}{} ORDER BY created DESC LIMIT $1 OFFSET $2",
704 if hide_nsfw {
705 " AND NOT context LIKE '%\"is_nsfw\":true%'"
706 } else {
707 ""
708 },
709 if hide_answers {
710 " AND context LIKE '%\"answering\":null%'"
711 } else {
712 ""
713 },
714 if !can_view_deleted {
715 " AND is_deleted = 0"
716 } else {
717 ""
718 }
719 ),
720 &[&(batch as i64), &((page * batch) as i64)],
721 |x| { Self::get_post_from_row(x) }
722 );
723
724 if let Err(e) = res {
725 return Err(Error::DatabaseError(e.to_string()));
726 }
727
728 Ok(res.unwrap())
729 }
730
731 pub async fn get_posts_from_user_following(
738 &self,
739 id: &Id,
740 batch: usize,
741 page: usize,
742 ) -> Result<Vec<Post>> {
743 let following = self.get_user_follows_by_initiator_all(id).await?;
744 let mut following = following.iter();
745 let first = match following.next() {
746 Some(f) => f,
747 None => return Ok(Vec::new()),
748 };
749
750 let mut query_string: String = String::new();
751
752 for user in following {
753 query_string.push_str(&format!(" OR owner = '{}'", user.receiver));
754 }
755
756 let conn = match self.0.connect().await {
758 Ok(c) => c,
759 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
760 };
761
762 let res = query_rows!(
763 &conn,
764 &format!(
765 "SELECT * FROM posts WHERE (owner = '{id}' OR owner = '{}' {query_string}) AND replying_to IS NULL AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
766 first.receiver,
767 ),
768 &[&(batch as i64), &((page * batch) as i64)],
769 |x| { Self::get_post_from_row(x) }
770 );
771
772 if let Err(e) = res {
773 return Err(Error::DatabaseError(e.to_string()));
774 }
775
776 Ok(res.unwrap())
777 }
778
779 pub async fn get_posts_from_user_groups(
786 &self,
787 id: &Id,
788 batch: usize,
789 page: usize,
790 ) -> Result<Vec<Post>> {
791 let groups = self.get_group_memberships_group_by_owner_all(id).await?;
792 let mut groups = groups.iter();
793 let first = match groups.next() {
794 Some(f) => f,
795 None => return Ok(Vec::new()),
796 };
797
798 let mut query_string: String = String::new();
799
800 for id in groups {
801 query_string.push_str(&format!(" OR group_id = '{}'", id));
802 }
803
804 let conn = match self.0.connect().await {
806 Ok(c) => c,
807 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
808 };
809
810 let res = query_rows!(
811 &conn,
812 &format!(
813 "SELECT * FROM posts WHERE group_id = '{first}' {query_string} AND replying_to IS NULL AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
814 ),
815 &[&(batch as i64), &((page * batch) as i64)],
816 |x| { Self::get_post_from_row(x) }
817 );
818
819 if let Err(e) = res {
820 return Err(Error::DatabaseError(e.to_string()));
821 }
822
823 Ok(res.unwrap())
824 }
825
826 pub async fn create_post(&self, mut data: Post) -> Result<Id> {
831 for ban in &self.0.0.banned_data {
833 match ban {
834 StringBan::String(x) => {
835 if data.content.contains(x) {
836 return Ok(Id::Legacy(0));
837 }
838 }
839 StringBan::Unicode(x) => {
840 if data.content.contains(&match char::from_u32(x.to_owned()) {
841 Some(c) => c.to_string(),
842 None => continue,
843 }) {
844 return Ok(Id::Legacy(0));
845 }
846 }
847 }
848 }
849
850 let mut owner = self.get_user_by_id(&data.owner).await?;
852
853 if let Some(ref group) = data.group {
855 let membership = self
857 .get_group_membership_by_owner_group(&owner.id, group)
858 .await?;
859
860 if membership.role == GroupRole::Pending || membership.role == GroupRole::Banned {
861 return Err(Error::NotAllowed);
862 }
863 }
864
865 let is_reposting = if let Some(ref repost) = data.context.repost {
867 repost.reposting.is_some()
868 } else {
869 false
870 };
871
872 if !is_reposting {
873 if data.content.len() < 2 && data.uploads.is_empty() {
874 return Err(Error::DataTooShort("content".to_string()));
875 } else if data.content.len() > 4096 {
876 return Err(Error::DataTooLong("content".to_string()));
877 }
878 }
879
880 if let Some(ref answering) = data.context.answering {
882 let question = self.get_question_by_id(answering).await?;
883
884 if self
886 .get_post_by_owner_question(&owner.id, &question.id)
887 .await
888 .is_ok()
889 {
890 return Err(Error::MiscError(
891 "You've already answered this question".to_string(),
892 ));
893 }
894
895 if !question.is_global {
896 self.delete_request(&question.id, &question.id, &owner, false)
897 .await?;
898 } else {
899 self.incr_question_answer_count(answering).await?;
900 }
901
902 if (question.owner != data.owner)
905 && (question.owner != Id::Legacy(0))
906 && (!owner.settings.private_profile
907 | self
908 .get_user_follow_by_initiator_receiver(&data.owner, &question.owner)
909 .await
910 .is_ok())
911 {
912 self.create_notification(Notification::new(
913 "Your question has received a new answer!".to_string(),
914 format!(
915 "[@{}](/api/v2/users/find/{}) has answered your [question](/questions/{}).",
916 owner.username, owner.id, question.id
917 ),
918 question.owner,
919 ))
920 .await?;
921 }
922
923 if question.context.is_nsfw {
925 data.context.is_nsfw = question.context.is_nsfw;
926 }
927 }
928
929 let reposting = if let Some(ref repost) = data.context.repost {
931 if let Some(ref id) = repost.reposting {
932 Some(self.get_post_by_id(id).await?)
933 } else {
934 None
935 }
936 } else {
937 None
938 };
939
940 if let Some(ref rt) = reposting {
941 if data.content.is_empty() {
942 data.context.reposts_enabled = false;
944 data.context.reactions_enabled = false;
945 data.context.comments_enabled = false;
946 }
947
948 if rt.context.is_nsfw {
950 data.context.is_nsfw = true;
951 }
952
953 if !rt.context.reposts_enabled {
955 return Err(Error::MiscError("Post has reposts disabled".to_string()));
956 }
957
958 if self
960 .get_user_block_by_initiator_receiver(&rt.owner, &data.owner)
961 .await
962 .is_ok()
963 {
964 return Err(Error::NotAllowed);
965 }
966
967 if owner.id != rt.owner && !owner.settings.private_profile {
969 self.create_notification(Notification::new(
970 format!(
971 "[@{}](/api/v2/users/find/{}) has [quoted](/posts/{}) your [post](/posts/{})",
972 owner.username, owner.id, data.id, rt.id
973 ),
974 if data.content.is_empty() {
975 String::new()
976 } else {
977 format!("\"{}\"", data.content)
978 },
979 rt.owner.to_owned(),
980 ))
981 .await?;
982 }
983
984 self.add_achievement(&mut owner, AchievementName::CreateRepost.into(), true)
986 .await?;
987 }
988
989 let replying_to = if let Some(ref id) = data.replying_to {
991 Some(self.get_post_by_id(id).await?)
992 } else {
993 None
994 };
995
996 if let Some(ref rt) = replying_to {
997 if !rt.context.comments_enabled {
998 return Err(Error::MiscError("Post has comments disabled".to_string()));
999 }
1000
1001 if self
1003 .get_user_block_by_initiator_receiver(&rt.owner, &data.owner)
1004 .await
1005 .is_ok()
1006 {
1007 return Err(Error::NotAllowed);
1008 }
1009 }
1010
1011 let mut already_notified: HashMap<String, User> = HashMap::new();
1013 for username in User::parse_mentions(&data.content) {
1014 let user = {
1015 if let Some(ua) = already_notified.get(&username) {
1016 ua.to_owned()
1017 } else {
1018 let user = self.get_user_by_username(&username).await?;
1019
1020 if self
1022 .get_user_block_by_initiator_receiver(&user.id, &data.owner)
1023 .await
1024 .is_ok()
1025 {
1026 return Err(Error::NotAllowed);
1027 }
1028
1029 if user.settings.private_profile
1031 && self
1032 .get_user_follow_by_initiator_receiver(&user.id, &data.owner)
1033 .await
1034 .is_err()
1035 {
1036 return Err(Error::NotAllowed);
1037 }
1038
1039 self.create_notification(Notification::new(
1041 "You've been mentioned in a post!".to_string(),
1042 format!(
1043 "[@{}](/api/v2/users/find/{}) has mentioned you in their [post](/posts/{}).",
1044 owner.username, owner.id, data.id
1045 ),
1046 user.id.to_owned(),
1047 ))
1048 .await?;
1049
1050 already_notified.insert(username.to_owned(), user.clone());
1052 user
1053 }
1054 };
1055
1056 data.content = data.content.replace(
1057 &format!("@{username}"),
1058 &format!("[@{username}](/api/v2/users/find/{})", user.id),
1059 );
1060 }
1061
1062 if owner.settings.auto_nsfw {
1064 data.context.is_nsfw = true;
1065 }
1066
1067 let conn = match self.0.connect().await {
1069 Ok(c) => c,
1070 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1071 };
1072
1073 let res = execute!(
1074 &conn,
1075 "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, DEFAULT, $12, $13, $14, $15)",
1076 params![
1077 &data.id.printable(),
1078 &(data.created as i64),
1079 &data.content,
1080 &data.owner.printable(),
1081 &serde_json::to_string(&data.context).unwrap(),
1082 &data.replying_to.map(|x| x.printable()),
1083 &0_i32,
1084 &0_i32,
1085 &0_i32,
1086 &serde_json::to_string(&data.uploads).unwrap(),
1087 &{ if data.is_deleted { 1 } else { 0 } },
1088 &data.poll_id.map(|x| x.printable()),
1089 &0_i32,
1090 &data.group.map(|x| x.printable()),
1091 &{ if data.limited_reach { 1 } else { 0 } },
1092 ]
1093 );
1094
1095 if let Err(e) = res {
1096 return Err(Error::DatabaseError(e.to_string()));
1097 }
1098
1099 if let Some(rt) = replying_to {
1101 self.incr_post_comments(&rt.id).await.unwrap();
1102
1103 if data.owner != rt.owner {
1105 let owner = self.get_user_by_id(&data.owner).await?;
1106
1107 if !owner.settings.private_profile
1111 | self
1112 .get_user_follow_by_initiator_receiver(&data.owner, &rt.owner)
1113 .await
1114 .is_ok()
1115 {
1116 self.create_notification(Notification::new(
1117 "Your post has received a new comment!".to_string(),
1118 format!(
1119 "[@{}](/api/v2/users/find/{}) has commented on your [post](/posts/{}).",
1120 owner.username, owner.id, rt.id
1121 ),
1122 rt.owner,
1123 ))
1124 .await?;
1125 }
1126
1127 if !rt.context.comments_enabled {
1128 return Err(Error::NotAllowed);
1129 }
1130 }
1131 }
1132
1133 self.incr_user_post_count(&data.owner).await?;
1135
1136 Ok(data.id)
1138 }
1139
1140 pub async fn delete_post(&self, id: &Id, user: User) -> Result<()> {
1141 let y = self.get_post_by_id(id).await?;
1142
1143 if user.id != y.owner {
1144 if !user.permissions.check(FinePermission::ManagePosts) {
1145 return Err(Error::NotAllowed);
1146 } else {
1147 self.create_audit_log_entry(AuditLogEntry::new(
1148 user.id.to_owned(),
1149 format!("invoked `delete_post` with x value `{id}`"),
1150 ))
1151 .await?
1152 }
1153 }
1154
1155 let conn = match self.0.connect().await {
1156 Ok(c) => c,
1157 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1158 };
1159
1160 let res = execute!(&conn, "DELETE FROM posts WHERE id = $1", &[&id.printable()]);
1161
1162 if let Err(e) = res {
1163 return Err(Error::DatabaseError(e.to_string()));
1164 }
1165
1166 self.0.1.remove(format!("atto.post:{}", id)).await;
1167
1168 if !y.is_deleted {
1169 if let Some(replying_to) = y.replying_to {
1171 self.decr_post_comments(&replying_to).await.unwrap();
1172 }
1173
1174 let owner = self.get_user_by_id(&y.owner).await?;
1176
1177 if owner.post_count > 0 {
1178 self.decr_user_post_count(&y.owner).await?;
1179 }
1180
1181 if let Some(answering) = y.context.answering {
1183 let question = self.get_question_by_id(&answering).await?;
1184
1185 if question.is_global {
1186 self.decr_question_answer_count(&answering).await?;
1187 } else {
1188 self.delete_question(&question.id, &user).await?;
1189 }
1190 }
1191 }
1192
1193 for upload in y.uploads {
1195 if let Err(e) = self.1.delete_upload(upload).await {
1196 return Err(Error::MiscError(e.to_string()));
1197 }
1198 }
1199
1200 if let Some(poll_id) = y.poll_id {
1202 self.delete_poll(&poll_id, &user).await?;
1203 }
1204
1205 Ok(())
1207 }
1208
1209 pub async fn fake_delete_post(&self, id: &Id, user: User, is_deleted: bool) -> Result<()> {
1210 let y = self.get_post_by_id(id).await?;
1211
1212 macro_rules! del {
1213 ($user:ident, $self:ident) => {
1214 if !$user.permissions.check(FinePermission::ManagePosts) {
1215 return Err(Error::NotAllowed);
1216 } else {
1217 $self
1218 .create_audit_log_entry(AuditLogEntry::new(
1219 user.id.to_owned(),
1220 format!("invoked `fake_delete_post` with x value `{id}`"),
1221 ))
1222 .await?
1223 }
1224 };
1225 }
1226
1227 if user.id != y.owner {
1228 if let Some(ref id) = y.group {
1229 if let Ok(x) = self.get_group_membership_by_owner_group(&user.id, id).await {
1230 if x.role != GroupRole::Owner {
1231 del!(user, self);
1232 }
1233 } else {
1234 del!(user, self);
1235 }
1236 } else {
1237 del!(user, self);
1238 }
1239 }
1240
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 = execute!(
1247 &conn,
1248 "UPDATE posts SET is_deleted = $1 WHERE id = $2",
1249 params![&if is_deleted { 1 } else { 0 }, &id.printable()]
1250 );
1251
1252 if let Err(e) = res {
1253 return Err(Error::DatabaseError(e.to_string()));
1254 }
1255
1256 self.0.1.remove(format!("atto.post:{}", id)).await;
1257
1258 if is_deleted {
1259 if let Some(replying_to) = y.replying_to {
1261 self.decr_post_comments(&replying_to).await.unwrap();
1262 }
1263
1264 let owner = self.get_user_by_id(&y.owner).await?;
1266
1267 if owner.post_count > 0 {
1268 self.decr_user_post_count(&y.owner).await?;
1269 }
1270
1271 if let Some(answering) = y.context.answering {
1273 let question = self.get_question_by_id(&answering).await?;
1274
1275 if question.is_global {
1276 self.decr_question_answer_count(&answering).await?;
1277 } else {
1278 self.delete_question(&question.id, &user).await?;
1279 }
1280 }
1281
1282 for upload in y.uploads {
1284 if let Err(e) = self.1.delete_upload(upload).await {
1285 return Err(Error::MiscError(e.to_string()));
1286 }
1287 }
1288 } else {
1289 if let Some(replying_to) = y.replying_to {
1291 self.incr_post_comments(&replying_to).await.unwrap();
1292 }
1293
1294 self.incr_user_post_count(&y.owner).await?;
1296
1297 if let Some(answering) = y.context.answering {
1299 let question = self.get_question_by_id(&answering).await?;
1300
1301 if question.is_global {
1302 self.incr_question_answer_count(&answering).await?;
1303 }
1304 }
1305
1306 }
1308
1309 Ok(())
1311 }
1312
1313 pub async fn update_post_context(&self, id: &Id, user: User, mut x: PostContext) -> Result<()> {
1314 let y = self.get_post_by_id(id).await?;
1315 x.repost = y.context.repost; x.answering = y.context.answering; if user.id != y.owner {
1319 if !user.permissions.check(FinePermission::ManagePosts) {
1320 return Err(Error::NotAllowed);
1321 } else {
1322 self.create_audit_log_entry(AuditLogEntry::new(
1323 user.id.clone(),
1324 format!("invoked `update_post_context` with x value `{id}`"),
1325 ))
1326 .await?
1327 }
1328 }
1329
1330 if (x.is_profile_pinned != y.context.is_profile_pinned) && (user.id != y.owner) {
1332 if !user.permissions.check(FinePermission::ManagePosts) {
1333 return Err(Error::NotAllowed);
1334 } else {
1335 self.create_audit_log_entry(AuditLogEntry::new(
1336 user.id,
1337 format!("invoked `update_post_context(profile_pinned)` with x value `{id}`"),
1338 ))
1339 .await?
1340 }
1341 }
1342
1343 if user.settings.auto_nsfw {
1345 x.is_nsfw = true;
1346 }
1347
1348 let conn = match self.0.connect().await {
1350 Ok(c) => c,
1351 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1352 };
1353
1354 let res = execute!(
1355 &conn,
1356 "UPDATE posts SET context = $1 WHERE id = $2",
1357 params![&serde_json::to_string(&x).unwrap(), &id.printable()]
1358 );
1359
1360 if let Err(e) = res {
1361 return Err(Error::DatabaseError(e.to_string()));
1362 }
1363
1364 self.0.1.remove(format!("atto.post:{}", id)).await;
1365
1366 Ok(())
1368 }
1369
1370 pub async fn update_post_content(&self, id: &Id, user: User, x: String) -> Result<()> {
1371 let mut y = self.get_post_by_id(id).await?;
1372
1373 if user.id != y.owner {
1374 if !user.permissions.check(FinePermission::ManagePosts) {
1375 return Err(Error::NotAllowed);
1376 } else {
1377 self.create_audit_log_entry(AuditLogEntry::new(
1378 user.id.clone(),
1379 format!("invoked `update_post_content` with x value `{id}`"),
1380 ))
1381 .await?
1382 }
1383 }
1384
1385 if x.len() < 2 {
1387 return Err(Error::DataTooShort("content".to_string()));
1388 } else if x.len() > 4096 {
1389 return Err(Error::DataTooLong("content".to_string()));
1390 }
1391
1392 let conn = match self.0.connect().await {
1394 Ok(c) => c,
1395 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
1396 };
1397
1398 let res = execute!(
1399 &conn,
1400 "UPDATE posts SET content = $1 WHERE id = $2",
1401 params![&x, &id.printable()]
1402 );
1403
1404 if let Err(e) = res {
1405 return Err(Error::DatabaseError(e.to_string()));
1406 }
1407
1408 y.context.edited = unix_epoch_timestamp();
1410 self.update_post_context(id, user, y.context).await?;
1411
1412 Ok(())
1414 }
1415
1416 auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
1417 auto_method!(incr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
1418 auto_method!(decr_post_likes() -> "UPDATE posts SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
1419 auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
1420
1421 auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
1422 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);
1423
1424 auto_method!(incr_post_views() -> "UPDATE posts SET views = views + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
1425 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);
1426}