9 use actix_web::web::Data;
11 use lemmy_apub::ActorType;
13 diesel_option_overwrite,
15 source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
17 comment_view::CommentQueryBuilder,
19 community_follower_view::CommunityFollowerView,
20 community_moderator_view::CommunityModeratorView,
21 community_view::{CommunityQueryBuilder, CommunityView},
23 user_view::UserViewSafe,
32 use lemmy_structs::{blocking, community::*};
34 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
36 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
41 use lemmy_websocket::{
42 messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
46 use std::str::FromStr;
48 #[async_trait::async_trait(?Send)]
49 impl Perform for GetCommunity {
50 type Response = GetCommunityResponse;
54 context: &Data<LemmyContext>,
55 _websocket_id: Option<ConnectionId>,
56 ) -> Result<GetCommunityResponse, LemmyError> {
57 let data: &GetCommunity = &self;
58 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
59 let user_id = user.map(|u| u.id);
61 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
62 let community = match data.id {
63 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
64 None => match blocking(context.pool(), move |conn| {
65 Community::read_from_name(conn, &name)
69 Ok(community) => community,
70 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
74 let community_id = community.id;
75 let community_view = match blocking(context.pool(), move |conn| {
76 CommunityView::read(conn, community_id, user_id)
80 Ok(community) => community,
81 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
84 let community_id = community.id;
85 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
86 CommunityModeratorView::for_community(conn, community_id)
90 Ok(moderators) => moderators,
91 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
96 .send(GetCommunityUsersOnline { community_id })
100 let res = GetCommunityResponse {
111 #[async_trait::async_trait(?Send)]
112 impl Perform for CreateCommunity {
113 type Response = CommunityResponse;
117 context: &Data<LemmyContext>,
118 _websocket_id: Option<ConnectionId>,
119 ) -> Result<CommunityResponse, LemmyError> {
120 let data: &CreateCommunity = &self;
121 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
123 check_slurs(&data.name)?;
124 check_slurs(&data.title)?;
125 check_slurs_opt(&data.description)?;
127 if !is_valid_community_name(&data.name) {
128 return Err(APIError::err("invalid_community_name").into());
131 // Double check for duplicate community actor_ids
132 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
133 let actor_id_cloned = actor_id.to_owned();
134 let community_dupe = blocking(context.pool(), move |conn| {
135 Community::read_from_apub_id(conn, &actor_id_cloned)
138 if community_dupe.is_ok() {
139 return Err(APIError::err("community_already_exists").into());
142 // Check to make sure the icon and banners are urls
143 let icon = diesel_option_overwrite(&data.icon);
144 let banner = diesel_option_overwrite(&data.banner);
146 check_optional_url(&icon)?;
147 check_optional_url(&banner)?;
149 // When you create a community, make sure the user becomes a moderator and a follower
150 let keypair = generate_actor_keypair()?;
152 let community_form = CommunityForm {
153 name: data.name.to_owned(),
154 title: data.title.to_owned(),
155 description: data.description.to_owned(),
158 category_id: data.category_id,
164 actor_id: Some(actor_id),
166 private_key: Some(keypair.private_key),
167 public_key: Some(keypair.public_key),
168 last_refreshed_at: None,
172 let inserted_community = match blocking(context.pool(), move |conn| {
173 Community::create(conn, &community_form)
177 Ok(community) => community,
178 Err(_e) => return Err(APIError::err("community_already_exists").into()),
181 let community_moderator_form = CommunityModeratorForm {
182 community_id: inserted_community.id,
186 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
187 if blocking(context.pool(), join).await?.is_err() {
188 return Err(APIError::err("community_moderator_already_exists").into());
191 let community_follower_form = CommunityFollowerForm {
192 community_id: inserted_community.id,
197 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
198 if blocking(context.pool(), follow).await?.is_err() {
199 return Err(APIError::err("community_follower_already_exists").into());
202 let user_id = user.id;
203 let community_view = blocking(context.pool(), move |conn| {
204 CommunityView::read(conn, inserted_community.id, Some(user_id))
208 Ok(CommunityResponse { community_view })
212 #[async_trait::async_trait(?Send)]
213 impl Perform for EditCommunity {
214 type Response = CommunityResponse;
218 context: &Data<LemmyContext>,
219 websocket_id: Option<ConnectionId>,
220 ) -> Result<CommunityResponse, LemmyError> {
221 let data: &EditCommunity = &self;
222 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
224 check_slurs(&data.title)?;
225 check_slurs_opt(&data.description)?;
227 // Verify its a mod (only mods can edit it)
228 let edit_id = data.edit_id;
229 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
230 CommunityModeratorView::for_community(conn, edit_id)
231 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
234 if !mods.contains(&user.id) {
235 return Err(APIError::err("not_a_moderator").into());
238 let edit_id = data.edit_id;
240 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
242 let icon = diesel_option_overwrite(&data.icon);
243 let banner = diesel_option_overwrite(&data.banner);
245 check_optional_url(&icon)?;
246 check_optional_url(&banner)?;
248 let community_form = CommunityForm {
249 name: read_community.name,
250 title: data.title.to_owned(),
251 description: data.description.to_owned(),
254 category_id: data.category_id.to_owned(),
255 creator_id: read_community.creator_id,
256 removed: Some(read_community.removed),
257 deleted: Some(read_community.deleted),
259 updated: Some(naive_now()),
260 actor_id: Some(read_community.actor_id),
261 local: read_community.local,
262 private_key: read_community.private_key,
263 public_key: read_community.public_key,
264 last_refreshed_at: None,
268 let edit_id = data.edit_id;
269 match blocking(context.pool(), move |conn| {
270 Community::update(conn, edit_id, &community_form)
274 Ok(community) => community,
275 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
278 // TODO there needs to be some kind of an apub update
279 // process for communities and users
281 let edit_id = data.edit_id;
282 let user_id = user.id;
283 let community_view = blocking(context.pool(), move |conn| {
284 CommunityView::read(conn, edit_id, Some(user_id))
288 let res = CommunityResponse { community_view };
290 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
296 #[async_trait::async_trait(?Send)]
297 impl Perform for DeleteCommunity {
298 type Response = CommunityResponse;
302 context: &Data<LemmyContext>,
303 websocket_id: Option<ConnectionId>,
304 ) -> Result<CommunityResponse, LemmyError> {
305 let data: &DeleteCommunity = &self;
306 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
308 // Verify its the creator (only a creator can delete the community)
309 let edit_id = data.edit_id;
311 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
312 if read_community.creator_id != user.id {
313 return Err(APIError::err("no_community_edit_allowed").into());
317 let edit_id = data.edit_id;
318 let deleted = data.deleted;
319 let updated_community = match blocking(context.pool(), move |conn| {
320 Community::update_deleted(conn, edit_id, deleted)
324 Ok(community) => community,
325 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
328 // Send apub messages
330 updated_community.send_delete(context).await?;
332 updated_community.send_undo_delete(context).await?;
335 let edit_id = data.edit_id;
336 let user_id = user.id;
337 let community_view = blocking(context.pool(), move |conn| {
338 CommunityView::read(conn, edit_id, Some(user_id))
342 let res = CommunityResponse { community_view };
344 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
350 #[async_trait::async_trait(?Send)]
351 impl Perform for RemoveCommunity {
352 type Response = CommunityResponse;
356 context: &Data<LemmyContext>,
357 websocket_id: Option<ConnectionId>,
358 ) -> Result<CommunityResponse, LemmyError> {
359 let data: &RemoveCommunity = &self;
360 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
362 // Verify its an admin (only an admin can remove a community)
363 is_admin(context.pool(), user.id).await?;
366 let edit_id = data.edit_id;
367 let removed = data.removed;
368 let updated_community = match blocking(context.pool(), move |conn| {
369 Community::update_removed(conn, edit_id, removed)
373 Ok(community) => community,
374 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
378 let expires = match data.expires {
379 Some(time) => Some(naive_from_unix(time)),
382 let form = ModRemoveCommunityForm {
383 mod_user_id: user.id,
384 community_id: data.edit_id,
385 removed: Some(removed),
386 reason: data.reason.to_owned(),
389 blocking(context.pool(), move |conn| {
390 ModRemoveCommunity::create(conn, &form)
396 updated_community.send_remove(context).await?;
398 updated_community.send_undo_remove(context).await?;
401 let edit_id = data.edit_id;
402 let user_id = user.id;
403 let community_view = blocking(context.pool(), move |conn| {
404 CommunityView::read(conn, edit_id, Some(user_id))
408 let res = CommunityResponse { community_view };
410 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
416 #[async_trait::async_trait(?Send)]
417 impl Perform for ListCommunities {
418 type Response = ListCommunitiesResponse;
422 context: &Data<LemmyContext>,
423 _websocket_id: Option<ConnectionId>,
424 ) -> Result<ListCommunitiesResponse, LemmyError> {
425 let data: &ListCommunities = &self;
426 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
428 let user_id = match &user {
429 Some(user) => Some(user.id),
433 let show_nsfw = match &user {
434 Some(user) => user.show_nsfw,
438 let sort = SortType::from_str(&data.sort)?;
440 let page = data.page;
441 let limit = data.limit;
442 let communities = blocking(context.pool(), move |conn| {
443 CommunityQueryBuilder::create(conn)
445 .show_nsfw(show_nsfw)
454 Ok(ListCommunitiesResponse { communities })
458 #[async_trait::async_trait(?Send)]
459 impl Perform for FollowCommunity {
460 type Response = CommunityResponse;
464 context: &Data<LemmyContext>,
465 _websocket_id: Option<ConnectionId>,
466 ) -> Result<CommunityResponse, LemmyError> {
467 let data: &FollowCommunity = &self;
468 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
470 let community_id = data.community_id;
471 let community = blocking(context.pool(), move |conn| {
472 Community::read(conn, community_id)
475 let community_follower_form = CommunityFollowerForm {
476 community_id: data.community_id,
483 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
484 if blocking(context.pool(), follow).await?.is_err() {
485 return Err(APIError::err("community_follower_already_exists").into());
489 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
490 if blocking(context.pool(), unfollow).await?.is_err() {
491 return Err(APIError::err("community_follower_already_exists").into());
494 } else if data.follow {
495 // Dont actually add to the community followers here, because you need
496 // to wait for the accept
497 user.send_follow(&community.actor_id()?, context).await?;
499 user.send_unfollow(&community.actor_id()?, context).await?;
500 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
501 if blocking(context.pool(), unfollow).await?.is_err() {
502 return Err(APIError::err("community_follower_already_exists").into());
506 let community_id = data.community_id;
507 let user_id = user.id;
508 let mut community_view = blocking(context.pool(), move |conn| {
509 CommunityView::read(conn, community_id, Some(user_id))
513 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
514 // For now, just assume that remote follows are accepted.
515 // Otherwise, the subscribed will be null
516 if !community.local {
517 community_view.subscribed = data.follow;
520 Ok(CommunityResponse { community_view })
524 #[async_trait::async_trait(?Send)]
525 impl Perform for GetFollowedCommunities {
526 type Response = GetFollowedCommunitiesResponse;
530 context: &Data<LemmyContext>,
531 _websocket_id: Option<ConnectionId>,
532 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
533 let data: &GetFollowedCommunities = &self;
534 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
536 let user_id = user.id;
537 let communities = match blocking(context.pool(), move |conn| {
538 CommunityFollowerView::for_user(conn, user_id)
542 Ok(communities) => communities,
543 _ => return Err(APIError::err("system_err_login").into()),
547 Ok(GetFollowedCommunitiesResponse { communities })
551 #[async_trait::async_trait(?Send)]
552 impl Perform for BanFromCommunity {
553 type Response = BanFromCommunityResponse;
557 context: &Data<LemmyContext>,
558 websocket_id: Option<ConnectionId>,
559 ) -> Result<BanFromCommunityResponse, LemmyError> {
560 let data: &BanFromCommunity = &self;
561 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
563 let community_id = data.community_id;
564 let banned_user_id = data.user_id;
566 // Verify that only mods or admins can ban
567 is_mod_or_admin(context.pool(), user.id, community_id).await?;
569 let community_user_ban_form = CommunityUserBanForm {
570 community_id: data.community_id,
571 user_id: data.user_id,
575 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
576 if blocking(context.pool(), ban).await?.is_err() {
577 return Err(APIError::err("community_user_already_banned").into());
580 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
581 if blocking(context.pool(), unban).await?.is_err() {
582 return Err(APIError::err("community_user_already_banned").into());
586 // Remove/Restore their data if that's desired
587 if let Some(remove_data) = data.remove_data {
589 blocking(context.pool(), move |conn: &'_ _| {
590 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
595 // Diesel doesn't allow updates with joins, so this has to be a loop
596 let comments = blocking(context.pool(), move |conn| {
597 CommentQueryBuilder::create(conn)
598 .creator_id(banned_user_id)
599 .community_id(community_id)
600 .limit(std::i64::MAX)
605 for comment_view in &comments {
606 let comment_id = comment_view.comment.id;
607 blocking(context.pool(), move |conn: &'_ _| {
608 Comment::update_removed(conn, comment_id, remove_data)
615 // TODO eventually do correct expires
616 let expires = match data.expires {
617 Some(time) => Some(naive_from_unix(time)),
621 let form = ModBanFromCommunityForm {
622 mod_user_id: user.id,
623 other_user_id: data.user_id,
624 community_id: data.community_id,
625 reason: data.reason.to_owned(),
626 banned: Some(data.ban),
629 blocking(context.pool(), move |conn| {
630 ModBanFromCommunity::create(conn, &form)
634 let user_id = data.user_id;
635 let user_view = blocking(context.pool(), move |conn| {
636 UserViewSafe::read(conn, user_id)
640 let res = BanFromCommunityResponse {
645 context.chat_server().do_send(SendCommunityRoomMessage {
646 op: UserOperation::BanFromCommunity,
647 response: res.clone(),
656 #[async_trait::async_trait(?Send)]
657 impl Perform for AddModToCommunity {
658 type Response = AddModToCommunityResponse;
662 context: &Data<LemmyContext>,
663 websocket_id: Option<ConnectionId>,
664 ) -> Result<AddModToCommunityResponse, LemmyError> {
665 let data: &AddModToCommunity = &self;
666 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
668 let community_moderator_form = CommunityModeratorForm {
669 community_id: data.community_id,
670 user_id: data.user_id,
673 let community_id = data.community_id;
675 // Verify that only mods or admins can add mod
676 is_mod_or_admin(context.pool(), user.id, community_id).await?;
679 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
680 if blocking(context.pool(), join).await?.is_err() {
681 return Err(APIError::err("community_moderator_already_exists").into());
684 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
685 if blocking(context.pool(), leave).await?.is_err() {
686 return Err(APIError::err("community_moderator_already_exists").into());
691 let form = ModAddCommunityForm {
692 mod_user_id: user.id,
693 other_user_id: data.user_id,
694 community_id: data.community_id,
695 removed: Some(!data.added),
697 blocking(context.pool(), move |conn| {
698 ModAddCommunity::create(conn, &form)
702 let community_id = data.community_id;
703 let moderators = blocking(context.pool(), move |conn| {
704 CommunityModeratorView::for_community(conn, community_id)
708 let res = AddModToCommunityResponse { moderators };
710 context.chat_server().do_send(SendCommunityRoomMessage {
711 op: UserOperation::AddModToCommunity,
712 response: res.clone(),
721 #[async_trait::async_trait(?Send)]
722 impl Perform for TransferCommunity {
723 type Response = GetCommunityResponse;
727 context: &Data<LemmyContext>,
728 _websocket_id: Option<ConnectionId>,
729 ) -> Result<GetCommunityResponse, LemmyError> {
730 let data: &TransferCommunity = &self;
731 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
733 let community_id = data.community_id;
734 let read_community = blocking(context.pool(), move |conn| {
735 Community::read(conn, community_id)
739 let site_creator_id = blocking(context.pool(), move |conn| {
740 Site::read(conn, 1).map(|s| s.creator_id)
744 let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
746 let creator_index = admins
748 .position(|r| r.user.id == site_creator_id)
749 .context(location_info!())?;
750 let creator_user = admins.remove(creator_index);
751 admins.insert(0, creator_user);
753 // Make sure user is the creator, or an admin
754 if user.id != read_community.creator_id
755 && !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
757 return Err(APIError::err("not_an_admin").into());
760 let community_id = data.community_id;
761 let new_creator = data.user_id;
762 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
763 if blocking(context.pool(), update).await?.is_err() {
764 return Err(APIError::err("couldnt_update_community").into());
767 // You also have to re-do the community_moderator table, reordering it.
768 let community_id = data.community_id;
769 let mut community_mods = blocking(context.pool(), move |conn| {
770 CommunityModeratorView::for_community(conn, community_id)
773 let creator_index = community_mods
775 .position(|r| r.moderator.id == data.user_id)
776 .context(location_info!())?;
777 let creator_user = community_mods.remove(creator_index);
778 community_mods.insert(0, creator_user);
780 let community_id = data.community_id;
781 blocking(context.pool(), move |conn| {
782 CommunityModerator::delete_for_community(conn, community_id)
786 // TODO: this should probably be a bulk operation
787 for cmod in &community_mods {
788 let community_moderator_form = CommunityModeratorForm {
789 community_id: cmod.community.id,
790 user_id: cmod.moderator.id,
793 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
794 if blocking(context.pool(), join).await?.is_err() {
795 return Err(APIError::err("community_moderator_already_exists").into());
800 let form = ModAddCommunityForm {
801 mod_user_id: user.id,
802 other_user_id: data.user_id,
803 community_id: data.community_id,
804 removed: Some(false),
806 blocking(context.pool(), move |conn| {
807 ModAddCommunity::create(conn, &form)
811 let community_id = data.community_id;
812 let user_id = user.id;
813 let community_view = match blocking(context.pool(), move |conn| {
814 CommunityView::read(conn, community_id, Some(user_id))
818 Ok(community) => community,
819 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
822 let community_id = data.community_id;
823 let moderators = match blocking(context.pool(), move |conn| {
824 CommunityModeratorView::for_community(conn, community_id)
828 Ok(moderators) => moderators,
829 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
833 Ok(GetCommunityResponse {
841 fn send_community_websocket(
842 res: &CommunityResponse,
843 context: &Data<LemmyContext>,
844 websocket_id: Option<ConnectionId>,
847 // TODO is there any way around this?
848 // Strip out the user id and subscribed when sending to others
849 // let mut res_sent = res.clone();
850 // res_sent.community_view.user_id = None;
851 // res_sent.community.subscribed = None;
853 context.chat_server().do_send(SendCommunityRoomMessage {
855 response: res.to_owned(),
856 community_id: res.community_view.community.id,
861 #[async_trait::async_trait(?Send)]
862 impl Perform for CommunityJoin {
863 type Response = CommunityJoinResponse;
867 context: &Data<LemmyContext>,
868 websocket_id: Option<ConnectionId>,
869 ) -> Result<CommunityJoinResponse, LemmyError> {
870 let data: &CommunityJoin = &self;
872 if let Some(ws_id) = websocket_id {
873 context.chat_server().do_send(JoinCommunityRoom {
874 community_id: data.community_id,
879 Ok(CommunityJoinResponse { joined: true })
883 #[async_trait::async_trait(?Send)]
884 impl Perform for ModJoin {
885 type Response = ModJoinResponse;
889 context: &Data<LemmyContext>,
890 websocket_id: Option<ConnectionId>,
891 ) -> Result<ModJoinResponse, LemmyError> {
892 let data: &ModJoin = &self;
894 if let Some(ws_id) = websocket_id {
895 context.chat_server().do_send(JoinModRoom {
896 community_id: data.community_id,
901 Ok(ModJoinResponse { joined: true })