]> Untitled Git - lemmy.git/blob - crates/db_views_actor/src/community_view.rs
improve admin and mod check to not do seq scans and return unnecessary data (#3483)
[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 =
94       CommunityModeratorView::is_community_moderator(pool, community_id, person_id).await?;
95     if is_mod {
96       return Ok(true);
97     }
98
99     PersonView::is_admin(pool, person_id).await
100   }
101 }
102
103 #[derive(TypedBuilder)]
104 #[builder(field_defaults(default))]
105 pub struct CommunityQuery<'a> {
106   #[builder(!default)]
107   pool: &'a DbPool,
108   listing_type: Option<ListingType>,
109   sort: Option<SortType>,
110   local_user: Option<&'a LocalUser>,
111   search_term: Option<String>,
112   is_mod_or_admin: Option<bool>,
113   show_nsfw: Option<bool>,
114   page: Option<i64>,
115   limit: Option<i64>,
116 }
117
118 impl<'a> CommunityQuery<'a> {
119   pub async fn list(self) -> Result<Vec<CommunityView>, Error> {
120     use SortType::*;
121
122     let conn = &mut get_conn(self.pool).await?;
123
124     // The left join below will return None in this case
125     let person_id_join = self.local_user.map(|l| l.person_id).unwrap_or(PersonId(-1));
126
127     let mut query = community::table
128       .inner_join(community_aggregates::table)
129       .left_join(local_user::table.on(local_user::person_id.eq(person_id_join)))
130       .left_join(
131         community_follower::table.on(
132           community::id
133             .eq(community_follower::community_id)
134             .and(community_follower::person_id.eq(person_id_join)),
135         ),
136       )
137       .left_join(
138         community_block::table.on(
139           community::id
140             .eq(community_block::community_id)
141             .and(community_block::person_id.eq(person_id_join)),
142         ),
143       )
144       .select((
145         community::all_columns,
146         community_aggregates::all_columns,
147         community_follower::all_columns.nullable(),
148         community_block::all_columns.nullable(),
149       ))
150       .into_boxed();
151
152     if let Some(search_term) = self.search_term {
153       let searcher = fuzzy_search(&search_term);
154       query = query
155         .filter(community::name.ilike(searcher.clone()))
156         .or_filter(community::title.ilike(searcher));
157     };
158
159     // Hide deleted and removed for non-admins or mods
160     if !self.is_mod_or_admin.unwrap_or(false) {
161       query = query
162         .filter(community::removed.eq(false))
163         .filter(community::deleted.eq(false))
164         .filter(
165           community::hidden
166             .eq(false)
167             .or(community_follower::person_id.eq(person_id_join)),
168         );
169     }
170     match self.sort.unwrap_or(Hot) {
171       Hot | Active => {
172         query = query
173           .filter(community_aggregates::hot_rank.gt(1))
174           .order_by(community_aggregates::hot_rank.desc())
175       }
176       NewComments | TopDay | TopTwelveHour | TopSixHour | TopHour => {
177         query = query.order_by(community_aggregates::users_active_day.desc())
178       }
179       New => query = query.order_by(community::published.desc()),
180       Old => query = query.order_by(community::published.asc()),
181       MostComments => query = query.order_by(community_aggregates::comments.desc()),
182       TopAll | TopYear | TopNineMonths => {
183         query = query.order_by(community_aggregates::subscribers.desc())
184       }
185       TopSixMonths | TopThreeMonths => {
186         query = query.order_by(community_aggregates::users_active_half_year.desc())
187       }
188       TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()),
189       TopWeek => query = query.order_by(community_aggregates::users_active_week.desc()),
190     };
191
192     if let Some(listing_type) = self.listing_type {
193       query = match listing_type {
194         ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
195         ListingType::Local => query.filter(community::local.eq(true)),
196         _ => query,
197       };
198     }
199
200     // Don't show blocked communities or nsfw communities if not enabled in profile
201     if self.local_user.is_some() {
202       query = query.filter(community_block::person_id.is_null());
203       query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true)));
204     } else {
205       // No person in request, only show nsfw communities if show_nsfw is passed into request
206       if !self.show_nsfw.unwrap_or(false) {
207         query = query.filter(community::nsfw.eq(false));
208       }
209     }
210
211     let (limit, offset) = limit_and_offset(self.page, self.limit)?;
212     let res = query
213       .limit(limit)
214       .offset(offset)
215       .load::<CommunityViewTuple>(conn)
216       .await?;
217
218     Ok(res.into_iter().map(CommunityView::from_tuple).collect())
219   }
220 }
221
222 impl JoinView for CommunityView {
223   type JoinTuple = CommunityViewTuple;
224   fn from_tuple(a: Self::JoinTuple) -> Self {
225     Self {
226       community: a.0,
227       counts: a.1,
228       subscribed: CommunityFollower::to_subscribed_type(&a.2),
229       blocked: a.3.is_some(),
230     }
231   }
232 }