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,
34 use lemmy_structs::{blocking, community::*};
36 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
38 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
43 use lemmy_websocket::{
44 messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
48 use std::str::FromStr;
50 #[async_trait::async_trait(?Send)]
51 impl Perform for GetCommunity {
52 type Response = GetCommunityResponse;
56 context: &Data<LemmyContext>,
57 _websocket_id: Option<ConnectionId>,
58 ) -> Result<GetCommunityResponse, LemmyError> {
59 let data: &GetCommunity = &self;
60 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
61 let user_id = user.map(|u| u.id);
63 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
64 let community = match data.id {
65 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
66 None => match blocking(context.pool(), move |conn| {
67 Community::read_from_name(conn, &name)
71 Ok(community) => community,
72 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
76 let community_id = community.id;
77 let community_view = match blocking(context.pool(), move |conn| {
78 CommunityView::read(conn, community_id, user_id)
82 Ok(community) => community,
83 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
86 let community_id = community.id;
87 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
88 CommunityModeratorView::for_community(conn, community_id)
92 Ok(moderators) => moderators,
93 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
98 .send(GetCommunityUsersOnline { community_id })
102 let res = GetCommunityResponse {
113 #[async_trait::async_trait(?Send)]
114 impl Perform for CreateCommunity {
115 type Response = CommunityResponse;
119 context: &Data<LemmyContext>,
120 _websocket_id: Option<ConnectionId>,
121 ) -> Result<CommunityResponse, LemmyError> {
122 let data: &CreateCommunity = &self;
123 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
125 check_slurs(&data.name)?;
126 check_slurs(&data.title)?;
127 check_slurs_opt(&data.description)?;
129 if !is_valid_community_name(&data.name) {
130 return Err(APIError::err("invalid_community_name").into());
133 // Double check for duplicate community actor_ids
134 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
135 let actor_id_cloned = actor_id.to_owned();
136 let community_dupe = blocking(context.pool(), move |conn| {
137 Community::read_from_apub_id(conn, &actor_id_cloned)
140 if community_dupe.is_ok() {
141 return Err(APIError::err("community_already_exists").into());
144 // Check to make sure the icon and banners are urls
145 let icon = diesel_option_overwrite(&data.icon);
146 let banner = diesel_option_overwrite(&data.banner);
148 check_optional_url(&icon)?;
149 check_optional_url(&banner)?;
151 // When you create a community, make sure the user becomes a moderator and a follower
152 let keypair = generate_actor_keypair()?;
154 let community_form = CommunityForm {
155 name: data.name.to_owned(),
156 title: data.title.to_owned(),
157 description: data.description.to_owned(),
160 category_id: data.category_id,
166 actor_id: Some(actor_id),
168 private_key: Some(keypair.private_key),
169 public_key: Some(keypair.public_key),
170 last_refreshed_at: None,
174 let inserted_community = match blocking(context.pool(), move |conn| {
175 Community::create(conn, &community_form)
179 Ok(community) => community,
180 Err(_e) => return Err(APIError::err("community_already_exists").into()),
183 let community_moderator_form = CommunityModeratorForm {
184 community_id: inserted_community.id,
188 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
189 if blocking(context.pool(), join).await?.is_err() {
190 return Err(APIError::err("community_moderator_already_exists").into());
193 let community_follower_form = CommunityFollowerForm {
194 community_id: inserted_community.id,
199 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
200 if blocking(context.pool(), follow).await?.is_err() {
201 return Err(APIError::err("community_follower_already_exists").into());
204 let user_id = user.id;
205 let community_view = blocking(context.pool(), move |conn| {
206 CommunityView::read(conn, inserted_community.id, Some(user_id))
210 Ok(CommunityResponse { community_view })
214 #[async_trait::async_trait(?Send)]
215 impl Perform for EditCommunity {
216 type Response = CommunityResponse;
220 context: &Data<LemmyContext>,
221 websocket_id: Option<ConnectionId>,
222 ) -> Result<CommunityResponse, LemmyError> {
223 let data: &EditCommunity = &self;
224 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
226 check_slurs(&data.title)?;
227 check_slurs_opt(&data.description)?;
229 // Verify its a mod (only mods can edit it)
230 let edit_id = data.edit_id;
231 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
232 CommunityModeratorView::for_community(conn, edit_id)
233 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
236 if !mods.contains(&user.id) {
237 return Err(APIError::err("not_a_moderator").into());
240 let edit_id = data.edit_id;
242 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
244 let icon = diesel_option_overwrite(&data.icon);
245 let banner = diesel_option_overwrite(&data.banner);
247 check_optional_url(&icon)?;
248 check_optional_url(&banner)?;
250 let community_form = CommunityForm {
251 name: read_community.name,
252 title: data.title.to_owned(),
253 description: data.description.to_owned(),
256 category_id: data.category_id.to_owned(),
257 creator_id: read_community.creator_id,
258 removed: Some(read_community.removed),
259 deleted: Some(read_community.deleted),
261 updated: Some(naive_now()),
262 actor_id: Some(read_community.actor_id),
263 local: read_community.local,
264 private_key: read_community.private_key,
265 public_key: read_community.public_key,
266 last_refreshed_at: None,
270 let edit_id = data.edit_id;
271 match blocking(context.pool(), move |conn| {
272 Community::update(conn, edit_id, &community_form)
276 Ok(community) => community,
277 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
280 // TODO there needs to be some kind of an apub update
281 // process for communities and users
283 let edit_id = data.edit_id;
284 let user_id = user.id;
285 let community_view = blocking(context.pool(), move |conn| {
286 CommunityView::read(conn, edit_id, Some(user_id))
290 let res = CommunityResponse { community_view };
292 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
298 #[async_trait::async_trait(?Send)]
299 impl Perform for DeleteCommunity {
300 type Response = CommunityResponse;
304 context: &Data<LemmyContext>,
305 websocket_id: Option<ConnectionId>,
306 ) -> Result<CommunityResponse, LemmyError> {
307 let data: &DeleteCommunity = &self;
308 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
310 // Verify its the creator (only a creator can delete the community)
311 let edit_id = data.edit_id;
313 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
314 if read_community.creator_id != user.id {
315 return Err(APIError::err("no_community_edit_allowed").into());
319 let edit_id = data.edit_id;
320 let deleted = data.deleted;
321 let updated_community = match blocking(context.pool(), move |conn| {
322 Community::update_deleted(conn, edit_id, deleted)
326 Ok(community) => community,
327 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
330 // Send apub messages
332 updated_community.send_delete(context).await?;
334 updated_community.send_undo_delete(context).await?;
337 let edit_id = data.edit_id;
338 let user_id = user.id;
339 let community_view = blocking(context.pool(), move |conn| {
340 CommunityView::read(conn, edit_id, Some(user_id))
344 let res = CommunityResponse { community_view };
346 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
352 #[async_trait::async_trait(?Send)]
353 impl Perform for RemoveCommunity {
354 type Response = CommunityResponse;
358 context: &Data<LemmyContext>,
359 websocket_id: Option<ConnectionId>,
360 ) -> Result<CommunityResponse, LemmyError> {
361 let data: &RemoveCommunity = &self;
362 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
364 // Verify its an admin (only an admin can remove a community)
365 is_admin(context.pool(), user.id).await?;
368 let edit_id = data.edit_id;
369 let removed = data.removed;
370 let updated_community = match blocking(context.pool(), move |conn| {
371 Community::update_removed(conn, edit_id, removed)
375 Ok(community) => community,
376 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
380 let expires = match data.expires {
381 Some(time) => Some(naive_from_unix(time)),
384 let form = ModRemoveCommunityForm {
385 mod_user_id: user.id,
386 community_id: data.edit_id,
387 removed: Some(removed),
388 reason: data.reason.to_owned(),
391 blocking(context.pool(), move |conn| {
392 ModRemoveCommunity::create(conn, &form)
398 updated_community.send_remove(context).await?;
400 updated_community.send_undo_remove(context).await?;
403 let edit_id = data.edit_id;
404 let user_id = user.id;
405 let community_view = blocking(context.pool(), move |conn| {
406 CommunityView::read(conn, edit_id, Some(user_id))
410 let res = CommunityResponse { community_view };
412 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
418 #[async_trait::async_trait(?Send)]
419 impl Perform for ListCommunities {
420 type Response = ListCommunitiesResponse;
424 context: &Data<LemmyContext>,
425 _websocket_id: Option<ConnectionId>,
426 ) -> Result<ListCommunitiesResponse, LemmyError> {
427 let data: &ListCommunities = &self;
428 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
430 let user_id = match &user {
431 Some(user) => Some(user.id),
435 let show_nsfw = match &user {
436 Some(user) => user.show_nsfw,
440 let sort = SortType::from_str(&data.sort)?;
442 let page = data.page;
443 let limit = data.limit;
444 let communities = blocking(context.pool(), move |conn| {
445 CommunityQueryBuilder::create(conn, user_id)
447 .show_nsfw(show_nsfw)
455 Ok(ListCommunitiesResponse { communities })
459 #[async_trait::async_trait(?Send)]
460 impl Perform for FollowCommunity {
461 type Response = CommunityResponse;
465 context: &Data<LemmyContext>,
466 _websocket_id: Option<ConnectionId>,
467 ) -> Result<CommunityResponse, LemmyError> {
468 let data: &FollowCommunity = &self;
469 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
471 let community_id = data.community_id;
472 let community = blocking(context.pool(), move |conn| {
473 Community::read(conn, community_id)
476 let community_follower_form = CommunityFollowerForm {
477 community_id: data.community_id,
484 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
485 if blocking(context.pool(), follow).await?.is_err() {
486 return Err(APIError::err("community_follower_already_exists").into());
490 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
491 if blocking(context.pool(), unfollow).await?.is_err() {
492 return Err(APIError::err("community_follower_already_exists").into());
495 } else if data.follow {
496 // Dont actually add to the community followers here, because you need
497 // to wait for the accept
498 user.send_follow(&community.actor_id()?, context).await?;
500 user.send_unfollow(&community.actor_id()?, context).await?;
501 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
502 if blocking(context.pool(), unfollow).await?.is_err() {
503 return Err(APIError::err("community_follower_already_exists").into());
507 let community_id = data.community_id;
508 let user_id = user.id;
509 let mut community_view = blocking(context.pool(), move |conn| {
510 CommunityView::read(conn, community_id, Some(user_id))
514 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
515 // For now, just assume that remote follows are accepted.
516 // Otherwise, the subscribed will be null
517 if !community.local {
518 community_view.subscribed = data.follow;
521 Ok(CommunityResponse { community_view })
525 #[async_trait::async_trait(?Send)]
526 impl Perform for GetFollowedCommunities {
527 type Response = GetFollowedCommunitiesResponse;
531 context: &Data<LemmyContext>,
532 _websocket_id: Option<ConnectionId>,
533 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
534 let data: &GetFollowedCommunities = &self;
535 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
537 let user_id = user.id;
538 let communities = match blocking(context.pool(), move |conn| {
539 CommunityFollowerView::for_user(conn, user_id)
543 Ok(communities) => communities,
544 _ => return Err(APIError::err("system_err_login").into()),
548 Ok(GetFollowedCommunitiesResponse { communities })
552 #[async_trait::async_trait(?Send)]
553 impl Perform for BanFromCommunity {
554 type Response = BanFromCommunityResponse;
558 context: &Data<LemmyContext>,
559 websocket_id: Option<ConnectionId>,
560 ) -> Result<BanFromCommunityResponse, LemmyError> {
561 let data: &BanFromCommunity = &self;
562 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
564 let community_id = data.community_id;
565 let banned_user_id = data.user_id;
567 // Verify that only mods or admins can ban
568 is_mod_or_admin(context.pool(), user.id, community_id).await?;
570 let community_user_ban_form = CommunityUserBanForm {
571 community_id: data.community_id,
572 user_id: data.user_id,
576 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
577 if blocking(context.pool(), ban).await?.is_err() {
578 return Err(APIError::err("community_user_already_banned").into());
581 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
582 if blocking(context.pool(), unban).await?.is_err() {
583 return Err(APIError::err("community_user_already_banned").into());
587 // Remove/Restore their data if that's desired
588 if let Some(remove_data) = data.remove_data {
590 blocking(context.pool(), move |conn: &'_ _| {
591 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
596 // Diesel doesn't allow updates with joins, so this has to be a loop
597 let comments = blocking(context.pool(), move |conn| {
598 CommentQueryBuilder::create(conn)
599 .for_creator_id(banned_user_id)
600 .for_community_id(community_id)
601 .limit(std::i64::MAX)
606 for comment in &comments {
607 let comment_id = comment.id;
608 blocking(context.pool(), move |conn: &'_ _| {
609 Comment::update_removed(conn, comment_id, remove_data)
616 // TODO eventually do correct expires
617 let expires = match data.expires {
618 Some(time) => Some(naive_from_unix(time)),
622 let form = ModBanFromCommunityForm {
623 mod_user_id: user.id,
624 other_user_id: data.user_id,
625 community_id: data.community_id,
626 reason: data.reason.to_owned(),
627 banned: Some(data.ban),
630 blocking(context.pool(), move |conn| {
631 ModBanFromCommunity::create(conn, &form)
635 let user_id = data.user_id;
636 let user_view = blocking(context.pool(), move |conn| {
637 UserViewSafe::read(conn, user_id)
641 let res = BanFromCommunityResponse {
646 context.chat_server().do_send(SendCommunityRoomMessage {
647 op: UserOperation::BanFromCommunity,
648 response: res.clone(),
657 #[async_trait::async_trait(?Send)]
658 impl Perform for AddModToCommunity {
659 type Response = AddModToCommunityResponse;
663 context: &Data<LemmyContext>,
664 websocket_id: Option<ConnectionId>,
665 ) -> Result<AddModToCommunityResponse, LemmyError> {
666 let data: &AddModToCommunity = &self;
667 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
669 let community_moderator_form = CommunityModeratorForm {
670 community_id: data.community_id,
671 user_id: data.user_id,
674 let community_id = data.community_id;
676 // Verify that only mods or admins can add mod
677 is_mod_or_admin(context.pool(), user.id, community_id).await?;
680 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
681 if blocking(context.pool(), join).await?.is_err() {
682 return Err(APIError::err("community_moderator_already_exists").into());
685 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
686 if blocking(context.pool(), leave).await?.is_err() {
687 return Err(APIError::err("community_moderator_already_exists").into());
692 let form = ModAddCommunityForm {
693 mod_user_id: user.id,
694 other_user_id: data.user_id,
695 community_id: data.community_id,
696 removed: Some(!data.added),
698 blocking(context.pool(), move |conn| {
699 ModAddCommunity::create(conn, &form)
703 let community_id = data.community_id;
704 let moderators = blocking(context.pool(), move |conn| {
705 CommunityModeratorView::for_community(conn, community_id)
709 let res = AddModToCommunityResponse { moderators };
711 context.chat_server().do_send(SendCommunityRoomMessage {
712 op: UserOperation::AddModToCommunity,
713 response: res.clone(),
722 #[async_trait::async_trait(?Send)]
723 impl Perform for TransferCommunity {
724 type Response = GetCommunityResponse;
728 context: &Data<LemmyContext>,
729 _websocket_id: Option<ConnectionId>,
730 ) -> Result<GetCommunityResponse, LemmyError> {
731 let data: &TransferCommunity = &self;
732 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
734 let community_id = data.community_id;
735 let read_community = blocking(context.pool(), move |conn| {
736 Community::read(conn, community_id)
740 let site_creator_id = blocking(context.pool(), move |conn| {
741 Site::read(conn, 1).map(|s| s.creator_id)
745 let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
747 let creator_index = admins
749 .position(|r| r.user.id == site_creator_id)
750 .context(location_info!())?;
751 let creator_user = admins.remove(creator_index);
752 admins.insert(0, creator_user);
754 // Make sure user is the creator, or an admin
755 if user.id != read_community.creator_id
756 && !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
758 return Err(APIError::err("not_an_admin").into());
761 let community_id = data.community_id;
762 let new_creator = data.user_id;
763 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
764 if blocking(context.pool(), update).await?.is_err() {
765 return Err(APIError::err("couldnt_update_community").into());
768 // You also have to re-do the community_moderator table, reordering it.
769 let community_id = data.community_id;
770 let mut community_mods = blocking(context.pool(), move |conn| {
771 CommunityModeratorView::for_community(conn, community_id)
774 let creator_index = community_mods
776 .position(|r| r.moderator.id == data.user_id)
777 .context(location_info!())?;
778 let creator_user = community_mods.remove(creator_index);
779 community_mods.insert(0, creator_user);
781 let community_id = data.community_id;
782 blocking(context.pool(), move |conn| {
783 CommunityModerator::delete_for_community(conn, community_id)
787 // TODO: this should probably be a bulk operation
788 for cmod in &community_mods {
789 let community_moderator_form = CommunityModeratorForm {
790 community_id: cmod.community.id,
791 user_id: cmod.moderator.id,
794 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
795 if blocking(context.pool(), join).await?.is_err() {
796 return Err(APIError::err("community_moderator_already_exists").into());
801 let form = ModAddCommunityForm {
802 mod_user_id: user.id,
803 other_user_id: data.user_id,
804 community_id: data.community_id,
805 removed: Some(false),
807 blocking(context.pool(), move |conn| {
808 ModAddCommunity::create(conn, &form)
812 let community_id = data.community_id;
813 let user_id = user.id;
814 let community_view = match blocking(context.pool(), move |conn| {
815 CommunityView::read(conn, community_id, Some(user_id))
819 Ok(community) => community,
820 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
823 let community_id = data.community_id;
824 let moderators = match blocking(context.pool(), move |conn| {
825 CommunityModeratorView::for_community(conn, community_id)
829 Ok(moderators) => moderators,
830 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
834 Ok(GetCommunityResponse {
842 fn send_community_websocket(
843 res: &CommunityResponse,
844 context: &Data<LemmyContext>,
845 websocket_id: Option<ConnectionId>,
848 // TODO is there any way around this?
849 // Strip out the user id and subscribed when sending to others
850 // let mut res_sent = res.clone();
851 // res_sent.community_view.user_id = None;
852 // res_sent.community.subscribed = None;
854 context.chat_server().do_send(SendCommunityRoomMessage {
856 response: res.to_owned(),
857 community_id: res.community_view.community.id,
862 #[async_trait::async_trait(?Send)]
863 impl Perform for CommunityJoin {
864 type Response = CommunityJoinResponse;
868 context: &Data<LemmyContext>,
869 websocket_id: Option<ConnectionId>,
870 ) -> Result<CommunityJoinResponse, LemmyError> {
871 let data: &CommunityJoin = &self;
873 if let Some(ws_id) = websocket_id {
874 context.chat_server().do_send(JoinCommunityRoom {
875 community_id: data.community_id,
880 Ok(CommunityJoinResponse { joined: true })
884 #[async_trait::async_trait(?Send)]
885 impl Perform for ModJoin {
886 type Response = ModJoinResponse;
890 context: &Data<LemmyContext>,
891 websocket_id: Option<ConnectionId>,
892 ) -> Result<ModJoinResponse, LemmyError> {
893 let data: &ModJoin = &self;
895 if let Some(ws_id) = websocket_id {
896 context.chat_server().do_send(JoinModRoom {
897 community_id: data.community_id,
902 Ok(ModJoinResponse { joined: true })