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