1 use crate::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform};
2 use actix_web::web::Data;
4 use lemmy_apub::ActorType;
7 comment_view::CommentQueryBuilder,
10 diesel_option_overwrite,
22 use lemmy_structs::{blocking, community::*};
24 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
26 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
31 use lemmy_websocket::{
32 messages::{GetCommunityUsersOnline, JoinCommunityRoom, SendCommunityRoomMessage},
36 use std::str::FromStr;
38 #[async_trait::async_trait(?Send)]
39 impl Perform for GetCommunity {
40 type Response = GetCommunityResponse;
44 context: &Data<LemmyContext>,
45 _websocket_id: Option<ConnectionId>,
46 ) -> Result<GetCommunityResponse, LemmyError> {
47 let data: &GetCommunity = &self;
48 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
49 let user_id = user.map(|u| u.id);
51 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
52 let community = match data.id {
53 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
54 None => match blocking(context.pool(), move |conn| {
55 Community::read_from_name(conn, &name)
59 Ok(community) => community,
60 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
64 let community_id = community.id;
65 let community_view = match blocking(context.pool(), move |conn| {
66 CommunityView::read(conn, community_id, user_id)
70 Ok(community) => community,
71 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
74 let community_id = community.id;
75 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
76 CommunityModeratorView::for_community(conn, community_id)
80 Ok(moderators) => moderators,
81 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
86 .send(GetCommunityUsersOnline { community_id })
90 let res = GetCommunityResponse {
91 community: community_view,
101 #[async_trait::async_trait(?Send)]
102 impl Perform for CreateCommunity {
103 type Response = CommunityResponse;
107 context: &Data<LemmyContext>,
108 _websocket_id: Option<ConnectionId>,
109 ) -> Result<CommunityResponse, LemmyError> {
110 let data: &CreateCommunity = &self;
111 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
113 check_slurs(&data.name)?;
114 check_slurs(&data.title)?;
115 check_slurs_opt(&data.description)?;
117 if !is_valid_community_name(&data.name) {
118 return Err(APIError::err("invalid_community_name").into());
121 // Double check for duplicate community actor_ids
122 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
123 let actor_id_cloned = actor_id.to_owned();
124 let community_dupe = blocking(context.pool(), move |conn| {
125 Community::read_from_actor_id(conn, &actor_id_cloned)
128 if community_dupe.is_ok() {
129 return Err(APIError::err("community_already_exists").into());
132 // When you create a community, make sure the user becomes a moderator and a follower
133 let keypair = generate_actor_keypair()?;
135 let community_form = CommunityForm {
136 name: data.name.to_owned(),
137 title: data.title.to_owned(),
138 description: data.description.to_owned(),
139 icon: Some(data.icon.to_owned()),
140 banner: Some(data.banner.to_owned()),
141 category_id: data.category_id,
147 actor_id: Some(actor_id),
149 private_key: Some(keypair.private_key),
150 public_key: Some(keypair.public_key),
151 last_refreshed_at: None,
155 let inserted_community = match blocking(context.pool(), move |conn| {
156 Community::create(conn, &community_form)
160 Ok(community) => community,
161 Err(_e) => return Err(APIError::err("community_already_exists").into()),
164 let community_moderator_form = CommunityModeratorForm {
165 community_id: inserted_community.id,
169 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
170 if blocking(context.pool(), join).await?.is_err() {
171 return Err(APIError::err("community_moderator_already_exists").into());
174 let community_follower_form = CommunityFollowerForm {
175 community_id: inserted_community.id,
179 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
180 if blocking(context.pool(), follow).await?.is_err() {
181 return Err(APIError::err("community_follower_already_exists").into());
184 let user_id = user.id;
185 let community_view = blocking(context.pool(), move |conn| {
186 CommunityView::read(conn, inserted_community.id, Some(user_id))
190 Ok(CommunityResponse {
191 community: community_view,
196 #[async_trait::async_trait(?Send)]
197 impl Perform for EditCommunity {
198 type Response = CommunityResponse;
202 context: &Data<LemmyContext>,
203 websocket_id: Option<ConnectionId>,
204 ) -> Result<CommunityResponse, LemmyError> {
205 let data: &EditCommunity = &self;
206 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
208 check_slurs(&data.title)?;
209 check_slurs_opt(&data.description)?;
211 // Verify its a mod (only mods can edit it)
212 let edit_id = data.edit_id;
213 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
214 CommunityModeratorView::for_community(conn, edit_id)
215 .map(|v| v.into_iter().map(|m| m.user_id).collect())
218 if !mods.contains(&user.id) {
219 return Err(APIError::err("not_a_moderator").into());
222 let edit_id = data.edit_id;
224 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
226 let icon = diesel_option_overwrite(&data.icon);
227 let banner = diesel_option_overwrite(&data.banner);
229 let community_form = CommunityForm {
230 name: read_community.name,
231 title: data.title.to_owned(),
232 description: data.description.to_owned(),
235 category_id: data.category_id.to_owned(),
236 creator_id: read_community.creator_id,
237 removed: Some(read_community.removed),
238 deleted: Some(read_community.deleted),
240 updated: Some(naive_now()),
241 actor_id: Some(read_community.actor_id),
242 local: read_community.local,
243 private_key: read_community.private_key,
244 public_key: read_community.public_key,
245 last_refreshed_at: None,
249 let edit_id = data.edit_id;
250 match blocking(context.pool(), move |conn| {
251 Community::update(conn, edit_id, &community_form)
255 Ok(community) => community,
256 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
259 // TODO there needs to be some kind of an apub update
260 // process for communities and users
262 let edit_id = data.edit_id;
263 let user_id = user.id;
264 let community_view = blocking(context.pool(), move |conn| {
265 CommunityView::read(conn, edit_id, Some(user_id))
269 let res = CommunityResponse {
270 community: community_view,
273 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
279 #[async_trait::async_trait(?Send)]
280 impl Perform for DeleteCommunity {
281 type Response = CommunityResponse;
285 context: &Data<LemmyContext>,
286 websocket_id: Option<ConnectionId>,
287 ) -> Result<CommunityResponse, LemmyError> {
288 let data: &DeleteCommunity = &self;
289 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
291 // Verify its the creator (only a creator can delete the community)
292 let edit_id = data.edit_id;
294 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
295 if read_community.creator_id != user.id {
296 return Err(APIError::err("no_community_edit_allowed").into());
300 let edit_id = data.edit_id;
301 let deleted = data.deleted;
302 let updated_community = match blocking(context.pool(), move |conn| {
303 Community::update_deleted(conn, edit_id, deleted)
307 Ok(community) => community,
308 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
311 // Send apub messages
313 updated_community.send_delete(&user, context).await?;
315 updated_community.send_undo_delete(&user, context).await?;
318 let edit_id = data.edit_id;
319 let user_id = user.id;
320 let community_view = blocking(context.pool(), move |conn| {
321 CommunityView::read(conn, edit_id, Some(user_id))
325 let res = CommunityResponse {
326 community: community_view,
329 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
335 #[async_trait::async_trait(?Send)]
336 impl Perform for RemoveCommunity {
337 type Response = CommunityResponse;
341 context: &Data<LemmyContext>,
342 websocket_id: Option<ConnectionId>,
343 ) -> Result<CommunityResponse, LemmyError> {
344 let data: &RemoveCommunity = &self;
345 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
347 // Verify its an admin (only an admin can remove a community)
348 is_admin(context.pool(), user.id).await?;
351 let edit_id = data.edit_id;
352 let removed = data.removed;
353 let updated_community = match blocking(context.pool(), move |conn| {
354 Community::update_removed(conn, edit_id, removed)
358 Ok(community) => community,
359 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
363 let expires = match data.expires {
364 Some(time) => Some(naive_from_unix(time)),
367 let form = ModRemoveCommunityForm {
368 mod_user_id: user.id,
369 community_id: data.edit_id,
370 removed: Some(removed),
371 reason: data.reason.to_owned(),
374 blocking(context.pool(), move |conn| {
375 ModRemoveCommunity::create(conn, &form)
381 updated_community.send_remove(&user, context).await?;
383 updated_community.send_undo_remove(&user, context).await?;
386 let edit_id = data.edit_id;
387 let user_id = user.id;
388 let community_view = blocking(context.pool(), move |conn| {
389 CommunityView::read(conn, edit_id, Some(user_id))
393 let res = CommunityResponse {
394 community: community_view,
397 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
403 #[async_trait::async_trait(?Send)]
404 impl Perform for ListCommunities {
405 type Response = ListCommunitiesResponse;
409 context: &Data<LemmyContext>,
410 _websocket_id: Option<ConnectionId>,
411 ) -> Result<ListCommunitiesResponse, LemmyError> {
412 let data: &ListCommunities = &self;
413 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
415 let user_id = match &user {
416 Some(user) => Some(user.id),
420 let show_nsfw = match &user {
421 Some(user) => user.show_nsfw,
425 let sort = SortType::from_str(&data.sort)?;
427 let page = data.page;
428 let limit = data.limit;
429 let communities = blocking(context.pool(), move |conn| {
430 CommunityQueryBuilder::create(conn)
433 .show_nsfw(show_nsfw)
441 Ok(ListCommunitiesResponse { communities })
445 #[async_trait::async_trait(?Send)]
446 impl Perform for FollowCommunity {
447 type Response = CommunityResponse;
451 context: &Data<LemmyContext>,
452 _websocket_id: Option<ConnectionId>,
453 ) -> Result<CommunityResponse, LemmyError> {
454 let data: &FollowCommunity = &self;
455 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
457 let community_id = data.community_id;
458 let community = blocking(context.pool(), move |conn| {
459 Community::read(conn, community_id)
462 let community_follower_form = CommunityFollowerForm {
463 community_id: data.community_id,
469 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
470 if blocking(context.pool(), follow).await?.is_err() {
471 return Err(APIError::err("community_follower_already_exists").into());
475 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
476 if blocking(context.pool(), unfollow).await?.is_err() {
477 return Err(APIError::err("community_follower_already_exists").into());
480 } else if data.follow {
481 // Dont actually add to the community followers here, because you need
482 // to wait for the accept
483 user.send_follow(&community.actor_id()?, context).await?;
485 user.send_unfollow(&community.actor_id()?, context).await?;
486 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
487 if blocking(context.pool(), unfollow).await?.is_err() {
488 return Err(APIError::err("community_follower_already_exists").into());
492 let community_id = data.community_id;
493 let user_id = user.id;
494 let mut community_view = blocking(context.pool(), move |conn| {
495 CommunityView::read(conn, community_id, Some(user_id))
499 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
500 // For now, just assume that remote follows are accepted.
501 // Otherwise, the subscribed will be null
502 if !community.local {
503 community_view.subscribed = Some(data.follow);
506 Ok(CommunityResponse {
507 community: community_view,
512 #[async_trait::async_trait(?Send)]
513 impl Perform for GetFollowedCommunities {
514 type Response = GetFollowedCommunitiesResponse;
518 context: &Data<LemmyContext>,
519 _websocket_id: Option<ConnectionId>,
520 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
521 let data: &GetFollowedCommunities = &self;
522 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
524 let user_id = user.id;
525 let communities = match blocking(context.pool(), move |conn| {
526 CommunityFollowerView::for_user(conn, user_id)
530 Ok(communities) => communities,
531 _ => return Err(APIError::err("system_err_login").into()),
535 Ok(GetFollowedCommunitiesResponse { communities })
539 #[async_trait::async_trait(?Send)]
540 impl Perform for BanFromCommunity {
541 type Response = BanFromCommunityResponse;
545 context: &Data<LemmyContext>,
546 websocket_id: Option<ConnectionId>,
547 ) -> Result<BanFromCommunityResponse, LemmyError> {
548 let data: &BanFromCommunity = &self;
549 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
551 let community_id = data.community_id;
552 let banned_user_id = data.user_id;
554 // Verify that only mods or admins can ban
555 is_mod_or_admin(context.pool(), user.id, community_id).await?;
557 let community_user_ban_form = CommunityUserBanForm {
558 community_id: data.community_id,
559 user_id: data.user_id,
563 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
564 if blocking(context.pool(), ban).await?.is_err() {
565 return Err(APIError::err("community_user_already_banned").into());
568 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
569 if blocking(context.pool(), unban).await?.is_err() {
570 return Err(APIError::err("community_user_already_banned").into());
574 // Remove/Restore their data if that's desired
575 if let Some(remove_data) = data.remove_data {
577 blocking(context.pool(), move |conn: &'_ _| {
578 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
583 // Diesel doesn't allow updates with joins, so this has to be a loop
584 let comments = blocking(context.pool(), move |conn| {
585 CommentQueryBuilder::create(conn)
586 .for_creator_id(banned_user_id)
587 .for_community_id(community_id)
588 .limit(std::i64::MAX)
593 for comment in &comments {
594 let comment_id = comment.id;
595 blocking(context.pool(), move |conn: &'_ _| {
596 Comment::update_removed(conn, comment_id, remove_data)
603 // TODO eventually do correct expires
604 let expires = match data.expires {
605 Some(time) => Some(naive_from_unix(time)),
609 let form = ModBanFromCommunityForm {
610 mod_user_id: user.id,
611 other_user_id: data.user_id,
612 community_id: data.community_id,
613 reason: data.reason.to_owned(),
614 banned: Some(data.ban),
617 blocking(context.pool(), move |conn| {
618 ModBanFromCommunity::create(conn, &form)
622 let user_id = data.user_id;
623 let user_view = blocking(context.pool(), move |conn| {
624 UserView::get_user_secure(conn, user_id)
628 let res = BanFromCommunityResponse {
633 context.chat_server().do_send(SendCommunityRoomMessage {
634 op: UserOperation::BanFromCommunity,
635 response: res.clone(),
644 #[async_trait::async_trait(?Send)]
645 impl Perform for AddModToCommunity {
646 type Response = AddModToCommunityResponse;
650 context: &Data<LemmyContext>,
651 websocket_id: Option<ConnectionId>,
652 ) -> Result<AddModToCommunityResponse, LemmyError> {
653 let data: &AddModToCommunity = &self;
654 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
656 let community_moderator_form = CommunityModeratorForm {
657 community_id: data.community_id,
658 user_id: data.user_id,
661 let community_id = data.community_id;
663 // Verify that only mods or admins can add mod
664 is_mod_or_admin(context.pool(), user.id, community_id).await?;
667 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
668 if blocking(context.pool(), join).await?.is_err() {
669 return Err(APIError::err("community_moderator_already_exists").into());
672 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
673 if blocking(context.pool(), leave).await?.is_err() {
674 return Err(APIError::err("community_moderator_already_exists").into());
679 let form = ModAddCommunityForm {
680 mod_user_id: user.id,
681 other_user_id: data.user_id,
682 community_id: data.community_id,
683 removed: Some(!data.added),
685 blocking(context.pool(), move |conn| {
686 ModAddCommunity::create(conn, &form)
690 let community_id = data.community_id;
691 let moderators = blocking(context.pool(), move |conn| {
692 CommunityModeratorView::for_community(conn, community_id)
696 let res = AddModToCommunityResponse { moderators };
698 context.chat_server().do_send(SendCommunityRoomMessage {
699 op: UserOperation::AddModToCommunity,
700 response: res.clone(),
709 #[async_trait::async_trait(?Send)]
710 impl Perform for TransferCommunity {
711 type Response = GetCommunityResponse;
715 context: &Data<LemmyContext>,
716 _websocket_id: Option<ConnectionId>,
717 ) -> Result<GetCommunityResponse, LemmyError> {
718 let data: &TransferCommunity = &self;
719 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
721 let community_id = data.community_id;
722 let read_community = blocking(context.pool(), move |conn| {
723 Community::read(conn, community_id)
727 let site_creator_id = blocking(context.pool(), move |conn| {
728 Site::read(conn, 1).map(|s| s.creator_id)
732 let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
734 let creator_index = admins
736 .position(|r| r.id == site_creator_id)
737 .context(location_info!())?;
738 let creator_user = admins.remove(creator_index);
739 admins.insert(0, creator_user);
741 // Make sure user is the creator, or an admin
742 if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
743 return Err(APIError::err("not_an_admin").into());
746 let community_id = data.community_id;
747 let new_creator = data.user_id;
748 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
749 if blocking(context.pool(), update).await?.is_err() {
750 return Err(APIError::err("couldnt_update_community").into());
753 // You also have to re-do the community_moderator table, reordering it.
754 let community_id = data.community_id;
755 let mut community_mods = blocking(context.pool(), move |conn| {
756 CommunityModeratorView::for_community(conn, community_id)
759 let creator_index = community_mods
761 .position(|r| r.user_id == data.user_id)
762 .context(location_info!())?;
763 let creator_user = community_mods.remove(creator_index);
764 community_mods.insert(0, creator_user);
766 let community_id = data.community_id;
767 blocking(context.pool(), move |conn| {
768 CommunityModerator::delete_for_community(conn, community_id)
772 // TODO: this should probably be a bulk operation
773 for cmod in &community_mods {
774 let community_moderator_form = CommunityModeratorForm {
775 community_id: cmod.community_id,
776 user_id: cmod.user_id,
779 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
780 if blocking(context.pool(), join).await?.is_err() {
781 return Err(APIError::err("community_moderator_already_exists").into());
786 let form = ModAddCommunityForm {
787 mod_user_id: user.id,
788 other_user_id: data.user_id,
789 community_id: data.community_id,
790 removed: Some(false),
792 blocking(context.pool(), move |conn| {
793 ModAddCommunity::create(conn, &form)
797 let community_id = data.community_id;
798 let user_id = user.id;
799 let community_view = match blocking(context.pool(), move |conn| {
800 CommunityView::read(conn, community_id, Some(user_id))
804 Ok(community) => community,
805 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
808 let community_id = data.community_id;
809 let moderators = match blocking(context.pool(), move |conn| {
810 CommunityModeratorView::for_community(conn, community_id)
814 Ok(moderators) => moderators,
815 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
819 Ok(GetCommunityResponse {
820 community: community_view,
827 pub fn send_community_websocket(
828 res: &CommunityResponse,
829 context: &Data<LemmyContext>,
830 websocket_id: Option<ConnectionId>,
833 // Strip out the user id and subscribed when sending to others
834 let mut res_sent = res.clone();
835 res_sent.community.user_id = None;
836 res_sent.community.subscribed = None;
838 context.chat_server().do_send(SendCommunityRoomMessage {
841 community_id: res.community.id,
846 #[async_trait::async_trait(?Send)]
847 impl Perform for CommunityJoin {
848 type Response = CommunityJoinResponse;
852 context: &Data<LemmyContext>,
853 websocket_id: Option<ConnectionId>,
854 ) -> Result<CommunityJoinResponse, LemmyError> {
855 let data: &CommunityJoin = &self;
857 if let Some(ws_id) = websocket_id {
858 context.chat_server().do_send(JoinCommunityRoom {
859 community_id: data.community_id,
864 Ok(CommunityJoinResponse { joined: true })