]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
Merge pull request #1850 from LemmyNet/refactor-apub
[lemmy.git] / crates / db_views / src / post_view.rs
1 use diesel::{pg::Pg, result::Error, *};
2 use lemmy_db_schema::{
3   aggregates::post_aggregates::PostAggregates,
4   functions::hot_rank,
5   fuzzy_search,
6   limit_and_offset,
7   newtypes::{CommunityId, DbUrl, PersonId, PostId},
8   schema::{
9     community,
10     community_block,
11     community_follower,
12     community_person_ban,
13     person,
14     person_block,
15     post,
16     post_aggregates,
17     post_like,
18     post_read,
19     post_saved,
20   },
21   source::{
22     community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
23     person::{Person, PersonSafe},
24     person_block::PersonBlock,
25     post::{Post, PostRead, PostSaved},
26   },
27   traits::{MaybeOptional, ToSafe, ViewToVec},
28   ListingType,
29   SortType,
30 };
31 use log::debug;
32 use serde::{Deserialize, Serialize};
33
34 #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
35 pub struct PostView {
36   pub post: Post,
37   pub creator: PersonSafe,
38   pub community: CommunitySafe,
39   pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
40   pub counts: PostAggregates,
41   pub subscribed: bool,      // Left join to CommunityFollower
42   pub saved: bool,           // Left join to PostSaved
43   pub read: bool,            // Left join to PostRead
44   pub creator_blocked: bool, // Left join to PersonBlock
45   pub my_vote: Option<i16>,  // Left join to PostLike
46 }
47
48 type PostViewTuple = (
49   Post,
50   PersonSafe,
51   CommunitySafe,
52   Option<CommunityPersonBan>,
53   PostAggregates,
54   Option<CommunityFollower>,
55   Option<PostSaved>,
56   Option<PostRead>,
57   Option<PersonBlock>,
58   Option<i16>,
59 );
60
61 impl PostView {
62   pub fn read(
63     conn: &PgConnection,
64     post_id: PostId,
65     my_person_id: Option<PersonId>,
66   ) -> Result<Self, Error> {
67     // The left join below will return None in this case
68     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
69
70     let (
71       post,
72       creator,
73       community,
74       creator_banned_from_community,
75       counts,
76       follower,
77       saved,
78       read,
79       creator_blocked,
80       post_like,
81     ) = post::table
82       .find(post_id)
83       .inner_join(person::table)
84       .inner_join(community::table)
85       .left_join(
86         community_person_ban::table.on(
87           post::community_id
88             .eq(community_person_ban::community_id)
89             .and(community_person_ban::person_id.eq(post::creator_id)),
90         ),
91       )
92       .inner_join(post_aggregates::table)
93       .left_join(
94         community_follower::table.on(
95           post::community_id
96             .eq(community_follower::community_id)
97             .and(community_follower::person_id.eq(person_id_join)),
98         ),
99       )
100       .left_join(
101         post_saved::table.on(
102           post::id
103             .eq(post_saved::post_id)
104             .and(post_saved::person_id.eq(person_id_join)),
105         ),
106       )
107       .left_join(
108         post_read::table.on(
109           post::id
110             .eq(post_read::post_id)
111             .and(post_read::person_id.eq(person_id_join)),
112         ),
113       )
114       .left_join(
115         person_block::table.on(
116           post::creator_id
117             .eq(person_block::target_id)
118             .and(person_block::person_id.eq(person_id_join)),
119         ),
120       )
121       .left_join(
122         post_like::table.on(
123           post::id
124             .eq(post_like::post_id)
125             .and(post_like::person_id.eq(person_id_join)),
126         ),
127       )
128       .select((
129         post::all_columns,
130         Person::safe_columns_tuple(),
131         Community::safe_columns_tuple(),
132         community_person_ban::all_columns.nullable(),
133         post_aggregates::all_columns,
134         community_follower::all_columns.nullable(),
135         post_saved::all_columns.nullable(),
136         post_read::all_columns.nullable(),
137         person_block::all_columns.nullable(),
138         post_like::score.nullable(),
139       ))
140       .first::<PostViewTuple>(conn)?;
141
142     // If a person is given, then my_vote, if None, should be 0, not null
143     // Necessary to differentiate between other person's votes
144     let my_vote = if my_person_id.is_some() && post_like.is_none() {
145       Some(0)
146     } else {
147       post_like
148     };
149
150     Ok(PostView {
151       post,
152       creator,
153       community,
154       creator_banned_from_community: creator_banned_from_community.is_some(),
155       counts,
156       subscribed: follower.is_some(),
157       saved: saved.is_some(),
158       read: read.is_some(),
159       creator_blocked: creator_blocked.is_some(),
160       my_vote,
161     })
162   }
163 }
164
165 pub struct PostQueryBuilder<'a> {
166   conn: &'a PgConnection,
167   listing_type: Option<ListingType>,
168   sort: Option<SortType>,
169   creator_id: Option<PersonId>,
170   community_id: Option<CommunityId>,
171   community_actor_id: Option<DbUrl>,
172   my_person_id: Option<PersonId>,
173   search_term: Option<String>,
174   url_search: Option<String>,
175   show_nsfw: Option<bool>,
176   show_bot_accounts: Option<bool>,
177   show_read_posts: Option<bool>,
178   saved_only: Option<bool>,
179   page: Option<i64>,
180   limit: Option<i64>,
181 }
182
183 impl<'a> PostQueryBuilder<'a> {
184   pub fn create(conn: &'a PgConnection) -> Self {
185     PostQueryBuilder {
186       conn,
187       listing_type: None,
188       sort: None,
189       creator_id: None,
190       community_id: None,
191       community_actor_id: None,
192       my_person_id: None,
193       search_term: None,
194       url_search: None,
195       show_nsfw: None,
196       show_bot_accounts: None,
197       show_read_posts: None,
198       saved_only: None,
199       page: None,
200       limit: None,
201     }
202   }
203
204   pub fn listing_type<T: MaybeOptional<ListingType>>(mut self, listing_type: T) -> Self {
205     self.listing_type = listing_type.get_optional();
206     self
207   }
208
209   pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self {
210     self.sort = sort.get_optional();
211     self
212   }
213
214   pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
215     self.community_id = community_id.get_optional();
216     self
217   }
218
219   pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
220     self.my_person_id = my_person_id.get_optional();
221     self
222   }
223
224   pub fn community_actor_id<T: MaybeOptional<DbUrl>>(mut self, community_actor_id: T) -> Self {
225     self.community_actor_id = community_actor_id.get_optional();
226     self
227   }
228
229   pub fn creator_id<T: MaybeOptional<PersonId>>(mut self, creator_id: T) -> Self {
230     self.creator_id = creator_id.get_optional();
231     self
232   }
233
234   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
235     self.search_term = search_term.get_optional();
236     self
237   }
238
239   pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
240     self.url_search = url_search.get_optional();
241     self
242   }
243
244   pub fn show_nsfw<T: MaybeOptional<bool>>(mut self, show_nsfw: T) -> Self {
245     self.show_nsfw = show_nsfw.get_optional();
246     self
247   }
248
249   pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
250     self.show_bot_accounts = show_bot_accounts.get_optional();
251     self
252   }
253
254   pub fn show_read_posts<T: MaybeOptional<bool>>(mut self, show_read_posts: T) -> Self {
255     self.show_read_posts = show_read_posts.get_optional();
256     self
257   }
258
259   pub fn saved_only<T: MaybeOptional<bool>>(mut self, saved_only: T) -> Self {
260     self.saved_only = saved_only.get_optional();
261     self
262   }
263
264   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
265     self.page = page.get_optional();
266     self
267   }
268
269   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
270     self.limit = limit.get_optional();
271     self
272   }
273
274   pub fn list(self) -> Result<Vec<PostView>, Error> {
275     use diesel::dsl::*;
276
277     // The left join below will return None in this case
278     let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
279
280     let mut query = post::table
281       .inner_join(person::table)
282       .inner_join(community::table)
283       .left_join(
284         community_person_ban::table.on(
285           post::community_id
286             .eq(community_person_ban::community_id)
287             .and(community_person_ban::person_id.eq(post::creator_id)),
288         ),
289       )
290       .inner_join(post_aggregates::table)
291       .left_join(
292         community_follower::table.on(
293           post::community_id
294             .eq(community_follower::community_id)
295             .and(community_follower::person_id.eq(person_id_join)),
296         ),
297       )
298       .left_join(
299         post_saved::table.on(
300           post::id
301             .eq(post_saved::post_id)
302             .and(post_saved::person_id.eq(person_id_join)),
303         ),
304       )
305       .left_join(
306         post_read::table.on(
307           post::id
308             .eq(post_read::post_id)
309             .and(post_read::person_id.eq(person_id_join)),
310         ),
311       )
312       .left_join(
313         person_block::table.on(
314           post::creator_id
315             .eq(person_block::target_id)
316             .and(person_block::person_id.eq(person_id_join)),
317         ),
318       )
319       .left_join(
320         community_block::table.on(
321           community::id
322             .eq(community_block::community_id)
323             .and(community_block::person_id.eq(person_id_join)),
324         ),
325       )
326       .left_join(
327         post_like::table.on(
328           post::id
329             .eq(post_like::post_id)
330             .and(post_like::person_id.eq(person_id_join)),
331         ),
332       )
333       .select((
334         post::all_columns,
335         Person::safe_columns_tuple(),
336         Community::safe_columns_tuple(),
337         community_person_ban::all_columns.nullable(),
338         post_aggregates::all_columns,
339         community_follower::all_columns.nullable(),
340         post_saved::all_columns.nullable(),
341         post_read::all_columns.nullable(),
342         person_block::all_columns.nullable(),
343         post_like::score.nullable(),
344       ))
345       .into_boxed();
346
347     if let Some(listing_type) = self.listing_type {
348       query = match listing_type {
349         ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()),
350         ListingType::Local => query.filter(community::local.eq(true)),
351         _ => query,
352       };
353     }
354
355     if let Some(community_id) = self.community_id {
356       query = query
357         .filter(post::community_id.eq(community_id))
358         .then_order_by(post_aggregates::stickied.desc());
359     }
360
361     if let Some(community_actor_id) = self.community_actor_id {
362       query = query
363         .filter(community::actor_id.eq(community_actor_id))
364         .then_order_by(post_aggregates::stickied.desc());
365     }
366
367     if let Some(url_search) = self.url_search {
368       query = query.filter(post::url.eq(url_search));
369     }
370
371     if let Some(search_term) = self.search_term {
372       let searcher = fuzzy_search(&search_term);
373       query = query.filter(
374         post::name
375           .ilike(searcher.to_owned())
376           .or(post::body.ilike(searcher)),
377       );
378     }
379
380     // If its for a specific person, show the removed / deleted
381     if let Some(creator_id) = self.creator_id {
382       query = query.filter(post::creator_id.eq(creator_id));
383     }
384
385     if !self.show_nsfw.unwrap_or(false) {
386       query = query
387         .filter(post::nsfw.eq(false))
388         .filter(community::nsfw.eq(false));
389     };
390
391     if !self.show_bot_accounts.unwrap_or(true) {
392       query = query.filter(person::bot_account.eq(false));
393     };
394
395     if self.saved_only.unwrap_or(false) {
396       query = query.filter(post_saved::id.is_not_null());
397     }
398     // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read
399     // setting wont be able to see saved posts.
400     else if !self.show_read_posts.unwrap_or(true) {
401       query = query.filter(post_read::id.is_null());
402     }
403
404     // Don't show blocked communities or persons
405     if self.my_person_id.is_some() {
406       query = query.filter(community_block::person_id.is_null());
407       query = query.filter(person_block::person_id.is_null());
408     }
409
410     query = match self.sort.unwrap_or(SortType::Hot) {
411       SortType::Active => query
412         .then_order_by(
413           hot_rank(
414             post_aggregates::score,
415             post_aggregates::newest_comment_time_necro,
416           )
417           .desc(),
418         )
419         .then_order_by(post_aggregates::newest_comment_time_necro.desc()),
420       SortType::Hot => query
421         .then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
422         .then_order_by(post_aggregates::published.desc()),
423       SortType::New => query.then_order_by(post_aggregates::published.desc()),
424       SortType::MostComments => query.then_order_by(post_aggregates::comments.desc()),
425       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
426       SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
427       SortType::TopYear => query
428         .filter(post::published.gt(now - 1.years()))
429         .then_order_by(post_aggregates::score.desc()),
430       SortType::TopMonth => query
431         .filter(post::published.gt(now - 1.months()))
432         .then_order_by(post_aggregates::score.desc()),
433       SortType::TopWeek => query
434         .filter(post::published.gt(now - 1.weeks()))
435         .then_order_by(post_aggregates::score.desc()),
436       SortType::TopDay => query
437         .filter(post::published.gt(now - 1.days()))
438         .then_order_by(post_aggregates::score.desc()),
439     };
440
441     let (limit, offset) = limit_and_offset(self.page, self.limit);
442
443     query = query
444       .limit(limit)
445       .offset(offset)
446       .filter(post::removed.eq(false))
447       .filter(post::deleted.eq(false))
448       .filter(community::removed.eq(false))
449       .filter(community::deleted.eq(false));
450
451     debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
452
453     let res = query.load::<PostViewTuple>(self.conn)?;
454
455     Ok(PostView::from_tuple_to_vec(res))
456   }
457 }
458
459 impl ViewToVec for PostView {
460   type DbTuple = PostViewTuple;
461   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
462     items
463       .iter()
464       .map(|a| Self {
465         post: a.0.to_owned(),
466         creator: a.1.to_owned(),
467         community: a.2.to_owned(),
468         creator_banned_from_community: a.3.is_some(),
469         counts: a.4.to_owned(),
470         subscribed: a.5.is_some(),
471         saved: a.6.is_some(),
472         read: a.7.is_some(),
473         creator_blocked: a.8.is_some(),
474         my_vote: a.9,
475       })
476       .collect::<Vec<Self>>()
477   }
478 }
479
480 #[cfg(test)]
481 mod tests {
482   use crate::post_view::{PostQueryBuilder, PostView};
483   use lemmy_db_schema::{
484     aggregates::post_aggregates::PostAggregates,
485     establish_unpooled_connection,
486     source::{
487       community::*,
488       community_block::{CommunityBlock, CommunityBlockForm},
489       person::*,
490       person_block::{PersonBlock, PersonBlockForm},
491       post::*,
492     },
493     traits::{Blockable, Crud, Likeable},
494     ListingType,
495     SortType,
496   };
497   use serial_test::serial;
498
499   #[test]
500   #[serial]
501   fn test_crud() {
502     let conn = establish_unpooled_connection();
503
504     let person_name = "tegan".to_string();
505     let community_name = "test_community_3".to_string();
506     let post_name = "test post 3".to_string();
507     let bot_post_name = "test bot post".to_string();
508
509     let new_person = PersonForm {
510       name: person_name.to_owned(),
511       ..PersonForm::default()
512     };
513
514     let inserted_person = Person::create(&conn, &new_person).unwrap();
515
516     let new_bot = PersonForm {
517       name: person_name.to_owned(),
518       bot_account: Some(true),
519       ..PersonForm::default()
520     };
521
522     let inserted_bot = Person::create(&conn, &new_bot).unwrap();
523
524     let new_community = CommunityForm {
525       name: community_name.to_owned(),
526       title: "nada".to_owned(),
527       ..CommunityForm::default()
528     };
529
530     let inserted_community = Community::create(&conn, &new_community).unwrap();
531
532     // Test a person block, make sure the post query doesn't include their post
533     let blocked_person = PersonForm {
534       name: person_name.to_owned(),
535       ..PersonForm::default()
536     };
537
538     let inserted_blocked_person = Person::create(&conn, &blocked_person).unwrap();
539
540     let post_from_blocked_person = PostForm {
541       name: "blocked_person_post".to_string(),
542       creator_id: inserted_blocked_person.id,
543       community_id: inserted_community.id,
544       ..PostForm::default()
545     };
546
547     Post::create(&conn, &post_from_blocked_person).unwrap();
548
549     // block that person
550     let person_block = PersonBlockForm {
551       person_id: inserted_person.id,
552       target_id: inserted_blocked_person.id,
553     };
554
555     PersonBlock::block(&conn, &person_block).unwrap();
556
557     // A sample post
558     let new_post = PostForm {
559       name: post_name.to_owned(),
560       creator_id: inserted_person.id,
561       community_id: inserted_community.id,
562       ..PostForm::default()
563     };
564
565     let inserted_post = Post::create(&conn, &new_post).unwrap();
566
567     let new_bot_post = PostForm {
568       name: bot_post_name,
569       creator_id: inserted_bot.id,
570       community_id: inserted_community.id,
571       ..PostForm::default()
572     };
573
574     let _inserted_bot_post = Post::create(&conn, &new_bot_post).unwrap();
575
576     let post_like_form = PostLikeForm {
577       post_id: inserted_post.id,
578       person_id: inserted_person.id,
579       score: 1,
580     };
581
582     let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
583
584     let expected_post_like = PostLike {
585       id: inserted_post_like.id,
586       post_id: inserted_post.id,
587       person_id: inserted_person.id,
588       published: inserted_post_like.published,
589       score: 1,
590     };
591
592     let read_post_listings_with_person = PostQueryBuilder::create(&conn)
593       .listing_type(ListingType::Community)
594       .sort(SortType::New)
595       .show_bot_accounts(false)
596       .community_id(inserted_community.id)
597       .my_person_id(inserted_person.id)
598       .list()
599       .unwrap();
600
601     let read_post_listings_no_person = PostQueryBuilder::create(&conn)
602       .listing_type(ListingType::Community)
603       .sort(SortType::New)
604       .community_id(inserted_community.id)
605       .list()
606       .unwrap();
607
608     let read_post_listing_no_person = PostView::read(&conn, inserted_post.id, None).unwrap();
609     let read_post_listing_with_person =
610       PostView::read(&conn, inserted_post.id, Some(inserted_person.id)).unwrap();
611
612     let agg = PostAggregates::read(&conn, inserted_post.id).unwrap();
613
614     // the non person version
615     let expected_post_listing_no_person = PostView {
616       post: Post {
617         id: inserted_post.id,
618         name: post_name,
619         creator_id: inserted_person.id,
620         url: None,
621         body: None,
622         published: inserted_post.published,
623         updated: None,
624         community_id: inserted_community.id,
625         removed: false,
626         deleted: false,
627         locked: false,
628         stickied: false,
629         nsfw: false,
630         embed_title: None,
631         embed_description: None,
632         embed_html: None,
633         thumbnail_url: None,
634         ap_id: inserted_post.ap_id.to_owned(),
635         local: true,
636       },
637       my_vote: None,
638       creator: PersonSafe {
639         id: inserted_person.id,
640         name: person_name,
641         display_name: None,
642         published: inserted_person.published,
643         avatar: None,
644         actor_id: inserted_person.actor_id.to_owned(),
645         local: true,
646         admin: false,
647         bot_account: false,
648         banned: false,
649         deleted: false,
650         bio: None,
651         banner: None,
652         updated: None,
653         inbox_url: inserted_person.inbox_url.to_owned(),
654         shared_inbox_url: None,
655         matrix_user_id: None,
656       },
657       creator_banned_from_community: false,
658       community: CommunitySafe {
659         id: inserted_community.id,
660         name: community_name,
661         icon: None,
662         removed: false,
663         deleted: false,
664         nsfw: false,
665         actor_id: inserted_community.actor_id.to_owned(),
666         local: true,
667         title: "nada".to_owned(),
668         description: None,
669         updated: None,
670         banner: None,
671         published: inserted_community.published,
672       },
673       counts: PostAggregates {
674         id: agg.id,
675         post_id: inserted_post.id,
676         comments: 0,
677         score: 1,
678         upvotes: 1,
679         downvotes: 0,
680         stickied: false,
681         published: agg.published,
682         newest_comment_time_necro: inserted_post.published,
683         newest_comment_time: inserted_post.published,
684       },
685       subscribed: false,
686       read: false,
687       saved: false,
688       creator_blocked: false,
689     };
690
691     // Test a community block
692     let community_block = CommunityBlockForm {
693       person_id: inserted_person.id,
694       community_id: inserted_community.id,
695     };
696     CommunityBlock::block(&conn, &community_block).unwrap();
697
698     let read_post_listings_with_person_after_block = PostQueryBuilder::create(&conn)
699       .listing_type(ListingType::Community)
700       .sort(SortType::New)
701       .show_bot_accounts(false)
702       .community_id(inserted_community.id)
703       .my_person_id(inserted_person.id)
704       .list()
705       .unwrap();
706
707     // TODO More needs to be added here
708     let mut expected_post_listing_with_user = expected_post_listing_no_person.to_owned();
709     expected_post_listing_with_user.my_vote = Some(1);
710
711     let like_removed = PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
712     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
713     PersonBlock::unblock(&conn, &person_block).unwrap();
714     CommunityBlock::unblock(&conn, &community_block).unwrap();
715     Community::delete(&conn, inserted_community.id).unwrap();
716     Person::delete(&conn, inserted_person.id).unwrap();
717     Person::delete(&conn, inserted_bot.id).unwrap();
718     Person::delete(&conn, inserted_blocked_person.id).unwrap();
719
720     // The with user
721     assert_eq!(
722       expected_post_listing_with_user,
723       read_post_listings_with_person[0]
724     );
725     assert_eq!(
726       expected_post_listing_with_user,
727       read_post_listing_with_person
728     );
729
730     // Should be only one person, IE the bot post, and blocked should be missing
731     assert_eq!(1, read_post_listings_with_person.len());
732
733     // Without the user
734     assert_eq!(
735       expected_post_listing_no_person,
736       read_post_listings_no_person[1]
737     );
738     assert_eq!(expected_post_listing_no_person, read_post_listing_no_person);
739
740     // Should be 2 posts, with the bot post, and the blocked
741     assert_eq!(3, read_post_listings_no_person.len());
742
743     // Should be 0 posts after the community block
744     assert_eq!(0, read_post_listings_with_person_after_block.len());
745
746     assert_eq!(expected_post_like, inserted_post_like);
747     assert_eq!(1, like_removed);
748     assert_eq!(1, num_deleted);
749   }
750 }