]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
Rework error handling (fixes #1714) (#2135)
[lemmy.git] / crates / api / src / site.rs
1 use crate::Perform;
2 use actix_web::web::Data;
3 use diesel::NotFound;
4 use lemmy_api_common::{
5   blocking,
6   build_federated_instances,
7   check_private_instance,
8   get_local_user_view_from_jwt,
9   get_local_user_view_from_jwt_opt,
10   is_admin,
11   resolve_actor_identifier,
12   send_application_approved_email,
13   site::*,
14 };
15 use lemmy_apub::fetcher::search::{search_by_apub_id, SearchableObjects};
16 use lemmy_db_schema::{
17   diesel_option_overwrite,
18   from_opt_str_to_opt_enum,
19   newtypes::PersonId,
20   source::{
21     community::Community,
22     local_user::{LocalUser, LocalUserForm},
23     moderator::*,
24     person::Person,
25     registration_application::{RegistrationApplication, RegistrationApplicationForm},
26     site::Site,
27   },
28   traits::{Crud, DeleteableOrRemoveable},
29   DbPool,
30   ListingType,
31   SearchType,
32   SortType,
33 };
34 use lemmy_db_views::{
35   comment_view::{CommentQueryBuilder, CommentView},
36   local_user_view::LocalUserView,
37   post_view::{PostQueryBuilder, PostView},
38   registration_application_view::{
39     RegistrationApplicationQueryBuilder,
40     RegistrationApplicationView,
41   },
42   site_view::SiteView,
43 };
44 use lemmy_db_views_actor::{
45   community_view::{CommunityQueryBuilder, CommunityView},
46   person_view::{PersonQueryBuilder, PersonViewSafe},
47 };
48 use lemmy_db_views_moderator::{
49   mod_add_community_view::ModAddCommunityView,
50   mod_add_view::ModAddView,
51   mod_ban_from_community_view::ModBanFromCommunityView,
52   mod_ban_view::ModBanView,
53   mod_hide_community_view::ModHideCommunityView,
54   mod_lock_post_view::ModLockPostView,
55   mod_remove_comment_view::ModRemoveCommentView,
56   mod_remove_community_view::ModRemoveCommunityView,
57   mod_remove_post_view::ModRemovePostView,
58   mod_sticky_post_view::ModStickyPostView,
59   mod_transfer_community_view::ModTransferCommunityView,
60 };
61 use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError};
62 use lemmy_websocket::LemmyContext;
63
64 #[async_trait::async_trait(?Send)]
65 impl Perform for GetModlog {
66   type Response = GetModlogResponse;
67
68   #[tracing::instrument(skip(context, _websocket_id))]
69   async fn perform(
70     &self,
71     context: &Data<LemmyContext>,
72     _websocket_id: Option<ConnectionId>,
73   ) -> Result<GetModlogResponse, LemmyError> {
74     let data: &GetModlog = self;
75
76     let local_user_view =
77       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
78         .await?;
79
80     check_private_instance(&local_user_view, context.pool()).await?;
81
82     let community_id = data.community_id;
83     let mod_person_id = data.mod_person_id;
84     let page = data.page;
85     let limit = data.limit;
86     let removed_posts = blocking(context.pool(), move |conn| {
87       ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
88     })
89     .await??;
90
91     let locked_posts = blocking(context.pool(), move |conn| {
92       ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
93     })
94     .await??;
95
96     let stickied_posts = blocking(context.pool(), move |conn| {
97       ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
98     })
99     .await??;
100
101     let removed_comments = blocking(context.pool(), move |conn| {
102       ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
103     })
104     .await??;
105
106     let banned_from_community = blocking(context.pool(), move |conn| {
107       ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
108     })
109     .await??;
110
111     let added_to_community = blocking(context.pool(), move |conn| {
112       ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
113     })
114     .await??;
115
116     let transferred_to_community = blocking(context.pool(), move |conn| {
117       ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit)
118     })
119     .await??;
120
121     let hidden_communities = blocking(context.pool(), move |conn| {
122       ModHideCommunityView::list(conn, community_id, mod_person_id, page, limit)
123     })
124     .await??;
125
126     // These arrays are only for the full modlog, when a community isn't given
127     let (removed_communities, banned, added) = if data.community_id.is_none() {
128       blocking(context.pool(), move |conn| {
129         Ok((
130           ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
131           ModBanView::list(conn, mod_person_id, page, limit)?,
132           ModAddView::list(conn, mod_person_id, page, limit)?,
133         )) as Result<_, LemmyError>
134       })
135       .await??
136     } else {
137       (Vec::new(), Vec::new(), Vec::new())
138     };
139
140     // Return the jwt
141     Ok(GetModlogResponse {
142       removed_posts,
143       locked_posts,
144       stickied_posts,
145       removed_comments,
146       removed_communities,
147       banned_from_community,
148       banned,
149       added_to_community,
150       added,
151       transferred_to_community,
152       hidden_communities,
153     })
154   }
155 }
156
157 #[async_trait::async_trait(?Send)]
158 impl Perform for Search {
159   type Response = SearchResponse;
160
161   #[tracing::instrument(skip(context, _websocket_id))]
162   async fn perform(
163     &self,
164     context: &Data<LemmyContext>,
165     _websocket_id: Option<ConnectionId>,
166   ) -> Result<SearchResponse, LemmyError> {
167     let data: &Search = self;
168
169     let local_user_view =
170       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
171         .await?;
172
173     check_private_instance(&local_user_view, context.pool()).await?;
174
175     let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
176     let show_bot_accounts = local_user_view
177       .as_ref()
178       .map(|t| t.local_user.show_bot_accounts);
179     let show_read_posts = local_user_view
180       .as_ref()
181       .map(|t| t.local_user.show_read_posts);
182
183     let person_id = local_user_view.map(|u| u.person.id);
184
185     let mut posts = Vec::new();
186     let mut comments = Vec::new();
187     let mut communities = Vec::new();
188     let mut users = Vec::new();
189
190     // TODO no clean / non-nsfw searching rn
191
192     let q = data.q.to_owned();
193     let page = data.page;
194     let limit = data.limit;
195     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
196     let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.listing_type);
197     let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All);
198     let community_id = data.community_id;
199     let community_actor_id = if let Some(name) = &data.community_name {
200       resolve_actor_identifier::<Community>(name, context.pool())
201         .await
202         .ok()
203         .map(|c| c.actor_id)
204     } else {
205       None
206     };
207     let creator_id = data.creator_id;
208     match search_type {
209       SearchType::Posts => {
210         posts = blocking(context.pool(), move |conn| {
211           PostQueryBuilder::create(conn)
212             .sort(sort)
213             .show_nsfw(show_nsfw)
214             .show_bot_accounts(show_bot_accounts)
215             .show_read_posts(show_read_posts)
216             .listing_type(listing_type)
217             .community_id(community_id)
218             .community_actor_id(community_actor_id)
219             .creator_id(creator_id)
220             .my_person_id(person_id)
221             .search_term(q)
222             .page(page)
223             .limit(limit)
224             .list()
225         })
226         .await??;
227       }
228       SearchType::Comments => {
229         comments = blocking(context.pool(), move |conn| {
230           CommentQueryBuilder::create(conn)
231             .sort(sort)
232             .listing_type(listing_type)
233             .search_term(q)
234             .show_bot_accounts(show_bot_accounts)
235             .community_id(community_id)
236             .community_actor_id(community_actor_id)
237             .creator_id(creator_id)
238             .my_person_id(person_id)
239             .page(page)
240             .limit(limit)
241             .list()
242         })
243         .await??;
244       }
245       SearchType::Communities => {
246         communities = blocking(context.pool(), move |conn| {
247           CommunityQueryBuilder::create(conn)
248             .sort(sort)
249             .listing_type(listing_type)
250             .search_term(q)
251             .my_person_id(person_id)
252             .page(page)
253             .limit(limit)
254             .list()
255         })
256         .await??;
257       }
258       SearchType::Users => {
259         users = blocking(context.pool(), move |conn| {
260           PersonQueryBuilder::create(conn)
261             .sort(sort)
262             .search_term(q)
263             .page(page)
264             .limit(limit)
265             .list()
266         })
267         .await??;
268       }
269       SearchType::All => {
270         // If the community or creator is included, dont search communities or users
271         let community_or_creator_included =
272           data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
273         let community_actor_id_2 = community_actor_id.to_owned();
274
275         posts = blocking(context.pool(), move |conn| {
276           PostQueryBuilder::create(conn)
277             .sort(sort)
278             .show_nsfw(show_nsfw)
279             .show_bot_accounts(show_bot_accounts)
280             .show_read_posts(show_read_posts)
281             .listing_type(listing_type)
282             .community_id(community_id)
283             .community_actor_id(community_actor_id_2)
284             .creator_id(creator_id)
285             .my_person_id(person_id)
286             .search_term(q)
287             .page(page)
288             .limit(limit)
289             .list()
290         })
291         .await??;
292
293         let q = data.q.to_owned();
294         let community_actor_id = community_actor_id.to_owned();
295
296         comments = blocking(context.pool(), move |conn| {
297           CommentQueryBuilder::create(conn)
298             .sort(sort)
299             .listing_type(listing_type)
300             .search_term(q)
301             .show_bot_accounts(show_bot_accounts)
302             .community_id(community_id)
303             .community_actor_id(community_actor_id)
304             .creator_id(creator_id)
305             .my_person_id(person_id)
306             .page(page)
307             .limit(limit)
308             .list()
309         })
310         .await??;
311
312         let q = data.q.to_owned();
313
314         communities = if community_or_creator_included {
315           vec![]
316         } else {
317           blocking(context.pool(), move |conn| {
318             CommunityQueryBuilder::create(conn)
319               .sort(sort)
320               .listing_type(listing_type)
321               .search_term(q)
322               .my_person_id(person_id)
323               .page(page)
324               .limit(limit)
325               .list()
326           })
327           .await??
328         };
329
330         let q = data.q.to_owned();
331
332         users = if community_or_creator_included {
333           vec![]
334         } else {
335           blocking(context.pool(), move |conn| {
336             PersonQueryBuilder::create(conn)
337               .sort(sort)
338               .search_term(q)
339               .page(page)
340               .limit(limit)
341               .list()
342           })
343           .await??
344         };
345       }
346       SearchType::Url => {
347         posts = blocking(context.pool(), move |conn| {
348           PostQueryBuilder::create(conn)
349             .sort(sort)
350             .show_nsfw(show_nsfw)
351             .show_bot_accounts(show_bot_accounts)
352             .show_read_posts(show_read_posts)
353             .listing_type(listing_type)
354             .my_person_id(person_id)
355             .community_id(community_id)
356             .community_actor_id(community_actor_id)
357             .creator_id(creator_id)
358             .url_search(q)
359             .page(page)
360             .limit(limit)
361             .list()
362         })
363         .await??;
364       }
365     };
366
367     // Blank out deleted or removed info for non logged in users
368     if person_id.is_none() {
369       for cv in communities
370         .iter_mut()
371         .filter(|cv| cv.community.deleted || cv.community.removed)
372       {
373         cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
374       }
375
376       for pv in posts
377         .iter_mut()
378         .filter(|p| p.post.deleted || p.post.removed)
379       {
380         pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
381       }
382
383       for cv in comments
384         .iter_mut()
385         .filter(|cv| cv.comment.deleted || cv.comment.removed)
386       {
387         cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
388       }
389     }
390
391     // Return the jwt
392     Ok(SearchResponse {
393       type_: search_type.to_string(),
394       comments,
395       posts,
396       communities,
397       users,
398     })
399   }
400 }
401
402 #[async_trait::async_trait(?Send)]
403 impl Perform for ResolveObject {
404   type Response = ResolveObjectResponse;
405
406   #[tracing::instrument(skip(context, _websocket_id))]
407   async fn perform(
408     &self,
409     context: &Data<LemmyContext>,
410     _websocket_id: Option<ConnectionId>,
411   ) -> Result<ResolveObjectResponse, LemmyError> {
412     let local_user_view =
413       get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
414         .await?;
415     check_private_instance(&local_user_view, context.pool()).await?;
416
417     let res = search_by_apub_id(&self.q, context)
418       .await
419       .map_err(|e| e.with_message("couldnt_find_object"))?;
420     convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
421       .await
422       .map_err(|e| e.with_message("couldnt_find_object"))
423   }
424 }
425
426 async fn convert_response(
427   object: SearchableObjects,
428   user_id: Option<PersonId>,
429   pool: &DbPool,
430 ) -> Result<ResolveObjectResponse, LemmyError> {
431   let removed_or_deleted;
432   let mut res = ResolveObjectResponse {
433     comment: None,
434     post: None,
435     community: None,
436     person: None,
437   };
438   use SearchableObjects::*;
439   match object {
440     Person(p) => {
441       removed_or_deleted = p.deleted;
442       res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
443     }
444     Community(c) => {
445       removed_or_deleted = c.deleted || c.removed;
446       res.community =
447         Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
448     }
449     Post(p) => {
450       removed_or_deleted = p.deleted || p.removed;
451       res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
452     }
453     Comment(c) => {
454       removed_or_deleted = c.deleted || c.removed;
455       res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
456     }
457   };
458   // if the object was deleted from database, dont return it
459   if removed_or_deleted {
460     return Err(NotFound {}.into());
461   }
462   Ok(res)
463 }
464
465 #[async_trait::async_trait(?Send)]
466 impl Perform for LeaveAdmin {
467   type Response = GetSiteResponse;
468
469   #[tracing::instrument(skip(context, _websocket_id))]
470   async fn perform(
471     &self,
472     context: &Data<LemmyContext>,
473     _websocket_id: Option<ConnectionId>,
474   ) -> Result<GetSiteResponse, LemmyError> {
475     let data: &LeaveAdmin = self;
476     let local_user_view =
477       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
478
479     is_admin(&local_user_view)?;
480
481     // Make sure there isn't just one admin (so if one leaves, there will still be one left)
482     let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
483     if admins.len() == 1 {
484       return Err(LemmyError::from_message("cannot_leave_admin"));
485     }
486
487     let person_id = local_user_view.person.id;
488     blocking(context.pool(), move |conn| {
489       Person::leave_admin(conn, person_id)
490     })
491     .await??;
492
493     // Mod tables
494     let form = ModAddForm {
495       mod_person_id: person_id,
496       other_person_id: person_id,
497       removed: Some(true),
498     };
499
500     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
501
502     // Reread site and admins
503     let site_view = blocking(context.pool(), SiteView::read_local).await??;
504     let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
505
506     let federated_instances = build_federated_instances(
507       context.pool(),
508       &context.settings().federation,
509       &context.settings().hostname,
510     )
511     .await?;
512
513     Ok(GetSiteResponse {
514       site_view: Some(site_view),
515       admins,
516       online: 0,
517       version: version::VERSION.to_string(),
518       my_user: None,
519       federated_instances,
520     })
521   }
522 }
523
524 #[async_trait::async_trait(?Send)]
525 impl Perform for GetSiteConfig {
526   type Response = GetSiteConfigResponse;
527
528   #[tracing::instrument(skip(context, _websocket_id))]
529   async fn perform(
530     &self,
531     context: &Data<LemmyContext>,
532     _websocket_id: Option<ConnectionId>,
533   ) -> Result<GetSiteConfigResponse, LemmyError> {
534     let data: &GetSiteConfig = self;
535     let local_user_view =
536       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
537
538     // Only let admins read this
539     is_admin(&local_user_view)?;
540
541     let config_hjson = Settings::read_config_file()?;
542
543     Ok(GetSiteConfigResponse { config_hjson })
544   }
545 }
546
547 #[async_trait::async_trait(?Send)]
548 impl Perform for SaveSiteConfig {
549   type Response = GetSiteConfigResponse;
550
551   #[tracing::instrument(skip(context, _websocket_id))]
552   async fn perform(
553     &self,
554     context: &Data<LemmyContext>,
555     _websocket_id: Option<ConnectionId>,
556   ) -> Result<GetSiteConfigResponse, LemmyError> {
557     let data: &SaveSiteConfig = self;
558     let local_user_view =
559       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
560
561     // Only let admins read this
562     is_admin(&local_user_view)?;
563
564     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
565     let config_hjson = Settings::save_config_file(&data.config_hjson)
566       .map_err(|e| e.with_message("couldnt_update_site"))?;
567
568     Ok(GetSiteConfigResponse { config_hjson })
569   }
570 }
571
572 /// Lists registration applications, filterable by undenied only.
573 #[async_trait::async_trait(?Send)]
574 impl Perform for ListRegistrationApplications {
575   type Response = ListRegistrationApplicationsResponse;
576
577   async fn perform(
578     &self,
579     context: &Data<LemmyContext>,
580     _websocket_id: Option<ConnectionId>,
581   ) -> Result<Self::Response, LemmyError> {
582     let data = self;
583     let local_user_view =
584       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
585
586     // Make sure user is an admin
587     is_admin(&local_user_view)?;
588
589     let unread_only = data.unread_only;
590     let verified_email_only = blocking(context.pool(), Site::read_local_site)
591       .await??
592       .require_email_verification;
593
594     let page = data.page;
595     let limit = data.limit;
596     let registration_applications = blocking(context.pool(), move |conn| {
597       RegistrationApplicationQueryBuilder::create(conn)
598         .unread_only(unread_only)
599         .verified_email_only(verified_email_only)
600         .page(page)
601         .limit(limit)
602         .list()
603     })
604     .await??;
605
606     let res = Self::Response {
607       registration_applications,
608     };
609
610     Ok(res)
611   }
612 }
613
614 #[async_trait::async_trait(?Send)]
615 impl Perform for ApproveRegistrationApplication {
616   type Response = RegistrationApplicationResponse;
617
618   async fn perform(
619     &self,
620     context: &Data<LemmyContext>,
621     _websocket_id: Option<ConnectionId>,
622   ) -> Result<Self::Response, LemmyError> {
623     let data = self;
624     let local_user_view =
625       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
626
627     let app_id = data.id;
628
629     // Only let admins do this
630     is_admin(&local_user_view)?;
631
632     // Update the registration with reason, admin_id
633     let deny_reason = diesel_option_overwrite(&data.deny_reason);
634     let app_form = RegistrationApplicationForm {
635       admin_id: Some(local_user_view.person.id),
636       deny_reason,
637       ..RegistrationApplicationForm::default()
638     };
639
640     let registration_application = blocking(context.pool(), move |conn| {
641       RegistrationApplication::update(conn, app_id, &app_form)
642     })
643     .await??;
644
645     // Update the local_user row
646     let local_user_form = LocalUserForm {
647       accepted_application: Some(data.approve),
648       ..LocalUserForm::default()
649     };
650
651     let approved_user_id = registration_application.local_user_id;
652     blocking(context.pool(), move |conn| {
653       LocalUser::update(conn, approved_user_id, &local_user_form)
654     })
655     .await??;
656
657     if data.approve {
658       let approved_local_user_view = blocking(context.pool(), move |conn| {
659         LocalUserView::read(conn, approved_user_id)
660       })
661       .await??;
662
663       if approved_local_user_view.local_user.email.is_some() {
664         send_application_approved_email(&approved_local_user_view, &context.settings())?;
665       }
666     }
667
668     // Read the view
669     let registration_application = blocking(context.pool(), move |conn| {
670       RegistrationApplicationView::read(conn, app_id)
671     })
672     .await??;
673
674     Ok(Self::Response {
675       registration_application,
676     })
677   }
678 }
679
680 #[async_trait::async_trait(?Send)]
681 impl Perform for GetUnreadRegistrationApplicationCount {
682   type Response = GetUnreadRegistrationApplicationCountResponse;
683
684   async fn perform(
685     &self,
686     context: &Data<LemmyContext>,
687     _websocket_id: Option<ConnectionId>,
688   ) -> Result<Self::Response, LemmyError> {
689     let data = self;
690     let local_user_view =
691       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
692
693     // Only let admins do this
694     is_admin(&local_user_view)?;
695
696     let verified_email_only = blocking(context.pool(), Site::read_local_site)
697       .await??
698       .require_email_verification;
699
700     let registration_applications = blocking(context.pool(), move |conn| {
701       RegistrationApplicationView::get_unread_count(conn, verified_email_only)
702     })
703     .await??;
704
705     Ok(Self::Response {
706       registration_applications,
707     })
708   }
709 }