]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
ec537ad9471298b62637afcd8b875243bd5183f3
[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.show_read_posts.unwrap_or(true) {
403       query = query.filter(post_read::id.is_null());
404     };
405
406     if self.saved_only.unwrap_or(false) {
407       query = query.filter(post_saved::id.is_not_null());
408     };
409
410     // Don't show blocked communities or persons
411     if self.my_person_id.is_some() {
412       query = query.filter(community_block::person_id.is_null());
413       query = query.filter(person_block::person_id.is_null());
414     }
415
416     query = match self.sort.unwrap_or(SortType::Hot) {
417       SortType::Active => query
418         .then_order_by(
419           hot_rank(
420             post_aggregates::score,
421             post_aggregates::newest_comment_time_necro,
422           )
423           .desc(),
424         )
425         .then_order_by(post_aggregates::newest_comment_time_necro.desc()),
426       SortType::Hot => query
427         .then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
428         .then_order_by(post_aggregates::published.desc()),
429       SortType::New => query.then_order_by(post_aggregates::published.desc()),
430       SortType::MostComments => query.then_order_by(post_aggregates::comments.desc()),
431       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
432       SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
433       SortType::TopYear => query
434         .filter(post::published.gt(now - 1.years()))
435         .then_order_by(post_aggregates::score.desc()),
436       SortType::TopMonth => query
437         .filter(post::published.gt(now - 1.months()))
438         .then_order_by(post_aggregates::score.desc()),
439       SortType::TopWeek => query
440         .filter(post::published.gt(now - 1.weeks()))
441         .then_order_by(post_aggregates::score.desc()),
442       SortType::TopDay => query
443         .filter(post::published.gt(now - 1.days()))
444         .then_order_by(post_aggregates::score.desc()),
445     };
446
447     let (limit, offset) = limit_and_offset(self.page, self.limit);
448
449     query = query
450       .limit(limit)
451       .offset(offset)
452       .filter(post::removed.eq(false))
453       .filter(post::deleted.eq(false))
454       .filter(community::removed.eq(false))
455       .filter(community::deleted.eq(false));
456
457     debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
458
459     let res = query.load::<PostViewTuple>(self.conn)?;
460
461     Ok(PostView::from_tuple_to_vec(res))
462   }
463 }
464
465 impl ViewToVec for PostView {
466   type DbTuple = PostViewTuple;
467   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
468     items
469       .iter()
470       .map(|a| Self {
471         post: a.0.to_owned(),
472         creator: a.1.to_owned(),
473         community: a.2.to_owned(),
474         creator_banned_from_community: a.3.is_some(),
475         counts: a.4.to_owned(),
476         subscribed: a.5.is_some(),
477         saved: a.6.is_some(),
478         read: a.7.is_some(),
479         creator_blocked: a.8.is_some(),
480         my_vote: a.9,
481       })
482       .collect::<Vec<Self>>()
483   }
484 }
485
486 #[cfg(test)]
487 mod tests {
488   use crate::post_view::{PostQueryBuilder, PostView};
489   use lemmy_db_queries::{
490     aggregates::post_aggregates::PostAggregates,
491     establish_unpooled_connection,
492     Blockable,
493     Crud,
494     Likeable,
495     ListingType,
496     SortType,
497   };
498   use lemmy_db_schema::source::{
499     community::*,
500     community_block::{CommunityBlock, CommunityBlockForm},
501     person::*,
502     person_block::{PersonBlock, PersonBlockForm},
503     post::*,
504   };
505   use serial_test::serial;
506
507   #[test]
508   #[serial]
509   fn test_crud() {
510     let conn = establish_unpooled_connection();
511
512     let person_name = "tegan".to_string();
513     let community_name = "test_community_3".to_string();
514     let post_name = "test post 3".to_string();
515     let bot_post_name = "test bot post".to_string();
516
517     let new_person = PersonForm {
518       name: person_name.to_owned(),
519       ..PersonForm::default()
520     };
521
522     let inserted_person = Person::create(&conn, &new_person).unwrap();
523
524     let new_bot = PersonForm {
525       name: person_name.to_owned(),
526       bot_account: Some(true),
527       ..PersonForm::default()
528     };
529
530     let inserted_bot = Person::create(&conn, &new_bot).unwrap();
531
532     let new_community = CommunityForm {
533       name: community_name.to_owned(),
534       title: "nada".to_owned(),
535       ..CommunityForm::default()
536     };
537
538     let inserted_community = Community::create(&conn, &new_community).unwrap();
539
540     // Test a person block, make sure the post query doesn't include their post
541     let blocked_person = PersonForm {
542       name: person_name.to_owned(),
543       ..PersonForm::default()
544     };
545
546     let inserted_blocked_person = Person::create(&conn, &blocked_person).unwrap();
547
548     let post_from_blocked_person = PostForm {
549       name: "blocked_person_post".to_string(),
550       creator_id: inserted_blocked_person.id,
551       community_id: inserted_community.id,
552       ..PostForm::default()
553     };
554
555     Post::create(&conn, &post_from_blocked_person).unwrap();
556
557     // block that person
558     let person_block = PersonBlockForm {
559       person_id: inserted_person.id,
560       target_id: inserted_blocked_person.id,
561     };
562
563     PersonBlock::block(&conn, &person_block).unwrap();
564
565     // A sample post
566     let new_post = PostForm {
567       name: post_name.to_owned(),
568       creator_id: inserted_person.id,
569       community_id: inserted_community.id,
570       ..PostForm::default()
571     };
572
573     let inserted_post = Post::create(&conn, &new_post).unwrap();
574
575     let new_bot_post = PostForm {
576       name: bot_post_name,
577       creator_id: inserted_bot.id,
578       community_id: inserted_community.id,
579       ..PostForm::default()
580     };
581
582     let _inserted_bot_post = Post::create(&conn, &new_bot_post).unwrap();
583
584     let post_like_form = PostLikeForm {
585       post_id: inserted_post.id,
586       person_id: inserted_person.id,
587       score: 1,
588     };
589
590     let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
591
592     let expected_post_like = PostLike {
593       id: inserted_post_like.id,
594       post_id: inserted_post.id,
595       person_id: inserted_person.id,
596       published: inserted_post_like.published,
597       score: 1,
598     };
599
600     let read_post_listings_with_person = PostQueryBuilder::create(&conn)
601       .listing_type(ListingType::Community)
602       .sort(SortType::New)
603       .show_bot_accounts(false)
604       .community_id(inserted_community.id)
605       .my_person_id(inserted_person.id)
606       .list()
607       .unwrap();
608
609     let read_post_listings_no_person = PostQueryBuilder::create(&conn)
610       .listing_type(ListingType::Community)
611       .sort(SortType::New)
612       .community_id(inserted_community.id)
613       .list()
614       .unwrap();
615
616     let read_post_listing_no_person = PostView::read(&conn, inserted_post.id, None).unwrap();
617     let read_post_listing_with_person =
618       PostView::read(&conn, inserted_post.id, Some(inserted_person.id)).unwrap();
619
620     let agg = PostAggregates::read(&conn, inserted_post.id).unwrap();
621
622     // the non person version
623     let expected_post_listing_no_person = PostView {
624       post: Post {
625         id: inserted_post.id,
626         name: post_name,
627         creator_id: inserted_person.id,
628         url: None,
629         body: None,
630         published: inserted_post.published,
631         updated: None,
632         community_id: inserted_community.id,
633         removed: false,
634         deleted: false,
635         locked: false,
636         stickied: false,
637         nsfw: false,
638         embed_title: None,
639         embed_description: None,
640         embed_html: None,
641         thumbnail_url: None,
642         ap_id: inserted_post.ap_id.to_owned(),
643         local: true,
644       },
645       my_vote: None,
646       creator: PersonSafe {
647         id: inserted_person.id,
648         name: person_name,
649         display_name: None,
650         published: inserted_person.published,
651         avatar: None,
652         actor_id: inserted_person.actor_id.to_owned(),
653         local: true,
654         admin: false,
655         bot_account: false,
656         banned: false,
657         deleted: false,
658         bio: None,
659         banner: None,
660         updated: None,
661         inbox_url: inserted_person.inbox_url.to_owned(),
662         shared_inbox_url: None,
663         matrix_user_id: None,
664       },
665       creator_banned_from_community: false,
666       community: CommunitySafe {
667         id: inserted_community.id,
668         name: community_name,
669         icon: None,
670         removed: false,
671         deleted: false,
672         nsfw: false,
673         actor_id: inserted_community.actor_id.to_owned(),
674         local: true,
675         title: "nada".to_owned(),
676         description: None,
677         updated: None,
678         banner: None,
679         published: inserted_community.published,
680       },
681       counts: PostAggregates {
682         id: agg.id,
683         post_id: inserted_post.id,
684         comments: 0,
685         score: 1,
686         upvotes: 1,
687         downvotes: 0,
688         stickied: false,
689         published: agg.published,
690         newest_comment_time_necro: inserted_post.published,
691         newest_comment_time: inserted_post.published,
692       },
693       subscribed: false,
694       read: false,
695       saved: false,
696       creator_blocked: false,
697     };
698
699     // Test a community block
700     let community_block = CommunityBlockForm {
701       person_id: inserted_person.id,
702       community_id: inserted_community.id,
703     };
704     CommunityBlock::block(&conn, &community_block).unwrap();
705
706     let read_post_listings_with_person_after_block = PostQueryBuilder::create(&conn)
707       .listing_type(ListingType::Community)
708       .sort(SortType::New)
709       .show_bot_accounts(false)
710       .community_id(inserted_community.id)
711       .my_person_id(inserted_person.id)
712       .list()
713       .unwrap();
714
715     // TODO More needs to be added here
716     let mut expected_post_listing_with_user = expected_post_listing_no_person.to_owned();
717     expected_post_listing_with_user.my_vote = Some(1);
718
719     let like_removed = PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
720     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
721     PersonBlock::unblock(&conn, &person_block).unwrap();
722     CommunityBlock::unblock(&conn, &community_block).unwrap();
723     Community::delete(&conn, inserted_community.id).unwrap();
724     Person::delete(&conn, inserted_person.id).unwrap();
725     Person::delete(&conn, inserted_bot.id).unwrap();
726     Person::delete(&conn, inserted_blocked_person.id).unwrap();
727
728     // The with user
729     assert_eq!(
730       expected_post_listing_with_user,
731       read_post_listings_with_person[0]
732     );
733     assert_eq!(
734       expected_post_listing_with_user,
735       read_post_listing_with_person
736     );
737
738     // Should be only one person, IE the bot post, and blocked should be missing
739     assert_eq!(1, read_post_listings_with_person.len());
740
741     // Without the user
742     assert_eq!(
743       expected_post_listing_no_person,
744       read_post_listings_no_person[1]
745     );
746     assert_eq!(expected_post_listing_no_person, read_post_listing_no_person);
747
748     // Should be 2 posts, with the bot post, and the blocked
749     assert_eq!(3, read_post_listings_no_person.len());
750
751     // Should be 0 posts after the community block
752     assert_eq!(0, read_post_listings_with_person_after_block.len());
753
754     assert_eq!(expected_post_like, inserted_post_like);
755     assert_eq!(1, like_removed);
756     assert_eq!(1, num_deleted);
757   }
758 }