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