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