]> Untitled Git - lemmy.git/blob - crates/api_common/src/lib.rs
User / community blocking. Fixes #426 (#1604)
[lemmy.git] / crates / api_common / src / lib.rs
1 pub mod comment;
2 pub mod community;
3 pub mod person;
4 pub mod post;
5 pub mod site;
6 pub mod websocket;
7
8 use crate::site::FederatedInstances;
9 use diesel::PgConnection;
10 use lemmy_db_queries::{
11   source::{
12     community::{CommunityModerator_, Community_},
13     person_block::PersonBlock_,
14     site::Site_,
15   },
16   Crud,
17   DbPool,
18   Readable,
19 };
20 use lemmy_db_schema::{
21   source::{
22     comment::Comment,
23     community::{Community, CommunityModerator},
24     person::Person,
25     person_block::PersonBlock,
26     person_mention::{PersonMention, PersonMentionForm},
27     post::{Post, PostRead, PostReadForm},
28     site::Site,
29   },
30   CommunityId,
31   LocalUserId,
32   PersonId,
33   PostId,
34 };
35 use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
36 use lemmy_db_views_actor::{
37   community_person_ban_view::CommunityPersonBanView,
38   community_view::CommunityView,
39 };
40 use lemmy_utils::{
41   claims::Claims,
42   email::send_email,
43   settings::structs::Settings,
44   utils::MentionData,
45   ApiError,
46   LemmyError,
47 };
48 use log::error;
49 use serde::{Deserialize, Serialize};
50 use url::Url;
51
52 #[derive(Serialize, Deserialize, Debug)]
53 pub struct WebFingerLink {
54   pub rel: Option<String>,
55   #[serde(rename(serialize = "type", deserialize = "type"))]
56   pub type_: Option<String>,
57   pub href: Option<Url>,
58   #[serde(skip_serializing_if = "Option::is_none")]
59   pub template: Option<String>,
60 }
61
62 #[derive(Serialize, Deserialize, Debug)]
63 pub struct WebFingerResponse {
64   pub subject: String,
65   pub aliases: Vec<Url>,
66   pub links: Vec<WebFingerLink>,
67 }
68
69 pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
70 where
71   F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
72   T: Send + 'static,
73 {
74   let pool = pool.clone();
75   let res = actix_web::web::block(move || {
76     let conn = pool.get()?;
77     let res = (f)(&conn);
78     Ok(res) as Result<T, LemmyError>
79   })
80   .await?;
81
82   res
83 }
84
85 pub async fn send_local_notifs(
86   mentions: Vec<MentionData>,
87   comment: Comment,
88   person: Person,
89   post: Post,
90   pool: &DbPool,
91   do_send_email: bool,
92 ) -> Result<Vec<LocalUserId>, LemmyError> {
93   let ids = blocking(pool, move |conn| {
94     do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email)
95   })
96   .await?;
97
98   Ok(ids)
99 }
100
101 fn do_send_local_notifs(
102   conn: &PgConnection,
103   mentions: &[MentionData],
104   comment: &Comment,
105   person: &Person,
106   post: &Post,
107   do_send_email: bool,
108 ) -> Vec<LocalUserId> {
109   let mut recipient_ids = Vec::new();
110
111   // Send the local mentions
112   for mention in mentions
113     .iter()
114     .filter(|m| m.is_local() && m.name.ne(&person.name))
115     .collect::<Vec<&MentionData>>()
116   {
117     if let Ok(mention_user_view) = LocalUserView::read_from_name(conn, &mention.name) {
118       // TODO
119       // At some point, make it so you can't tag the parent creator either
120       // This can cause two notifications, one for reply and the other for mention
121       recipient_ids.push(mention_user_view.local_user.id);
122
123       let user_mention_form = PersonMentionForm {
124         recipient_id: mention_user_view.person.id,
125         comment_id: comment.id,
126         read: None,
127       };
128
129       // Allow this to fail softly, since comment edits might re-update or replace it
130       // Let the uniqueness handle this fail
131       PersonMention::create(conn, &user_mention_form).ok();
132
133       // Send an email to those local users that have notifications on
134       if do_send_email {
135         send_email_to_user(
136           &mention_user_view,
137           "Mentioned by",
138           "Person Mention",
139           &comment.content,
140         )
141       }
142     }
143   }
144
145   // Send notifs to the parent commenter / poster
146   match comment.parent_id {
147     Some(parent_id) => {
148       if let Ok(parent_comment) = Comment::read(conn, parent_id) {
149         // Don't send a notif to yourself
150         if parent_comment.creator_id != person.id {
151           // Get the parent commenter local_user
152           if let Ok(parent_user_view) = LocalUserView::read_person(conn, parent_comment.creator_id)
153           {
154             recipient_ids.push(parent_user_view.local_user.id);
155
156             if do_send_email {
157               send_email_to_user(
158                 &parent_user_view,
159                 "Reply from",
160                 "Comment Reply",
161                 &comment.content,
162               )
163             }
164           }
165         }
166       }
167     }
168     // Its a post
169     None => {
170       if post.creator_id != person.id {
171         if let Ok(parent_user_view) = LocalUserView::read_person(conn, post.creator_id) {
172           recipient_ids.push(parent_user_view.local_user.id);
173
174           if do_send_email {
175             send_email_to_user(
176               &parent_user_view,
177               "Reply from",
178               "Post Reply",
179               &comment.content,
180             )
181           }
182         }
183       }
184     }
185   };
186   recipient_ids
187 }
188
189 pub fn send_email_to_user(
190   local_user_view: &LocalUserView,
191   subject_text: &str,
192   body_text: &str,
193   comment_content: &str,
194 ) {
195   if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
196     return;
197   }
198
199   if let Some(user_email) = &local_user_view.local_user.email {
200     let subject = &format!(
201       "{} - {} {}",
202       subject_text,
203       Settings::get().hostname,
204       local_user_view.person.name,
205     );
206     let html = &format!(
207       "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
208       body_text,
209       local_user_view.person.name,
210       comment_content,
211       Settings::get().get_protocol_and_hostname()
212     );
213     match send_email(subject, user_email, &local_user_view.person.name, html) {
214       Ok(_o) => _o,
215       Err(e) => error!("{}", e),
216     };
217   }
218 }
219
220 pub async fn is_mod_or_admin(
221   pool: &DbPool,
222   person_id: PersonId,
223   community_id: CommunityId,
224 ) -> Result<(), LemmyError> {
225   let is_mod_or_admin = blocking(pool, move |conn| {
226     CommunityView::is_mod_or_admin(conn, person_id, community_id)
227   })
228   .await?;
229   if !is_mod_or_admin {
230     return Err(ApiError::err("not_a_mod_or_admin").into());
231   }
232   Ok(())
233 }
234
235 pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
236   if !local_user_view.person.admin {
237     return Err(ApiError::err("not_an_admin").into());
238   }
239   Ok(())
240 }
241
242 pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
243   blocking(pool, move |conn| Post::read(conn, post_id))
244     .await?
245     .map_err(|_| ApiError::err("couldnt_find_post").into())
246 }
247
248 pub async fn mark_post_as_read(
249   person_id: PersonId,
250   post_id: PostId,
251   pool: &DbPool,
252 ) -> Result<PostRead, LemmyError> {
253   let post_read_form = PostReadForm { post_id, person_id };
254
255   blocking(pool, move |conn| {
256     PostRead::mark_as_read(conn, &post_read_form)
257   })
258   .await?
259   .map_err(|_| ApiError::err("couldnt_mark_post_as_read").into())
260 }
261
262 pub async fn get_local_user_view_from_jwt(
263   jwt: &str,
264   pool: &DbPool,
265 ) -> Result<LocalUserView, LemmyError> {
266   let claims = Claims::decode(jwt)
267     .map_err(|_| ApiError::err("not_logged_in"))?
268     .claims;
269   let local_user_id = LocalUserId(claims.sub);
270   let local_user_view =
271     blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
272   // Check for a site ban
273   if local_user_view.person.banned {
274     return Err(ApiError::err("site_ban").into());
275   }
276
277   // Check for user deletion
278   if local_user_view.person.deleted {
279     return Err(ApiError::err("deleted").into());
280   }
281
282   check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
283
284   Ok(local_user_view)
285 }
286
287 /// Checks if user's token was issued before user's password reset.
288 pub fn check_validator_time(
289   validator_time: &chrono::NaiveDateTime,
290   claims: &Claims,
291 ) -> Result<(), LemmyError> {
292   let user_validation_time = validator_time.timestamp();
293   if user_validation_time > claims.iat {
294     Err(ApiError::err("not_logged_in").into())
295   } else {
296     Ok(())
297   }
298 }
299
300 pub async fn get_local_user_view_from_jwt_opt(
301   jwt: &Option<String>,
302   pool: &DbPool,
303 ) -> Result<Option<LocalUserView>, LemmyError> {
304   match jwt {
305     Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)),
306     None => Ok(None),
307   }
308 }
309
310 pub async fn get_local_user_settings_view_from_jwt(
311   jwt: &str,
312   pool: &DbPool,
313 ) -> Result<LocalUserSettingsView, LemmyError> {
314   let claims = Claims::decode(jwt)
315     .map_err(|_| ApiError::err("not_logged_in"))?
316     .claims;
317   let local_user_id = LocalUserId(claims.sub);
318   let local_user_view = blocking(pool, move |conn| {
319     LocalUserSettingsView::read(conn, local_user_id)
320   })
321   .await??;
322   // Check for a site ban
323   if local_user_view.person.banned {
324     return Err(ApiError::err("site_ban").into());
325   }
326
327   check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
328
329   Ok(local_user_view)
330 }
331
332 pub async fn get_local_user_settings_view_from_jwt_opt(
333   jwt: &Option<String>,
334   pool: &DbPool,
335 ) -> Result<Option<LocalUserSettingsView>, LemmyError> {
336   match jwt {
337     Some(jwt) => Ok(Some(
338       get_local_user_settings_view_from_jwt(jwt, pool).await?,
339     )),
340     None => Ok(None),
341   }
342 }
343
344 pub async fn check_community_ban(
345   person_id: PersonId,
346   community_id: CommunityId,
347   pool: &DbPool,
348 ) -> Result<(), LemmyError> {
349   let is_banned =
350     move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
351   if blocking(pool, is_banned).await? {
352     Err(ApiError::err("community_ban").into())
353   } else {
354     Ok(())
355   }
356 }
357
358 pub async fn check_person_block(
359   my_id: PersonId,
360   potential_blocker_id: PersonId,
361   pool: &DbPool,
362 ) -> Result<(), LemmyError> {
363   let is_blocked = move |conn: &'_ _| PersonBlock::read(conn, potential_blocker_id, my_id).is_ok();
364   if blocking(pool, is_blocked).await? {
365     Err(ApiError::err("person_block").into())
366   } else {
367     Ok(())
368   }
369 }
370
371 pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
372   if score == -1 {
373     let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
374     if !site.enable_downvotes {
375       return Err(ApiError::err("downvotes_disabled").into());
376     }
377   }
378   Ok(())
379 }
380
381 /// Returns a list of communities that the user moderates
382 /// or if a community_id is supplied validates the user is a moderator
383 /// of that community and returns the community id in a vec
384 ///
385 /// * `person_id` - the person id of the moderator
386 /// * `community_id` - optional community id to check for moderator privileges
387 /// * `pool` - the diesel db pool
388 pub async fn collect_moderated_communities(
389   person_id: PersonId,
390   community_id: Option<CommunityId>,
391   pool: &DbPool,
392 ) -> Result<Vec<CommunityId>, LemmyError> {
393   if let Some(community_id) = community_id {
394     // if the user provides a community_id, just check for mod/admin privileges
395     is_mod_or_admin(pool, person_id, community_id).await?;
396     Ok(vec![community_id])
397   } else {
398     let ids = blocking(pool, move |conn: &'_ _| {
399       CommunityModerator::get_person_moderated_communities(conn, person_id)
400     })
401     .await??;
402     Ok(ids)
403   }
404 }
405
406 pub async fn build_federated_instances(
407   pool: &DbPool,
408 ) -> Result<Option<FederatedInstances>, LemmyError> {
409   if Settings::get().federation.enabled {
410     let distinct_communities = blocking(pool, move |conn| {
411       Community::distinct_federated_communities(conn)
412     })
413     .await??;
414
415     let allowed = Settings::get().federation.allowed_instances;
416     let blocked = Settings::get().federation.blocked_instances;
417
418     let mut linked = distinct_communities
419       .iter()
420       .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
421       .collect::<Result<Vec<String>, LemmyError>>()?;
422
423     if let Some(allowed) = allowed.as_ref() {
424       linked.extend_from_slice(allowed);
425     }
426
427     if let Some(blocked) = blocked.as_ref() {
428       linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname));
429     }
430
431     // Sort and remove dupes
432     linked.sort_unstable();
433     linked.dedup();
434
435     Ok(Some(FederatedInstances {
436       linked,
437       allowed,
438       blocked,
439     }))
440   } else {
441     Ok(None)
442   }
443 }
444
445 /// Checks the password length
446 pub fn password_length_check(pass: &str) -> Result<(), LemmyError> {
447   if pass.len() > 60 {
448     Err(ApiError::err("invalid_password").into())
449   } else {
450     Ok(())
451   }
452 }
453
454 /// Checks the site description length
455 pub fn site_description_length_check(description: &str) -> Result<(), LemmyError> {
456   if description.len() > 150 {
457     Err(ApiError::err("site_description_length_overflow").into())
458   } else {
459     Ok(())
460   }
461 }