9 use actix_web::web::Data;
11 use lemmy_apub::ActorType;
14 comment_view::CommentQueryBuilder,
16 diesel_option_overwrite,
22 community_follower_view::CommunityFollowerView,
23 community_moderator_view::CommunityModeratorView,
24 community_view::{CommunityQueryBuilder, CommunityView},
25 user_view::UserViewSafe,
33 use lemmy_structs::{blocking, community::*};
35 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
37 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
42 use lemmy_websocket::{
43 messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
47 use std::str::FromStr;
49 #[async_trait::async_trait(?Send)]
50 impl Perform for GetCommunity {
51 type Response = GetCommunityResponse;
55 context: &Data<LemmyContext>,
56 _websocket_id: Option<ConnectionId>,
57 ) -> Result<GetCommunityResponse, LemmyError> {
58 let data: &GetCommunity = &self;
59 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
60 let user_id = user.map(|u| u.id);
62 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
63 let community = match data.id {
64 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
65 None => match blocking(context.pool(), move |conn| {
66 Community::read_from_name(conn, &name)
70 Ok(community) => community,
71 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
75 let community_id = community.id;
76 let community_view = match blocking(context.pool(), move |conn| {
77 CommunityView::read(conn, community_id, user_id)
81 Ok(community) => community,
82 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
85 let community_id = community.id;
86 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
87 CommunityModeratorView::for_community(conn, community_id)
91 Ok(moderators) => moderators,
92 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
97 .send(GetCommunityUsersOnline { community_id })
101 let res = GetCommunityResponse {
112 #[async_trait::async_trait(?Send)]
113 impl Perform for CreateCommunity {
114 type Response = CommunityResponse;
118 context: &Data<LemmyContext>,
119 _websocket_id: Option<ConnectionId>,
120 ) -> Result<CommunityResponse, LemmyError> {
121 let data: &CreateCommunity = &self;
122 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
124 check_slurs(&data.name)?;
125 check_slurs(&data.title)?;
126 check_slurs_opt(&data.description)?;
128 if !is_valid_community_name(&data.name) {
129 return Err(APIError::err("invalid_community_name").into());
132 // Double check for duplicate community actor_ids
133 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
134 let actor_id_cloned = actor_id.to_owned();
135 let community_dupe = blocking(context.pool(), move |conn| {
136 Community::read_from_actor_id(conn, &actor_id_cloned)
139 if community_dupe.is_ok() {
140 return Err(APIError::err("community_already_exists").into());
143 // Check to make sure the icon and banners are urls
144 let icon = diesel_option_overwrite(&data.icon);
145 let banner = diesel_option_overwrite(&data.banner);
147 check_optional_url(&icon)?;
148 check_optional_url(&banner)?;
150 // When you create a community, make sure the user becomes a moderator and a follower
151 let keypair = generate_actor_keypair()?;
153 let community_form = CommunityForm {
154 name: data.name.to_owned(),
155 title: data.title.to_owned(),
156 description: data.description.to_owned(),
159 category_id: data.category_id,
165 actor_id: Some(actor_id),
167 private_key: Some(keypair.private_key),
168 public_key: Some(keypair.public_key),
169 last_refreshed_at: None,
173 let inserted_community = match blocking(context.pool(), move |conn| {
174 Community::create(conn, &community_form)
178 Ok(community) => community,
179 Err(_e) => return Err(APIError::err("community_already_exists").into()),
182 let community_moderator_form = CommunityModeratorForm {
183 community_id: inserted_community.id,
187 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
188 if blocking(context.pool(), join).await?.is_err() {
189 return Err(APIError::err("community_moderator_already_exists").into());
192 let community_follower_form = CommunityFollowerForm {
193 community_id: inserted_community.id,
198 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
199 if blocking(context.pool(), follow).await?.is_err() {
200 return Err(APIError::err("community_follower_already_exists").into());
203 let user_id = user.id;
204 let community_view = blocking(context.pool(), move |conn| {
205 CommunityView::read(conn, inserted_community.id, Some(user_id))
209 Ok(CommunityResponse { community_view })
213 #[async_trait::async_trait(?Send)]
214 impl Perform for EditCommunity {
215 type Response = CommunityResponse;
219 context: &Data<LemmyContext>,
220 websocket_id: Option<ConnectionId>,
221 ) -> Result<CommunityResponse, LemmyError> {
222 let data: &EditCommunity = &self;
223 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
225 check_slurs(&data.title)?;
226 check_slurs_opt(&data.description)?;
228 // Verify its a mod (only mods can edit it)
229 let edit_id = data.edit_id;
230 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
231 CommunityModeratorView::for_community(conn, edit_id)
232 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
235 if !mods.contains(&user.id) {
236 return Err(APIError::err("not_a_moderator").into());
239 let edit_id = data.edit_id;
241 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
243 let icon = diesel_option_overwrite(&data.icon);
244 let banner = diesel_option_overwrite(&data.banner);
246 check_optional_url(&icon)?;
247 check_optional_url(&banner)?;
249 let community_form = CommunityForm {
250 name: read_community.name,
251 title: data.title.to_owned(),
252 description: data.description.to_owned(),
255 category_id: data.category_id.to_owned(),
256 creator_id: read_community.creator_id,
257 removed: Some(read_community.removed),
258 deleted: Some(read_community.deleted),
260 updated: Some(naive_now()),
261 actor_id: Some(read_community.actor_id),
262 local: read_community.local,
263 private_key: read_community.private_key,
264 public_key: read_community.public_key,
265 last_refreshed_at: None,
269 let edit_id = data.edit_id;
270 match blocking(context.pool(), move |conn| {
271 Community::update(conn, edit_id, &community_form)
275 Ok(community) => community,
276 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
279 // TODO there needs to be some kind of an apub update
280 // process for communities and users
282 let edit_id = data.edit_id;
283 let user_id = user.id;
284 let community_view = blocking(context.pool(), move |conn| {
285 CommunityView::read(conn, edit_id, Some(user_id))
289 let res = CommunityResponse { community_view };
291 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
297 #[async_trait::async_trait(?Send)]
298 impl Perform for DeleteCommunity {
299 type Response = CommunityResponse;
303 context: &Data<LemmyContext>,
304 websocket_id: Option<ConnectionId>,
305 ) -> Result<CommunityResponse, LemmyError> {
306 let data: &DeleteCommunity = &self;
307 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
309 // Verify its the creator (only a creator can delete the community)
310 let edit_id = data.edit_id;
312 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
313 if read_community.creator_id != user.id {
314 return Err(APIError::err("no_community_edit_allowed").into());
318 let edit_id = data.edit_id;
319 let deleted = data.deleted;
320 let updated_community = match blocking(context.pool(), move |conn| {
321 Community::update_deleted(conn, edit_id, deleted)
325 Ok(community) => community,
326 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
329 // Send apub messages
331 updated_community.send_delete(context).await?;
333 updated_community.send_undo_delete(context).await?;
336 let edit_id = data.edit_id;
337 let user_id = user.id;
338 let community_view = blocking(context.pool(), move |conn| {
339 CommunityView::read(conn, edit_id, Some(user_id))
343 let res = CommunityResponse { community_view };
345 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
351 #[async_trait::async_trait(?Send)]
352 impl Perform for RemoveCommunity {
353 type Response = CommunityResponse;
357 context: &Data<LemmyContext>,
358 websocket_id: Option<ConnectionId>,
359 ) -> Result<CommunityResponse, LemmyError> {
360 let data: &RemoveCommunity = &self;
361 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
363 // Verify its an admin (only an admin can remove a community)
364 is_admin(context.pool(), user.id).await?;
367 let edit_id = data.edit_id;
368 let removed = data.removed;
369 let updated_community = match blocking(context.pool(), move |conn| {
370 Community::update_removed(conn, edit_id, removed)
374 Ok(community) => community,
375 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
379 let expires = match data.expires {
380 Some(time) => Some(naive_from_unix(time)),
383 let form = ModRemoveCommunityForm {
384 mod_user_id: user.id,
385 community_id: data.edit_id,
386 removed: Some(removed),
387 reason: data.reason.to_owned(),
390 blocking(context.pool(), move |conn| {
391 ModRemoveCommunity::create(conn, &form)
397 updated_community.send_remove(context).await?;
399 updated_community.send_undo_remove(context).await?;
402 let edit_id = data.edit_id;
403 let user_id = user.id;
404 let community_view = blocking(context.pool(), move |conn| {
405 CommunityView::read(conn, edit_id, Some(user_id))
409 let res = CommunityResponse { community_view };
411 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
417 #[async_trait::async_trait(?Send)]
418 impl Perform for ListCommunities {
419 type Response = ListCommunitiesResponse;
423 context: &Data<LemmyContext>,
424 _websocket_id: Option<ConnectionId>,
425 ) -> Result<ListCommunitiesResponse, LemmyError> {
426 let data: &ListCommunities = &self;
427 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
429 let user_id = match &user {
430 Some(user) => Some(user.id),
434 let show_nsfw = match &user {
435 Some(user) => user.show_nsfw,
439 let sort = SortType::from_str(&data.sort)?;
441 let page = data.page;
442 let limit = data.limit;
443 let communities = blocking(context.pool(), move |conn| {
444 CommunityQueryBuilder::create(conn, user_id)
446 .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 .for_creator_id(banned_user_id)
599 .for_community_id(community_id)
600 .limit(std::i64::MAX)
605 for comment in &comments {
606 let comment_id = 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 })