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