]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
Move most code into crates/ subfolder
[lemmy.git] / crates / api / src / site.rs
1 use crate::{
2   get_user_from_jwt,
3   get_user_from_jwt_opt,
4   get_user_safe_settings_from_jwt,
5   get_user_safe_settings_from_jwt_opt,
6   is_admin,
7   linked_instances,
8   version,
9   Perform,
10 };
11 use actix_web::web::Data;
12 use anyhow::Context;
13 use lemmy_apub::fetcher::search::search_by_apub_id;
14 use lemmy_db_queries::{
15   diesel_option_overwrite,
16   source::{category::Category_, site::Site_},
17   Crud,
18   SearchType,
19   SortType,
20 };
21 use lemmy_db_schema::{
22   naive_now,
23   source::{
24     category::Category,
25     moderator::*,
26     site::{Site, *},
27   },
28 };
29 use lemmy_db_views::{
30   comment_view::CommentQueryBuilder,
31   post_view::PostQueryBuilder,
32   site_view::SiteView,
33 };
34 use lemmy_db_views_actor::{
35   community_view::CommunityQueryBuilder,
36   user_view::{UserQueryBuilder, UserViewSafe},
37 };
38 use lemmy_db_views_moderator::{
39   mod_add_community_view::ModAddCommunityView,
40   mod_add_view::ModAddView,
41   mod_ban_from_community_view::ModBanFromCommunityView,
42   mod_ban_view::ModBanView,
43   mod_lock_post_view::ModLockPostView,
44   mod_remove_comment_view::ModRemoveCommentView,
45   mod_remove_community_view::ModRemoveCommunityView,
46   mod_remove_post_view::ModRemovePostView,
47   mod_sticky_post_view::ModStickyPostView,
48 };
49 use lemmy_structs::{blocking, site::*, user::Register};
50 use lemmy_utils::{
51   location_info,
52   settings::Settings,
53   utils::{check_slurs, check_slurs_opt},
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
328     Ok(GetSiteResponse {
329       site_view,
330       admins,
331       banned,
332       online,
333       version: version::VERSION.to_string(),
334       my_user,
335       federated_instances: linked_instances(context.pool()).await?,
336     })
337   }
338 }
339
340 #[async_trait::async_trait(?Send)]
341 impl Perform for Search {
342   type Response = SearchResponse;
343
344   async fn perform(
345     &self,
346     context: &Data<LemmyContext>,
347     _websocket_id: Option<ConnectionId>,
348   ) -> Result<SearchResponse, LemmyError> {
349     let data: &Search = &self;
350
351     match search_by_apub_id(&data.q, context).await {
352       Ok(r) => return Ok(r),
353       Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
354     }
355
356     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
357     let user_id = user.map(|u| u.id);
358
359     let type_ = SearchType::from_str(&data.type_)?;
360
361     let mut posts = Vec::new();
362     let mut comments = Vec::new();
363     let mut communities = Vec::new();
364     let mut users = Vec::new();
365
366     // TODO no clean / non-nsfw searching rn
367
368     let q = data.q.to_owned();
369     let page = data.page;
370     let limit = data.limit;
371     let sort = SortType::from_str(&data.sort)?;
372     let community_id = data.community_id;
373     let community_name = data.community_name.to_owned();
374     match type_ {
375       SearchType::Posts => {
376         posts = blocking(context.pool(), move |conn| {
377           PostQueryBuilder::create(conn)
378             .sort(&sort)
379             .show_nsfw(true)
380             .community_id(community_id)
381             .community_name(community_name)
382             .my_user_id(user_id)
383             .search_term(q)
384             .page(page)
385             .limit(limit)
386             .list()
387         })
388         .await??;
389       }
390       SearchType::Comments => {
391         comments = blocking(context.pool(), move |conn| {
392           CommentQueryBuilder::create(&conn)
393             .sort(&sort)
394             .search_term(q)
395             .my_user_id(user_id)
396             .page(page)
397             .limit(limit)
398             .list()
399         })
400         .await??;
401       }
402       SearchType::Communities => {
403         communities = blocking(context.pool(), move |conn| {
404           CommunityQueryBuilder::create(conn)
405             .sort(&sort)
406             .search_term(q)
407             .page(page)
408             .limit(limit)
409             .list()
410         })
411         .await??;
412       }
413       SearchType::Users => {
414         users = blocking(context.pool(), move |conn| {
415           UserQueryBuilder::create(conn)
416             .sort(&sort)
417             .search_term(q)
418             .page(page)
419             .limit(limit)
420             .list()
421         })
422         .await??;
423       }
424       SearchType::All => {
425         posts = blocking(context.pool(), move |conn| {
426           PostQueryBuilder::create(conn)
427             .sort(&sort)
428             .show_nsfw(true)
429             .community_id(community_id)
430             .community_name(community_name)
431             .my_user_id(user_id)
432             .search_term(q)
433             .page(page)
434             .limit(limit)
435             .list()
436         })
437         .await??;
438
439         let q = data.q.to_owned();
440         let sort = SortType::from_str(&data.sort)?;
441
442         comments = blocking(context.pool(), move |conn| {
443           CommentQueryBuilder::create(conn)
444             .sort(&sort)
445             .search_term(q)
446             .my_user_id(user_id)
447             .page(page)
448             .limit(limit)
449             .list()
450         })
451         .await??;
452
453         let q = data.q.to_owned();
454         let sort = SortType::from_str(&data.sort)?;
455
456         communities = blocking(context.pool(), move |conn| {
457           CommunityQueryBuilder::create(conn)
458             .sort(&sort)
459             .search_term(q)
460             .page(page)
461             .limit(limit)
462             .list()
463         })
464         .await??;
465
466         let q = data.q.to_owned();
467         let sort = SortType::from_str(&data.sort)?;
468
469         users = blocking(context.pool(), move |conn| {
470           UserQueryBuilder::create(conn)
471             .sort(&sort)
472             .search_term(q)
473             .page(page)
474             .limit(limit)
475             .list()
476         })
477         .await??;
478       }
479       SearchType::Url => {
480         posts = blocking(context.pool(), move |conn| {
481           PostQueryBuilder::create(conn)
482             .sort(&sort)
483             .show_nsfw(true)
484             .community_id(community_id)
485             .community_name(community_name)
486             .url_search(q)
487             .page(page)
488             .limit(limit)
489             .list()
490         })
491         .await??;
492       }
493     };
494
495     // Return the jwt
496     Ok(SearchResponse {
497       type_: data.type_.to_owned(),
498       comments,
499       posts,
500       communities,
501       users,
502     })
503   }
504 }
505
506 #[async_trait::async_trait(?Send)]
507 impl Perform for TransferSite {
508   type Response = GetSiteResponse;
509
510   async fn perform(
511     &self,
512     context: &Data<LemmyContext>,
513     _websocket_id: Option<ConnectionId>,
514   ) -> Result<GetSiteResponse, LemmyError> {
515     let data: &TransferSite = &self;
516     let user = get_user_safe_settings_from_jwt(&data.auth, context.pool()).await?;
517
518     is_admin(context.pool(), user.id).await?;
519
520     let read_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
521
522     // Make sure user is the creator
523     if read_site.creator_id != user.id {
524       return Err(APIError::err("not_an_admin").into());
525     }
526
527     let new_creator_id = data.user_id;
528     let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
529     if blocking(context.pool(), transfer_site).await?.is_err() {
530       return Err(APIError::err("couldnt_update_site").into());
531     };
532
533     // Mod tables
534     let form = ModAddForm {
535       mod_user_id: user.id,
536       other_user_id: data.user_id,
537       removed: Some(false),
538     };
539
540     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
541
542     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
543
544     let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
545     let creator_index = admins
546       .iter()
547       .position(|r| r.user.id == site_view.creator.id)
548       .context(location_info!())?;
549     let creator_user = admins.remove(creator_index);
550     admins.insert(0, creator_user);
551
552     let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
553
554     Ok(GetSiteResponse {
555       site_view: Some(site_view),
556       admins,
557       banned,
558       online: 0,
559       version: version::VERSION.to_string(),
560       my_user: Some(user),
561       federated_instances: linked_instances(context.pool()).await?,
562     })
563   }
564 }
565
566 #[async_trait::async_trait(?Send)]
567 impl Perform for GetSiteConfig {
568   type Response = GetSiteConfigResponse;
569
570   async fn perform(
571     &self,
572     context: &Data<LemmyContext>,
573     _websocket_id: Option<ConnectionId>,
574   ) -> Result<GetSiteConfigResponse, LemmyError> {
575     let data: &GetSiteConfig = &self;
576     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
577
578     // Only let admins read this
579     is_admin(context.pool(), user.id).await?;
580
581     let config_hjson = Settings::read_config_file()?;
582
583     Ok(GetSiteConfigResponse { config_hjson })
584   }
585 }
586
587 #[async_trait::async_trait(?Send)]
588 impl Perform for SaveSiteConfig {
589   type Response = GetSiteConfigResponse;
590
591   async fn perform(
592     &self,
593     context: &Data<LemmyContext>,
594     _websocket_id: Option<ConnectionId>,
595   ) -> Result<GetSiteConfigResponse, LemmyError> {
596     let data: &SaveSiteConfig = &self;
597     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
598
599     // Only let admins read this
600     let user_id = user.id;
601     is_admin(context.pool(), user_id).await?;
602
603     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
604     let config_hjson = match Settings::save_config_file(&data.config_hjson) {
605       Ok(config_hjson) => config_hjson,
606       Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
607     };
608
609     Ok(GetSiteConfigResponse { config_hjson })
610   }
611 }