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