]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
Hide community v2 (#2055)
[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(LemmyError::from)
420       .map_err(|e| e.with_message("couldnt_find_object"))?;
421     convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
422       .await
423       .map_err(LemmyError::from)
424       .map_err(|e| e.with_message("couldnt_find_object"))
425   }
426 }
427
428 async fn convert_response(
429   object: SearchableObjects,
430   user_id: Option<PersonId>,
431   pool: &DbPool,
432 ) -> Result<ResolveObjectResponse, LemmyError> {
433   let removed_or_deleted;
434   let mut res = ResolveObjectResponse {
435     comment: None,
436     post: None,
437     community: None,
438     person: None,
439   };
440   use SearchableObjects::*;
441   match object {
442     Person(p) => {
443       removed_or_deleted = p.deleted;
444       res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
445     }
446     Community(c) => {
447       removed_or_deleted = c.deleted || c.removed;
448       res.community =
449         Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
450     }
451     Post(p) => {
452       removed_or_deleted = p.deleted || p.removed;
453       res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
454     }
455     Comment(c) => {
456       removed_or_deleted = c.deleted || c.removed;
457       res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
458     }
459   };
460   // if the object was deleted from database, dont return it
461   if removed_or_deleted {
462     return Err(NotFound {}.into());
463   }
464   Ok(res)
465 }
466
467 #[async_trait::async_trait(?Send)]
468 impl Perform for LeaveAdmin {
469   type Response = GetSiteResponse;
470
471   #[tracing::instrument(skip(context, _websocket_id))]
472   async fn perform(
473     &self,
474     context: &Data<LemmyContext>,
475     _websocket_id: Option<ConnectionId>,
476   ) -> Result<GetSiteResponse, LemmyError> {
477     let data: &LeaveAdmin = self;
478     let local_user_view =
479       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
480
481     is_admin(&local_user_view)?;
482
483     // Make sure there isn't just one admin (so if one leaves, there will still be one left)
484     let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
485     if admins.len() == 1 {
486       return Err(LemmyError::from_message("cannot_leave_admin"));
487     }
488
489     let person_id = local_user_view.person.id;
490     blocking(context.pool(), move |conn| {
491       Person::leave_admin(conn, person_id)
492     })
493     .await??;
494
495     // Mod tables
496     let form = ModAddForm {
497       mod_person_id: person_id,
498       other_person_id: person_id,
499       removed: Some(true),
500     };
501
502     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
503
504     // Reread site and admins
505     let site_view = blocking(context.pool(), SiteView::read).await??;
506     let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
507
508     let federated_instances = build_federated_instances(
509       context.pool(),
510       &context.settings().federation,
511       &context.settings().hostname,
512     )
513     .await?;
514
515     Ok(GetSiteResponse {
516       site_view: Some(site_view),
517       admins,
518       online: 0,
519       version: version::VERSION.to_string(),
520       my_user: None,
521       federated_instances,
522     })
523   }
524 }
525
526 #[async_trait::async_trait(?Send)]
527 impl Perform for GetSiteConfig {
528   type Response = GetSiteConfigResponse;
529
530   #[tracing::instrument(skip(context, _websocket_id))]
531   async fn perform(
532     &self,
533     context: &Data<LemmyContext>,
534     _websocket_id: Option<ConnectionId>,
535   ) -> Result<GetSiteConfigResponse, LemmyError> {
536     let data: &GetSiteConfig = self;
537     let local_user_view =
538       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
539
540     // Only let admins read this
541     is_admin(&local_user_view)?;
542
543     let config_hjson = Settings::read_config_file()?;
544
545     Ok(GetSiteConfigResponse { config_hjson })
546   }
547 }
548
549 #[async_trait::async_trait(?Send)]
550 impl Perform for SaveSiteConfig {
551   type Response = GetSiteConfigResponse;
552
553   #[tracing::instrument(skip(context, _websocket_id))]
554   async fn perform(
555     &self,
556     context: &Data<LemmyContext>,
557     _websocket_id: Option<ConnectionId>,
558   ) -> Result<GetSiteConfigResponse, LemmyError> {
559     let data: &SaveSiteConfig = self;
560     let local_user_view =
561       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
562
563     // Only let admins read this
564     is_admin(&local_user_view)?;
565
566     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
567     let config_hjson = Settings::save_config_file(&data.config_hjson)
568       .map_err(LemmyError::from)
569       .map_err(|e| e.with_message("couldnt_update_site"))?;
570
571     Ok(GetSiteConfigResponse { config_hjson })
572   }
573 }
574
575 /// Lists registration applications, filterable by undenied only.
576 #[async_trait::async_trait(?Send)]
577 impl Perform for ListRegistrationApplications {
578   type Response = ListRegistrationApplicationsResponse;
579
580   async fn perform(
581     &self,
582     context: &Data<LemmyContext>,
583     _websocket_id: Option<ConnectionId>,
584   ) -> Result<Self::Response, LemmyError> {
585     let data = self;
586     let local_user_view =
587       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
588
589     // Make sure user is an admin
590     is_admin(&local_user_view)?;
591
592     let unread_only = data.unread_only;
593     let verified_email_only = blocking(context.pool(), Site::read_local_site)
594       .await??
595       .require_email_verification;
596
597     let page = data.page;
598     let limit = data.limit;
599     let registration_applications = blocking(context.pool(), move |conn| {
600       RegistrationApplicationQueryBuilder::create(conn)
601         .unread_only(unread_only)
602         .verified_email_only(verified_email_only)
603         .page(page)
604         .limit(limit)
605         .list()
606     })
607     .await??;
608
609     let res = Self::Response {
610       registration_applications,
611     };
612
613     Ok(res)
614   }
615 }
616
617 #[async_trait::async_trait(?Send)]
618 impl Perform for ApproveRegistrationApplication {
619   type Response = RegistrationApplicationResponse;
620
621   async fn perform(
622     &self,
623     context: &Data<LemmyContext>,
624     _websocket_id: Option<ConnectionId>,
625   ) -> Result<Self::Response, LemmyError> {
626     let data = self;
627     let local_user_view =
628       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
629
630     let app_id = data.id;
631
632     // Only let admins do this
633     is_admin(&local_user_view)?;
634
635     // Update the registration with reason, admin_id
636     let deny_reason = diesel_option_overwrite(&data.deny_reason);
637     let app_form = RegistrationApplicationForm {
638       admin_id: Some(local_user_view.person.id),
639       deny_reason,
640       ..RegistrationApplicationForm::default()
641     };
642
643     let registration_application = blocking(context.pool(), move |conn| {
644       RegistrationApplication::update(conn, app_id, &app_form)
645     })
646     .await??;
647
648     // Update the local_user row
649     let local_user_form = LocalUserForm {
650       accepted_application: Some(data.approve),
651       ..LocalUserForm::default()
652     };
653
654     let approved_user_id = registration_application.local_user_id;
655     blocking(context.pool(), move |conn| {
656       LocalUser::update(conn, approved_user_id, &local_user_form)
657     })
658     .await??;
659
660     if data.approve {
661       let approved_local_user_view = blocking(context.pool(), move |conn| {
662         LocalUserView::read(conn, approved_user_id)
663       })
664       .await??;
665
666       if approved_local_user_view.local_user.email.is_some() {
667         send_application_approved_email(&approved_local_user_view, &context.settings())?;
668       }
669     }
670
671     // Read the view
672     let registration_application = blocking(context.pool(), move |conn| {
673       RegistrationApplicationView::read(conn, app_id)
674     })
675     .await??;
676
677     Ok(Self::Response {
678       registration_application,
679     })
680   }
681 }
682
683 #[async_trait::async_trait(?Send)]
684 impl Perform for GetUnreadRegistrationApplicationCount {
685   type Response = GetUnreadRegistrationApplicationCountResponse;
686
687   async fn perform(
688     &self,
689     context: &Data<LemmyContext>,
690     _websocket_id: Option<ConnectionId>,
691   ) -> Result<Self::Response, LemmyError> {
692     let data = self;
693     let local_user_view =
694       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
695
696     // Only let admins do this
697     is_admin(&local_user_view)?;
698
699     let verified_email_only = blocking(context.pool(), Site::read_local_site)
700       .await??
701       .require_email_verification;
702
703     let registration_applications = blocking(context.pool(), move |conn| {
704       RegistrationApplicationView::get_unread_count(conn, verified_email_only)
705     })
706     .await??;
707
708     Ok(Self::Response {
709       registration_applications,
710     })
711   }
712 }