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