]> Untitled Git - lemmy.git/blob - crates/db_views_actor/src/community_view.rs
Use typed-builder crate for queries (#2379)
[lemmy.git] / crates / db_views_actor / src / community_view.rs
1 use crate::structs::{CommunityModeratorView, CommunityView, PersonViewSafe};
2 use diesel::{result::Error, *};
3 use lemmy_db_schema::{
4   aggregates::structs::CommunityAggregates,
5   newtypes::{CommunityId, PersonId},
6   schema::{community, community_aggregates, community_block, community_follower, local_user},
7   source::{
8     community::{Community, CommunityFollower, CommunitySafe},
9     community_block::CommunityBlock,
10   },
11   traits::{ToSafe, ViewToVec},
12   utils::{functions::hot_rank, fuzzy_search, limit_and_offset},
13   ListingType,
14   SortType,
15 };
16 use typed_builder::TypedBuilder;
17
18 type CommunityViewTuple = (
19   CommunitySafe,
20   CommunityAggregates,
21   Option<CommunityFollower>,
22   Option<CommunityBlock>,
23 );
24
25 impl CommunityView {
26   pub fn read(
27     conn: &PgConnection,
28     community_id: CommunityId,
29     my_person_id: Option<PersonId>,
30   ) -> Result<Self, Error> {
31     // The left join below will return None in this case
32     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
33
34     let (community, counts, follower, blocked) = community::table
35       .find(community_id)
36       .inner_join(community_aggregates::table)
37       .left_join(
38         community_follower::table.on(
39           community::id
40             .eq(community_follower::community_id)
41             .and(community_follower::person_id.eq(person_id_join)),
42         ),
43       )
44       .left_join(
45         community_block::table.on(
46           community::id
47             .eq(community_block::community_id)
48             .and(community_block::person_id.eq(person_id_join)),
49         ),
50       )
51       .select((
52         Community::safe_columns_tuple(),
53         community_aggregates::all_columns,
54         community_follower::all_columns.nullable(),
55         community_block::all_columns.nullable(),
56       ))
57       .first::<CommunityViewTuple>(conn)?;
58
59     Ok(CommunityView {
60       community,
61       subscribed: CommunityFollower::to_subscribed_type(&follower),
62       blocked: blocked.is_some(),
63       counts,
64     })
65   }
66
67   pub fn is_mod_or_admin(
68     conn: &PgConnection,
69     person_id: PersonId,
70     community_id: CommunityId,
71   ) -> bool {
72     let is_mod = CommunityModeratorView::for_community(conn, community_id)
73       .map(|v| {
74         v.into_iter()
75           .map(|m| m.moderator.id)
76           .collect::<Vec<PersonId>>()
77       })
78       .unwrap_or_default()
79       .contains(&person_id);
80     if is_mod {
81       return true;
82     }
83
84     PersonViewSafe::admins(conn)
85       .map(|v| {
86         v.into_iter()
87           .map(|a| a.person.id)
88           .collect::<Vec<PersonId>>()
89       })
90       .unwrap_or_default()
91       .contains(&person_id)
92   }
93 }
94
95 #[derive(TypedBuilder)]
96 #[builder(field_defaults(default))]
97 pub struct CommunityQuery<'a> {
98   #[builder(!default)]
99   conn: &'a PgConnection,
100   listing_type: Option<ListingType>,
101   sort: Option<SortType>,
102   my_person_id: Option<PersonId>,
103   show_nsfw: Option<bool>,
104   search_term: Option<String>,
105   page: Option<i64>,
106   limit: Option<i64>,
107 }
108
109 impl<'a> CommunityQuery<'a> {
110   pub fn list(self) -> Result<Vec<CommunityView>, Error> {
111     // The left join below will return None in this case
112     let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
113
114     let mut query = community::table
115       .inner_join(community_aggregates::table)
116       .left_join(local_user::table.on(local_user::person_id.eq(person_id_join)))
117       .left_join(
118         community_follower::table.on(
119           community::id
120             .eq(community_follower::community_id)
121             .and(community_follower::person_id.eq(person_id_join)),
122         ),
123       )
124       .left_join(
125         community_block::table.on(
126           community::id
127             .eq(community_block::community_id)
128             .and(community_block::person_id.eq(person_id_join)),
129         ),
130       )
131       .select((
132         Community::safe_columns_tuple(),
133         community_aggregates::all_columns,
134         community_follower::all_columns.nullable(),
135         community_block::all_columns.nullable(),
136       ))
137       .into_boxed();
138
139     if let Some(search_term) = self.search_term {
140       let searcher = fuzzy_search(&search_term);
141       query = query
142         .filter(community::name.ilike(searcher.to_owned()))
143         .or_filter(community::title.ilike(searcher.to_owned()))
144         .or_filter(community::description.ilike(searcher));
145     };
146
147     match self.sort.unwrap_or(SortType::Hot) {
148       SortType::New => query = query.order_by(community::published.desc()),
149       SortType::TopAll => query = query.order_by(community_aggregates::subscribers.desc()),
150       SortType::TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()),
151       SortType::Hot => {
152         query = query
153           .order_by(
154             hot_rank(
155               community_aggregates::subscribers,
156               community_aggregates::published,
157             )
158             .desc(),
159           )
160           .then_order_by(community_aggregates::published.desc());
161         // Don't show hidden communities in Hot (trending)
162         query = query.filter(
163           community::hidden
164             .eq(false)
165             .or(community_follower::person_id.eq(person_id_join)),
166         );
167       }
168       // Covers all other sorts
169       _ => {
170         query = query
171           .order_by(
172             hot_rank(
173               community_aggregates::subscribers,
174               community_aggregates::published,
175             )
176             .desc(),
177           )
178           .then_order_by(community_aggregates::published.desc())
179       }
180     };
181
182     if let Some(listing_type) = self.listing_type {
183       query = match listing_type {
184         ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
185         ListingType::Local => query.filter(community::local.eq(true)),
186         _ => query,
187       };
188     }
189
190     // Don't show blocked communities or nsfw communities if not enabled in profile
191     if self.my_person_id.is_some() {
192       query = query.filter(community_block::person_id.is_null());
193       query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true)));
194     } else {
195       // No person in request, only show nsfw communities if show_nsfw passed into request
196       if !self.show_nsfw.unwrap_or(false) {
197         query = query.filter(community::nsfw.eq(false));
198       }
199     }
200
201     let (limit, offset) = limit_and_offset(self.page, self.limit)?;
202     let res = query
203       .limit(limit)
204       .offset(offset)
205       .filter(community::removed.eq(false))
206       .filter(community::deleted.eq(false))
207       .load::<CommunityViewTuple>(self.conn)?;
208
209     Ok(CommunityView::from_tuple_to_vec(res))
210   }
211 }
212
213 impl ViewToVec for CommunityView {
214   type DbTuple = CommunityViewTuple;
215   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
216     items
217       .into_iter()
218       .map(|a| Self {
219         community: a.0,
220         counts: a.1,
221         subscribed: CommunityFollower::to_subscribed_type(&a.2),
222         blocked: a.3.is_some(),
223       })
224       .collect::<Vec<Self>>()
225   }
226 }