]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
Move routes into separate crate to speed up compilation
[lemmy.git] / crates / api / src / site.rs
1 use crate::{
2   build_federated_instances,
3   get_user_from_jwt,
4   get_user_from_jwt_opt,
5   get_user_safe_settings_from_jwt,
6   get_user_safe_settings_from_jwt_opt,
7   is_admin,
8   Perform,
9 };
10 use actix_web::web::Data;
11 use anyhow::Context;
12 use lemmy_apub::fetcher::search::search_by_apub_id;
13 use lemmy_db_queries::{
14   diesel_option_overwrite,
15   source::{category::Category_, site::Site_},
16   Crud,
17   SearchType,
18   SortType,
19 };
20 use lemmy_db_schema::{
21   naive_now,
22   source::{
23     category::Category,
24     moderator::*,
25     site::{Site, *},
26   },
27 };
28 use lemmy_db_views::{
29   comment_view::CommentQueryBuilder,
30   post_view::PostQueryBuilder,
31   site_view::SiteView,
32 };
33 use lemmy_db_views_actor::{
34   community_view::CommunityQueryBuilder,
35   user_view::{UserQueryBuilder, UserViewSafe},
36 };
37 use lemmy_db_views_moderator::{
38   mod_add_community_view::ModAddCommunityView,
39   mod_add_view::ModAddView,
40   mod_ban_from_community_view::ModBanFromCommunityView,
41   mod_ban_view::ModBanView,
42   mod_lock_post_view::ModLockPostView,
43   mod_remove_comment_view::ModRemoveCommentView,
44   mod_remove_community_view::ModRemoveCommunityView,
45   mod_remove_post_view::ModRemovePostView,
46   mod_sticky_post_view::ModStickyPostView,
47 };
48 use lemmy_structs::{blocking, site::*, user::Register};
49 use lemmy_utils::{
50   location_info,
51   settings::Settings,
52   utils::{check_slurs, check_slurs_opt},
53   version,
54   APIError,
55   ConnectionId,
56   LemmyError,
57 };
58 use lemmy_websocket::{
59   messages::{GetUsersOnline, SendAllMessage},
60   LemmyContext,
61   UserOperation,
62 };
63 use log::{debug, info};
64 use std::str::FromStr;
65
66 #[async_trait::async_trait(?Send)]
67 impl Perform for ListCategories {
68   type Response = ListCategoriesResponse;
69
70   async fn perform(
71     &self,
72     context: &Data<LemmyContext>,
73     _websocket_id: Option<ConnectionId>,
74   ) -> Result<ListCategoriesResponse, LemmyError> {
75     let _data: &ListCategories = &self;
76
77     let categories = blocking(context.pool(), move |conn| Category::list_all(conn)).await??;
78
79     // Return the jwt
80     Ok(ListCategoriesResponse { categories })
81   }
82 }
83
84 #[async_trait::async_trait(?Send)]
85 impl Perform for GetModlog {
86   type Response = GetModlogResponse;
87
88   async fn perform(
89     &self,
90     context: &Data<LemmyContext>,
91     _websocket_id: Option<ConnectionId>,
92   ) -> Result<GetModlogResponse, LemmyError> {
93     let data: &GetModlog = &self;
94
95     let community_id = data.community_id;
96     let mod_user_id = data.mod_user_id;
97     let page = data.page;
98     let limit = data.limit;
99     let removed_posts = blocking(context.pool(), move |conn| {
100       ModRemovePostView::list(conn, community_id, mod_user_id, page, limit)
101     })
102     .await??;
103
104     let locked_posts = blocking(context.pool(), move |conn| {
105       ModLockPostView::list(conn, community_id, mod_user_id, page, limit)
106     })
107     .await??;
108
109     let stickied_posts = blocking(context.pool(), move |conn| {
110       ModStickyPostView::list(conn, community_id, mod_user_id, page, limit)
111     })
112     .await??;
113
114     let removed_comments = blocking(context.pool(), move |conn| {
115       ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit)
116     })
117     .await??;
118
119     let banned_from_community = blocking(context.pool(), move |conn| {
120       ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit)
121     })
122     .await??;
123
124     let added_to_community = blocking(context.pool(), move |conn| {
125       ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit)
126     })
127     .await??;
128
129     // These arrays are only for the full modlog, when a community isn't given
130     let (removed_communities, banned, added) = if data.community_id.is_none() {
131       blocking(context.pool(), move |conn| {
132         Ok((
133           ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?,
134           ModBanView::list(conn, mod_user_id, page, limit)?,
135           ModAddView::list(conn, mod_user_id, page, limit)?,
136         )) as Result<_, LemmyError>
137       })
138       .await??
139     } else {
140       (Vec::new(), Vec::new(), Vec::new())
141     };
142
143     // Return the jwt
144     Ok(GetModlogResponse {
145       removed_posts,
146       locked_posts,
147       stickied_posts,
148       removed_comments,
149       removed_communities,
150       banned_from_community,
151       banned,
152       added_to_community,
153       added,
154     })
155   }
156 }
157
158 #[async_trait::async_trait(?Send)]
159 impl Perform for CreateSite {
160   type Response = SiteResponse;
161
162   async fn perform(
163     &self,
164     context: &Data<LemmyContext>,
165     _websocket_id: Option<ConnectionId>,
166   ) -> Result<SiteResponse, LemmyError> {
167     let data: &CreateSite = &self;
168
169     let read_site = move |conn: &'_ _| Site::read_simple(conn);
170     if blocking(context.pool(), read_site).await?.is_ok() {
171       return Err(APIError::err("site_already_exists").into());
172     };
173
174     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
175
176     check_slurs(&data.name)?;
177     check_slurs_opt(&data.description)?;
178
179     // Make sure user is an admin
180     is_admin(context.pool(), user.id).await?;
181
182     let site_form = SiteForm {
183       name: data.name.to_owned(),
184       description: data.description.to_owned(),
185       icon: Some(data.icon.to_owned()),
186       banner: Some(data.banner.to_owned()),
187       creator_id: user.id,
188       enable_downvotes: data.enable_downvotes,
189       open_registration: data.open_registration,
190       enable_nsfw: data.enable_nsfw,
191       updated: None,
192     };
193
194     let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
195     if blocking(context.pool(), create_site).await?.is_err() {
196       return Err(APIError::err("site_already_exists").into());
197     }
198
199     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
200
201     Ok(SiteResponse { site_view })
202   }
203 }
204
205 #[async_trait::async_trait(?Send)]
206 impl Perform for EditSite {
207   type Response = SiteResponse;
208   async fn perform(
209     &self,
210     context: &Data<LemmyContext>,
211     websocket_id: Option<ConnectionId>,
212   ) -> Result<SiteResponse, LemmyError> {
213     let data: &EditSite = &self;
214     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
215
216     check_slurs(&data.name)?;
217     check_slurs_opt(&data.description)?;
218
219     // Make sure user is an admin
220     is_admin(context.pool(), user.id).await?;
221
222     let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
223
224     let icon = diesel_option_overwrite(&data.icon);
225     let banner = diesel_option_overwrite(&data.banner);
226
227     let site_form = SiteForm {
228       name: data.name.to_owned(),
229       description: data.description.to_owned(),
230       icon,
231       banner,
232       creator_id: found_site.creator_id,
233       updated: Some(naive_now()),
234       enable_downvotes: data.enable_downvotes,
235       open_registration: data.open_registration,
236       enable_nsfw: data.enable_nsfw,
237     };
238
239     let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
240     if blocking(context.pool(), update_site).await?.is_err() {
241       return Err(APIError::err("couldnt_update_site").into());
242     }
243
244     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
245
246     let res = SiteResponse { site_view };
247
248     context.chat_server().do_send(SendAllMessage {
249       op: UserOperation::EditSite,
250       response: res.clone(),
251       websocket_id,
252     });
253
254     Ok(res)
255   }
256 }
257
258 #[async_trait::async_trait(?Send)]
259 impl Perform for GetSite {
260   type Response = GetSiteResponse;
261
262   async fn perform(
263     &self,
264     context: &Data<LemmyContext>,
265     websocket_id: Option<ConnectionId>,
266   ) -> Result<GetSiteResponse, LemmyError> {
267     let data: &GetSite = &self;
268
269     let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
270       Ok(site_view) => Some(site_view),
271       // If the site isn't created yet, check the setup
272       Err(_) => {
273         if let Some(setup) = Settings::get().setup.as_ref() {
274           let register = Register {
275             username: setup.admin_username.to_owned(),
276             email: setup.admin_email.to_owned(),
277             password: setup.admin_password.to_owned(),
278             password_verify: setup.admin_password.to_owned(),
279             show_nsfw: true,
280             captcha_uuid: None,
281             captcha_answer: None,
282           };
283           let login_response = register.perform(context, websocket_id).await?;
284           info!("Admin {} created", setup.admin_username);
285
286           let create_site = CreateSite {
287             name: setup.site_name.to_owned(),
288             description: None,
289             icon: None,
290             banner: None,
291             enable_downvotes: true,
292             open_registration: true,
293             enable_nsfw: true,
294             auth: login_response.jwt,
295           };
296           create_site.perform(context, websocket_id).await?;
297           info!("Site {} created", setup.site_name);
298           Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
299         } else {
300           None
301         }
302       }
303     };
304
305     let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
306
307     // Make sure the site creator is the top admin
308     if let Some(site_view) = site_view.to_owned() {
309       let site_creator_id = site_view.creator.id;
310       // TODO investigate why this is sometimes coming back null
311       // Maybe user_.admin isn't being set to true?
312       if let Some(creator_index) = admins.iter().position(|r| r.user.id == site_creator_id) {
313         let creator_user = admins.remove(creator_index);
314         admins.insert(0, creator_user);
315       }
316     }
317
318     let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
319
320     let online = context
321       .chat_server()
322       .send(GetUsersOnline)
323       .await
324       .unwrap_or(1);
325
326     let my_user = get_user_safe_settings_from_jwt_opt(&data.auth, context.pool()).await?;
327     let federated_instances = build_federated_instances(context.pool()).await?;
328
329     Ok(GetSiteResponse {
330       site_view,
331       admins,
332       banned,
333       online,
334       version: version::VERSION.to_string(),
335       my_user,
336       federated_instances,
337     })
338   }
339 }
340
341 #[async_trait::async_trait(?Send)]
342 impl Perform for Search {
343   type Response = SearchResponse;
344
345   async fn perform(
346     &self,
347     context: &Data<LemmyContext>,
348     _websocket_id: Option<ConnectionId>,
349   ) -> Result<SearchResponse, LemmyError> {
350     let data: &Search = &self;
351
352     match search_by_apub_id(&data.q, context).await {
353       Ok(r) => return Ok(r),
354       Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
355     }
356
357     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
358     let user_id = user.map(|u| u.id);
359
360     let type_ = SearchType::from_str(&data.type_)?;
361
362     let mut posts = Vec::new();
363     let mut comments = Vec::new();
364     let mut communities = Vec::new();
365     let mut users = Vec::new();
366
367     // TODO no clean / non-nsfw searching rn
368
369     let q = data.q.to_owned();
370     let page = data.page;
371     let limit = data.limit;
372     let sort = SortType::from_str(&data.sort)?;
373     let community_id = data.community_id;
374     let community_name = data.community_name.to_owned();
375     match type_ {
376       SearchType::Posts => {
377         posts = blocking(context.pool(), move |conn| {
378           PostQueryBuilder::create(conn)
379             .sort(&sort)
380             .show_nsfw(true)
381             .community_id(community_id)
382             .community_name(community_name)
383             .my_user_id(user_id)
384             .search_term(q)
385             .page(page)
386             .limit(limit)
387             .list()
388         })
389         .await??;
390       }
391       SearchType::Comments => {
392         comments = blocking(context.pool(), move |conn| {
393           CommentQueryBuilder::create(&conn)
394             .sort(&sort)
395             .search_term(q)
396             .my_user_id(user_id)
397             .page(page)
398             .limit(limit)
399             .list()
400         })
401         .await??;
402       }
403       SearchType::Communities => {
404         communities = blocking(context.pool(), move |conn| {
405           CommunityQueryBuilder::create(conn)
406             .sort(&sort)
407             .search_term(q)
408             .my_user_id(user_id)
409             .page(page)
410             .limit(limit)
411             .list()
412         })
413         .await??;
414       }
415       SearchType::Users => {
416         users = blocking(context.pool(), move |conn| {
417           UserQueryBuilder::create(conn)
418             .sort(&sort)
419             .search_term(q)
420             .page(page)
421             .limit(limit)
422             .list()
423         })
424         .await??;
425       }
426       SearchType::All => {
427         posts = blocking(context.pool(), move |conn| {
428           PostQueryBuilder::create(conn)
429             .sort(&sort)
430             .show_nsfw(true)
431             .community_id(community_id)
432             .community_name(community_name)
433             .my_user_id(user_id)
434             .search_term(q)
435             .page(page)
436             .limit(limit)
437             .list()
438         })
439         .await??;
440
441         let q = data.q.to_owned();
442         let sort = SortType::from_str(&data.sort)?;
443
444         comments = blocking(context.pool(), move |conn| {
445           CommentQueryBuilder::create(conn)
446             .sort(&sort)
447             .search_term(q)
448             .my_user_id(user_id)
449             .page(page)
450             .limit(limit)
451             .list()
452         })
453         .await??;
454
455         let q = data.q.to_owned();
456         let sort = SortType::from_str(&data.sort)?;
457
458         communities = blocking(context.pool(), move |conn| {
459           CommunityQueryBuilder::create(conn)
460             .sort(&sort)
461             .search_term(q)
462             .my_user_id(user_id)
463             .page(page)
464             .limit(limit)
465             .list()
466         })
467         .await??;
468
469         let q = data.q.to_owned();
470         let sort = SortType::from_str(&data.sort)?;
471
472         users = blocking(context.pool(), move |conn| {
473           UserQueryBuilder::create(conn)
474             .sort(&sort)
475             .search_term(q)
476             .page(page)
477             .limit(limit)
478             .list()
479         })
480         .await??;
481       }
482       SearchType::Url => {
483         posts = blocking(context.pool(), move |conn| {
484           PostQueryBuilder::create(conn)
485             .sort(&sort)
486             .show_nsfw(true)
487             .my_user_id(user_id)
488             .community_id(community_id)
489             .community_name(community_name)
490             .url_search(q)
491             .page(page)
492             .limit(limit)
493             .list()
494         })
495         .await??;
496       }
497     };
498
499     // Return the jwt
500     Ok(SearchResponse {
501       type_: data.type_.to_owned(),
502       comments,
503       posts,
504       communities,
505       users,
506     })
507   }
508 }
509
510 #[async_trait::async_trait(?Send)]
511 impl Perform for TransferSite {
512   type Response = GetSiteResponse;
513
514   async fn perform(
515     &self,
516     context: &Data<LemmyContext>,
517     _websocket_id: Option<ConnectionId>,
518   ) -> Result<GetSiteResponse, LemmyError> {
519     let data: &TransferSite = &self;
520     let user = get_user_safe_settings_from_jwt(&data.auth, context.pool()).await?;
521
522     is_admin(context.pool(), user.id).await?;
523
524     let read_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
525
526     // Make sure user is the creator
527     if read_site.creator_id != user.id {
528       return Err(APIError::err("not_an_admin").into());
529     }
530
531     let new_creator_id = data.user_id;
532     let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
533     if blocking(context.pool(), transfer_site).await?.is_err() {
534       return Err(APIError::err("couldnt_update_site").into());
535     };
536
537     // Mod tables
538     let form = ModAddForm {
539       mod_user_id: user.id,
540       other_user_id: data.user_id,
541       removed: Some(false),
542     };
543
544     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
545
546     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
547
548     let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
549     let creator_index = admins
550       .iter()
551       .position(|r| r.user.id == site_view.creator.id)
552       .context(location_info!())?;
553     let creator_user = admins.remove(creator_index);
554     admins.insert(0, creator_user);
555
556     let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
557     let federated_instances = build_federated_instances(context.pool()).await?;
558
559     Ok(GetSiteResponse {
560       site_view: Some(site_view),
561       admins,
562       banned,
563       online: 0,
564       version: version::VERSION.to_string(),
565       my_user: Some(user),
566       federated_instances,
567     })
568   }
569 }
570
571 #[async_trait::async_trait(?Send)]
572 impl Perform for GetSiteConfig {
573   type Response = GetSiteConfigResponse;
574
575   async fn perform(
576     &self,
577     context: &Data<LemmyContext>,
578     _websocket_id: Option<ConnectionId>,
579   ) -> Result<GetSiteConfigResponse, LemmyError> {
580     let data: &GetSiteConfig = &self;
581     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
582
583     // Only let admins read this
584     is_admin(context.pool(), user.id).await?;
585
586     let config_hjson = Settings::read_config_file()?;
587
588     Ok(GetSiteConfigResponse { config_hjson })
589   }
590 }
591
592 #[async_trait::async_trait(?Send)]
593 impl Perform for SaveSiteConfig {
594   type Response = GetSiteConfigResponse;
595
596   async fn perform(
597     &self,
598     context: &Data<LemmyContext>,
599     _websocket_id: Option<ConnectionId>,
600   ) -> Result<GetSiteConfigResponse, LemmyError> {
601     let data: &SaveSiteConfig = &self;
602     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
603
604     // Only let admins read this
605     let user_id = user.id;
606     is_admin(context.pool(), user_id).await?;
607
608     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
609     let config_hjson = match Settings::save_config_file(&data.config_hjson) {
610       Ok(config_hjson) => config_hjson,
611       Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
612     };
613
614     Ok(GetSiteConfigResponse { config_hjson })
615   }
616 }