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