9 use actix_web::web::Data;
11 use lemmy_apub::ActorType;
14 comment_view::CommentQueryBuilder,
17 diesel_option_overwrite,
29 use lemmy_structs::{blocking, community::*};
31 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
33 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
38 use lemmy_websocket::{
39 messages::{GetCommunityUsersOnline, JoinCommunityRoom, SendCommunityRoomMessage},
43 use std::str::FromStr;
45 #[async_trait::async_trait(?Send)]
46 impl Perform for GetCommunity {
47 type Response = GetCommunityResponse;
51 context: &Data<LemmyContext>,
52 _websocket_id: Option<ConnectionId>,
53 ) -> Result<GetCommunityResponse, LemmyError> {
54 let data: &GetCommunity = &self;
55 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
56 let user_id = user.map(|u| u.id);
58 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
59 let community = match data.id {
60 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
61 None => match blocking(context.pool(), move |conn| {
62 Community::read_from_name(conn, &name)
66 Ok(community) => community,
67 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
71 let community_id = community.id;
72 let community_view = match blocking(context.pool(), move |conn| {
73 CommunityView::read(conn, community_id, user_id)
77 Ok(community) => community,
78 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
81 let community_id = community.id;
82 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
83 CommunityModeratorView::for_community(conn, community_id)
87 Ok(moderators) => moderators,
88 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
93 .send(GetCommunityUsersOnline { community_id })
97 let res = GetCommunityResponse {
98 community: community_view,
108 #[async_trait::async_trait(?Send)]
109 impl Perform for CreateCommunity {
110 type Response = CommunityResponse;
114 context: &Data<LemmyContext>,
115 _websocket_id: Option<ConnectionId>,
116 ) -> Result<CommunityResponse, LemmyError> {
117 let data: &CreateCommunity = &self;
118 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
120 check_slurs(&data.name)?;
121 check_slurs(&data.title)?;
122 check_slurs_opt(&data.description)?;
124 if !is_valid_community_name(&data.name) {
125 return Err(APIError::err("invalid_community_name").into());
128 // Double check for duplicate community actor_ids
129 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
130 let actor_id_cloned = actor_id.to_owned();
131 let community_dupe = blocking(context.pool(), move |conn| {
132 Community::read_from_actor_id(conn, &actor_id_cloned)
135 if community_dupe.is_ok() {
136 return Err(APIError::err("community_already_exists").into());
139 // Check to make sure the icon and banners are urls
140 let icon = diesel_option_overwrite(&data.icon);
141 let banner = diesel_option_overwrite(&data.banner);
143 check_optional_url(&icon)?;
144 check_optional_url(&banner)?;
146 // When you create a community, make sure the user becomes a moderator and a follower
147 let keypair = generate_actor_keypair()?;
149 let community_form = CommunityForm {
150 name: data.name.to_owned(),
151 title: data.title.to_owned(),
152 description: data.description.to_owned(),
155 category_id: data.category_id,
161 actor_id: Some(actor_id),
163 private_key: Some(keypair.private_key),
164 public_key: Some(keypair.public_key),
165 last_refreshed_at: None,
169 let inserted_community = match blocking(context.pool(), move |conn| {
170 Community::create(conn, &community_form)
174 Ok(community) => community,
175 Err(_e) => return Err(APIError::err("community_already_exists").into()),
178 let community_moderator_form = CommunityModeratorForm {
179 community_id: inserted_community.id,
183 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
184 if blocking(context.pool(), join).await?.is_err() {
185 return Err(APIError::err("community_moderator_already_exists").into());
188 let community_follower_form = CommunityFollowerForm {
189 community_id: inserted_community.id,
193 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
194 if blocking(context.pool(), follow).await?.is_err() {
195 return Err(APIError::err("community_follower_already_exists").into());
198 let user_id = user.id;
199 let community_view = blocking(context.pool(), move |conn| {
200 CommunityView::read(conn, inserted_community.id, Some(user_id))
204 Ok(CommunityResponse {
205 community: community_view,
210 #[async_trait::async_trait(?Send)]
211 impl Perform for EditCommunity {
212 type Response = CommunityResponse;
216 context: &Data<LemmyContext>,
217 websocket_id: Option<ConnectionId>,
218 ) -> Result<CommunityResponse, LemmyError> {
219 let data: &EditCommunity = &self;
220 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
222 check_slurs(&data.title)?;
223 check_slurs_opt(&data.description)?;
225 // Verify its a mod (only mods can edit it)
226 let edit_id = data.edit_id;
227 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
228 CommunityModeratorView::for_community(conn, edit_id)
229 .map(|v| v.into_iter().map(|m| m.user_id).collect())
232 if !mods.contains(&user.id) {
233 return Err(APIError::err("not_a_moderator").into());
236 let edit_id = data.edit_id;
238 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
240 let icon = diesel_option_overwrite(&data.icon);
241 let banner = diesel_option_overwrite(&data.banner);
243 check_optional_url(&icon)?;
244 check_optional_url(&banner)?;
246 let community_form = CommunityForm {
247 name: read_community.name,
248 title: data.title.to_owned(),
249 description: data.description.to_owned(),
252 category_id: data.category_id.to_owned(),
253 creator_id: read_community.creator_id,
254 removed: Some(read_community.removed),
255 deleted: Some(read_community.deleted),
257 updated: Some(naive_now()),
258 actor_id: Some(read_community.actor_id),
259 local: read_community.local,
260 private_key: read_community.private_key,
261 public_key: read_community.public_key,
262 last_refreshed_at: None,
266 let edit_id = data.edit_id;
267 match blocking(context.pool(), move |conn| {
268 Community::update(conn, edit_id, &community_form)
272 Ok(community) => community,
273 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
276 // TODO there needs to be some kind of an apub update
277 // process for communities and users
279 let edit_id = data.edit_id;
280 let user_id = user.id;
281 let community_view = blocking(context.pool(), move |conn| {
282 CommunityView::read(conn, edit_id, Some(user_id))
286 let res = CommunityResponse {
287 community: 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 {
343 community: 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 {
411 community: community_view,
414 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
420 #[async_trait::async_trait(?Send)]
421 impl Perform for ListCommunities {
422 type Response = ListCommunitiesResponse;
426 context: &Data<LemmyContext>,
427 _websocket_id: Option<ConnectionId>,
428 ) -> Result<ListCommunitiesResponse, LemmyError> {
429 let data: &ListCommunities = &self;
430 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
432 let user_id = match &user {
433 Some(user) => Some(user.id),
437 let show_nsfw = match &user {
438 Some(user) => user.show_nsfw,
442 let sort = SortType::from_str(&data.sort)?;
444 let page = data.page;
445 let limit = data.limit;
446 let communities = blocking(context.pool(), move |conn| {
447 CommunityQueryBuilder::create(conn)
450 .show_nsfw(show_nsfw)
458 Ok(ListCommunitiesResponse { communities })
462 #[async_trait::async_trait(?Send)]
463 impl Perform for FollowCommunity {
464 type Response = CommunityResponse;
468 context: &Data<LemmyContext>,
469 _websocket_id: Option<ConnectionId>,
470 ) -> Result<CommunityResponse, LemmyError> {
471 let data: &FollowCommunity = &self;
472 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
474 let community_id = data.community_id;
475 let community = blocking(context.pool(), move |conn| {
476 Community::read(conn, community_id)
479 let community_follower_form = CommunityFollowerForm {
480 community_id: data.community_id,
486 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
487 if blocking(context.pool(), follow).await?.is_err() {
488 return Err(APIError::err("community_follower_already_exists").into());
492 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
493 if blocking(context.pool(), unfollow).await?.is_err() {
494 return Err(APIError::err("community_follower_already_exists").into());
497 } else if data.follow {
498 // Dont actually add to the community followers here, because you need
499 // to wait for the accept
500 user.send_follow(&community.actor_id()?, context).await?;
502 user.send_unfollow(&community.actor_id()?, context).await?;
503 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
504 if blocking(context.pool(), unfollow).await?.is_err() {
505 return Err(APIError::err("community_follower_already_exists").into());
509 let community_id = data.community_id;
510 let user_id = user.id;
511 let mut community_view = blocking(context.pool(), move |conn| {
512 CommunityView::read(conn, community_id, Some(user_id))
516 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
517 // For now, just assume that remote follows are accepted.
518 // Otherwise, the subscribed will be null
519 if !community.local {
520 community_view.subscribed = Some(data.follow);
523 Ok(CommunityResponse {
524 community: community_view,
529 #[async_trait::async_trait(?Send)]
530 impl Perform for GetFollowedCommunities {
531 type Response = GetFollowedCommunitiesResponse;
535 context: &Data<LemmyContext>,
536 _websocket_id: Option<ConnectionId>,
537 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
538 let data: &GetFollowedCommunities = &self;
539 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
541 let user_id = user.id;
542 let communities = match blocking(context.pool(), move |conn| {
543 CommunityFollowerView::for_user(conn, user_id)
547 Ok(communities) => communities,
548 _ => return Err(APIError::err("system_err_login").into()),
552 Ok(GetFollowedCommunitiesResponse { communities })
556 #[async_trait::async_trait(?Send)]
557 impl Perform for BanFromCommunity {
558 type Response = BanFromCommunityResponse;
562 context: &Data<LemmyContext>,
563 websocket_id: Option<ConnectionId>,
564 ) -> Result<BanFromCommunityResponse, LemmyError> {
565 let data: &BanFromCommunity = &self;
566 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
568 let community_id = data.community_id;
569 let banned_user_id = data.user_id;
571 // Verify that only mods or admins can ban
572 is_mod_or_admin(context.pool(), user.id, community_id).await?;
574 let community_user_ban_form = CommunityUserBanForm {
575 community_id: data.community_id,
576 user_id: data.user_id,
580 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
581 if blocking(context.pool(), ban).await?.is_err() {
582 return Err(APIError::err("community_user_already_banned").into());
585 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
586 if blocking(context.pool(), unban).await?.is_err() {
587 return Err(APIError::err("community_user_already_banned").into());
591 // Remove/Restore their data if that's desired
592 if let Some(remove_data) = data.remove_data {
594 blocking(context.pool(), move |conn: &'_ _| {
595 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
600 // Diesel doesn't allow updates with joins, so this has to be a loop
601 let comments = blocking(context.pool(), move |conn| {
602 CommentQueryBuilder::create(conn)
603 .for_creator_id(banned_user_id)
604 .for_community_id(community_id)
605 .limit(std::i64::MAX)
610 for comment in &comments {
611 let comment_id = comment.id;
612 blocking(context.pool(), move |conn: &'_ _| {
613 Comment::update_removed(conn, comment_id, remove_data)
620 // TODO eventually do correct expires
621 let expires = match data.expires {
622 Some(time) => Some(naive_from_unix(time)),
626 let form = ModBanFromCommunityForm {
627 mod_user_id: user.id,
628 other_user_id: data.user_id,
629 community_id: data.community_id,
630 reason: data.reason.to_owned(),
631 banned: Some(data.ban),
634 blocking(context.pool(), move |conn| {
635 ModBanFromCommunity::create(conn, &form)
639 let user_id = data.user_id;
640 let user_view = blocking(context.pool(), move |conn| {
641 UserView::get_user_secure(conn, user_id)
645 let res = BanFromCommunityResponse {
650 context.chat_server().do_send(SendCommunityRoomMessage {
651 op: UserOperation::BanFromCommunity,
652 response: res.clone(),
661 #[async_trait::async_trait(?Send)]
662 impl Perform for AddModToCommunity {
663 type Response = AddModToCommunityResponse;
667 context: &Data<LemmyContext>,
668 websocket_id: Option<ConnectionId>,
669 ) -> Result<AddModToCommunityResponse, LemmyError> {
670 let data: &AddModToCommunity = &self;
671 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
673 let community_moderator_form = CommunityModeratorForm {
674 community_id: data.community_id,
675 user_id: data.user_id,
678 let community_id = data.community_id;
680 // Verify that only mods or admins can add mod
681 is_mod_or_admin(context.pool(), user.id, community_id).await?;
684 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
685 if blocking(context.pool(), join).await?.is_err() {
686 return Err(APIError::err("community_moderator_already_exists").into());
689 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
690 if blocking(context.pool(), leave).await?.is_err() {
691 return Err(APIError::err("community_moderator_already_exists").into());
696 let form = ModAddCommunityForm {
697 mod_user_id: user.id,
698 other_user_id: data.user_id,
699 community_id: data.community_id,
700 removed: Some(!data.added),
702 blocking(context.pool(), move |conn| {
703 ModAddCommunity::create(conn, &form)
707 let community_id = data.community_id;
708 let moderators = blocking(context.pool(), move |conn| {
709 CommunityModeratorView::for_community(conn, community_id)
713 let res = AddModToCommunityResponse { moderators };
715 context.chat_server().do_send(SendCommunityRoomMessage {
716 op: UserOperation::AddModToCommunity,
717 response: res.clone(),
726 #[async_trait::async_trait(?Send)]
727 impl Perform for TransferCommunity {
728 type Response = GetCommunityResponse;
732 context: &Data<LemmyContext>,
733 _websocket_id: Option<ConnectionId>,
734 ) -> Result<GetCommunityResponse, LemmyError> {
735 let data: &TransferCommunity = &self;
736 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
738 let community_id = data.community_id;
739 let read_community = blocking(context.pool(), move |conn| {
740 Community::read(conn, community_id)
744 let site_creator_id = blocking(context.pool(), move |conn| {
745 Site::read(conn, 1).map(|s| s.creator_id)
749 let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
751 let creator_index = admins
753 .position(|r| r.id == site_creator_id)
754 .context(location_info!())?;
755 let creator_user = admins.remove(creator_index);
756 admins.insert(0, creator_user);
758 // Make sure user is the creator, or an admin
759 if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
760 return Err(APIError::err("not_an_admin").into());
763 let community_id = data.community_id;
764 let new_creator = data.user_id;
765 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
766 if blocking(context.pool(), update).await?.is_err() {
767 return Err(APIError::err("couldnt_update_community").into());
770 // You also have to re-do the community_moderator table, reordering it.
771 let community_id = data.community_id;
772 let mut community_mods = blocking(context.pool(), move |conn| {
773 CommunityModeratorView::for_community(conn, community_id)
776 let creator_index = community_mods
778 .position(|r| r.user_id == data.user_id)
779 .context(location_info!())?;
780 let creator_user = community_mods.remove(creator_index);
781 community_mods.insert(0, creator_user);
783 let community_id = data.community_id;
784 blocking(context.pool(), move |conn| {
785 CommunityModerator::delete_for_community(conn, community_id)
789 // TODO: this should probably be a bulk operation
790 for cmod in &community_mods {
791 let community_moderator_form = CommunityModeratorForm {
792 community_id: cmod.community_id,
793 user_id: cmod.user_id,
796 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
797 if blocking(context.pool(), join).await?.is_err() {
798 return Err(APIError::err("community_moderator_already_exists").into());
803 let form = ModAddCommunityForm {
804 mod_user_id: user.id,
805 other_user_id: data.user_id,
806 community_id: data.community_id,
807 removed: Some(false),
809 blocking(context.pool(), move |conn| {
810 ModAddCommunity::create(conn, &form)
814 let community_id = data.community_id;
815 let user_id = user.id;
816 let community_view = match blocking(context.pool(), move |conn| {
817 CommunityView::read(conn, community_id, Some(user_id))
821 Ok(community) => community,
822 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
825 let community_id = data.community_id;
826 let moderators = match blocking(context.pool(), move |conn| {
827 CommunityModeratorView::for_community(conn, community_id)
831 Ok(moderators) => moderators,
832 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
836 Ok(GetCommunityResponse {
837 community: community_view,
844 pub fn send_community_websocket(
845 res: &CommunityResponse,
846 context: &Data<LemmyContext>,
847 websocket_id: Option<ConnectionId>,
850 // Strip out the user id and subscribed when sending to others
851 let mut res_sent = res.clone();
852 res_sent.community.user_id = None;
853 res_sent.community.subscribed = None;
855 context.chat_server().do_send(SendCommunityRoomMessage {
858 community_id: res.community.id,
863 #[async_trait::async_trait(?Send)]
864 impl Perform for CommunityJoin {
865 type Response = CommunityJoinResponse;
869 context: &Data<LemmyContext>,
870 websocket_id: Option<ConnectionId>,
871 ) -> Result<CommunityJoinResponse, LemmyError> {
872 let data: &CommunityJoin = &self;
874 if let Some(ws_id) = websocket_id {
875 context.chat_server().do_send(JoinCommunityRoom {
876 community_id: data.community_id,
881 Ok(CommunityJoinResponse { joined: true })