2 api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform},
5 messages::{GetCommunityUsersOnline, JoinCommunityRoom, SendCommunityRoomMessage},
10 use actix_web::web::Data;
12 use lemmy_api_structs::{blocking, community::*};
15 comment_view::CommentQueryBuilder,
18 diesel_option_overwrite,
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 std::str::FromStr;
40 #[async_trait::async_trait(?Send)]
41 impl Perform for GetCommunity {
42 type Response = GetCommunityResponse;
46 context: &Data<LemmyContext>,
47 _websocket_id: Option<ConnectionId>,
48 ) -> Result<GetCommunityResponse, LemmyError> {
49 let data: &GetCommunity = &self;
50 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
51 let user_id = user.map(|u| u.id);
53 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
54 let community = match data.id {
55 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
56 None => match blocking(context.pool(), move |conn| {
57 Community::read_from_name(conn, &name)
61 Ok(community) => community,
62 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
66 let community_id = community.id;
67 let community_view = match blocking(context.pool(), move |conn| {
68 CommunityView::read(conn, community_id, user_id)
72 Ok(community) => community,
73 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
76 let community_id = community.id;
77 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
78 CommunityModeratorView::for_community(conn, community_id)
82 Ok(moderators) => moderators,
83 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
88 .send(GetCommunityUsersOnline { community_id })
92 let res = GetCommunityResponse {
93 community: community_view,
103 #[async_trait::async_trait(?Send)]
104 impl Perform for CreateCommunity {
105 type Response = CommunityResponse;
109 context: &Data<LemmyContext>,
110 _websocket_id: Option<ConnectionId>,
111 ) -> Result<CommunityResponse, LemmyError> {
112 let data: &CreateCommunity = &self;
113 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
115 check_slurs(&data.name)?;
116 check_slurs(&data.title)?;
117 check_slurs_opt(&data.description)?;
119 if !is_valid_community_name(&data.name) {
120 return Err(APIError::err("invalid_community_name").into());
123 // Double check for duplicate community actor_ids
124 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
125 let actor_id_cloned = actor_id.to_owned();
126 let community_dupe = blocking(context.pool(), move |conn| {
127 Community::read_from_actor_id(conn, &actor_id_cloned)
130 if community_dupe.is_ok() {
131 return Err(APIError::err("community_already_exists").into());
134 // When you create a community, make sure the user becomes a moderator and a follower
135 let keypair = generate_actor_keypair()?;
137 let community_form = CommunityForm {
138 name: data.name.to_owned(),
139 title: data.title.to_owned(),
140 description: data.description.to_owned(),
141 icon: Some(data.icon.to_owned()),
142 banner: Some(data.banner.to_owned()),
143 category_id: data.category_id,
149 actor_id: Some(actor_id),
151 private_key: Some(keypair.private_key),
152 public_key: Some(keypair.public_key),
153 last_refreshed_at: None,
157 let inserted_community = match blocking(context.pool(), move |conn| {
158 Community::create(conn, &community_form)
162 Ok(community) => community,
163 Err(_e) => return Err(APIError::err("community_already_exists").into()),
166 let community_moderator_form = CommunityModeratorForm {
167 community_id: inserted_community.id,
171 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
172 if blocking(context.pool(), join).await?.is_err() {
173 return Err(APIError::err("community_moderator_already_exists").into());
176 let community_follower_form = CommunityFollowerForm {
177 community_id: inserted_community.id,
181 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
182 if blocking(context.pool(), follow).await?.is_err() {
183 return Err(APIError::err("community_follower_already_exists").into());
186 let user_id = user.id;
187 let community_view = blocking(context.pool(), move |conn| {
188 CommunityView::read(conn, inserted_community.id, Some(user_id))
192 Ok(CommunityResponse {
193 community: community_view,
198 #[async_trait::async_trait(?Send)]
199 impl Perform for EditCommunity {
200 type Response = CommunityResponse;
204 context: &Data<LemmyContext>,
205 websocket_id: Option<ConnectionId>,
206 ) -> Result<CommunityResponse, LemmyError> {
207 let data: &EditCommunity = &self;
208 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
210 check_slurs(&data.title)?;
211 check_slurs_opt(&data.description)?;
213 // Verify its a mod (only mods can edit it)
214 let edit_id = data.edit_id;
215 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
216 CommunityModeratorView::for_community(conn, edit_id)
217 .map(|v| v.into_iter().map(|m| m.user_id).collect())
220 if !mods.contains(&user.id) {
221 return Err(APIError::err("not_a_moderator").into());
224 let edit_id = data.edit_id;
226 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
228 let icon = diesel_option_overwrite(&data.icon);
229 let banner = diesel_option_overwrite(&data.banner);
231 let community_form = CommunityForm {
232 name: read_community.name,
233 title: data.title.to_owned(),
234 description: data.description.to_owned(),
237 category_id: data.category_id.to_owned(),
238 creator_id: read_community.creator_id,
239 removed: Some(read_community.removed),
240 deleted: Some(read_community.deleted),
242 updated: Some(naive_now()),
243 actor_id: Some(read_community.actor_id),
244 local: read_community.local,
245 private_key: read_community.private_key,
246 public_key: read_community.public_key,
247 last_refreshed_at: None,
251 let edit_id = data.edit_id;
252 match blocking(context.pool(), move |conn| {
253 Community::update(conn, edit_id, &community_form)
257 Ok(community) => community,
258 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
261 // TODO there needs to be some kind of an apub update
262 // process for communities and users
264 let edit_id = data.edit_id;
265 let user_id = user.id;
266 let community_view = blocking(context.pool(), move |conn| {
267 CommunityView::read(conn, edit_id, Some(user_id))
271 let res = CommunityResponse {
272 community: community_view,
275 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
281 #[async_trait::async_trait(?Send)]
282 impl Perform for DeleteCommunity {
283 type Response = CommunityResponse;
287 context: &Data<LemmyContext>,
288 websocket_id: Option<ConnectionId>,
289 ) -> Result<CommunityResponse, LemmyError> {
290 let data: &DeleteCommunity = &self;
291 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
293 // Verify its the creator (only a creator can delete the community)
294 let edit_id = data.edit_id;
296 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
297 if read_community.creator_id != user.id {
298 return Err(APIError::err("no_community_edit_allowed").into());
302 let edit_id = data.edit_id;
303 let deleted = data.deleted;
304 let updated_community = match blocking(context.pool(), move |conn| {
305 Community::update_deleted(conn, edit_id, deleted)
309 Ok(community) => community,
310 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
313 // Send apub messages
315 updated_community.send_delete(&user, context).await?;
317 updated_community.send_undo_delete(&user, context).await?;
320 let edit_id = data.edit_id;
321 let user_id = user.id;
322 let community_view = blocking(context.pool(), move |conn| {
323 CommunityView::read(conn, edit_id, Some(user_id))
327 let res = CommunityResponse {
328 community: community_view,
331 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
337 #[async_trait::async_trait(?Send)]
338 impl Perform for RemoveCommunity {
339 type Response = CommunityResponse;
343 context: &Data<LemmyContext>,
344 websocket_id: Option<ConnectionId>,
345 ) -> Result<CommunityResponse, LemmyError> {
346 let data: &RemoveCommunity = &self;
347 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
349 // Verify its an admin (only an admin can remove a community)
350 is_admin(context.pool(), user.id).await?;
353 let edit_id = data.edit_id;
354 let removed = data.removed;
355 let updated_community = match blocking(context.pool(), move |conn| {
356 Community::update_removed(conn, edit_id, removed)
360 Ok(community) => community,
361 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
365 let expires = match data.expires {
366 Some(time) => Some(naive_from_unix(time)),
369 let form = ModRemoveCommunityForm {
370 mod_user_id: user.id,
371 community_id: data.edit_id,
372 removed: Some(removed),
373 reason: data.reason.to_owned(),
376 blocking(context.pool(), move |conn| {
377 ModRemoveCommunity::create(conn, &form)
383 updated_community.send_remove(&user, context).await?;
385 updated_community.send_undo_remove(&user, context).await?;
388 let edit_id = data.edit_id;
389 let user_id = user.id;
390 let community_view = blocking(context.pool(), move |conn| {
391 CommunityView::read(conn, edit_id, Some(user_id))
395 let res = CommunityResponse {
396 community: community_view,
399 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
405 #[async_trait::async_trait(?Send)]
406 impl Perform for ListCommunities {
407 type Response = ListCommunitiesResponse;
411 context: &Data<LemmyContext>,
412 _websocket_id: Option<ConnectionId>,
413 ) -> Result<ListCommunitiesResponse, LemmyError> {
414 let data: &ListCommunities = &self;
415 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
417 let user_id = match &user {
418 Some(user) => Some(user.id),
422 let show_nsfw = match &user {
423 Some(user) => user.show_nsfw,
427 let sort = SortType::from_str(&data.sort)?;
429 let page = data.page;
430 let limit = data.limit;
431 let communities = blocking(context.pool(), move |conn| {
432 CommunityQueryBuilder::create(conn)
435 .show_nsfw(show_nsfw)
443 Ok(ListCommunitiesResponse { communities })
447 #[async_trait::async_trait(?Send)]
448 impl Perform for FollowCommunity {
449 type Response = CommunityResponse;
453 context: &Data<LemmyContext>,
454 _websocket_id: Option<ConnectionId>,
455 ) -> Result<CommunityResponse, LemmyError> {
456 let data: &FollowCommunity = &self;
457 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
459 let community_id = data.community_id;
460 let community = blocking(context.pool(), move |conn| {
461 Community::read(conn, community_id)
464 let community_follower_form = CommunityFollowerForm {
465 community_id: data.community_id,
471 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
472 if blocking(context.pool(), follow).await?.is_err() {
473 return Err(APIError::err("community_follower_already_exists").into());
477 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
478 if blocking(context.pool(), unfollow).await?.is_err() {
479 return Err(APIError::err("community_follower_already_exists").into());
482 } else if data.follow {
483 // Dont actually add to the community followers here, because you need
484 // to wait for the accept
485 user.send_follow(&community.actor_id()?, context).await?;
487 user.send_unfollow(&community.actor_id()?, context).await?;
488 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
489 if blocking(context.pool(), unfollow).await?.is_err() {
490 return Err(APIError::err("community_follower_already_exists").into());
493 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
495 let community_id = data.community_id;
496 let user_id = user.id;
497 let community_view = blocking(context.pool(), move |conn| {
498 CommunityView::read(conn, community_id, Some(user_id))
502 Ok(CommunityResponse {
503 community: community_view,
508 #[async_trait::async_trait(?Send)]
509 impl Perform for GetFollowedCommunities {
510 type Response = GetFollowedCommunitiesResponse;
514 context: &Data<LemmyContext>,
515 _websocket_id: Option<ConnectionId>,
516 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
517 let data: &GetFollowedCommunities = &self;
518 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
520 let user_id = user.id;
521 let communities = match blocking(context.pool(), move |conn| {
522 CommunityFollowerView::for_user(conn, user_id)
526 Ok(communities) => communities,
527 _ => return Err(APIError::err("system_err_login").into()),
531 Ok(GetFollowedCommunitiesResponse { communities })
535 #[async_trait::async_trait(?Send)]
536 impl Perform for BanFromCommunity {
537 type Response = BanFromCommunityResponse;
541 context: &Data<LemmyContext>,
542 websocket_id: Option<ConnectionId>,
543 ) -> Result<BanFromCommunityResponse, LemmyError> {
544 let data: &BanFromCommunity = &self;
545 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
547 let community_id = data.community_id;
548 let banned_user_id = data.user_id;
550 // Verify that only mods or admins can ban
551 is_mod_or_admin(context.pool(), user.id, community_id).await?;
553 let community_user_ban_form = CommunityUserBanForm {
554 community_id: data.community_id,
555 user_id: data.user_id,
559 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
560 if blocking(context.pool(), ban).await?.is_err() {
561 return Err(APIError::err("community_user_already_banned").into());
564 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
565 if blocking(context.pool(), unban).await?.is_err() {
566 return Err(APIError::err("community_user_already_banned").into());
570 // Remove/Restore their data if that's desired
571 if let Some(remove_data) = data.remove_data {
573 blocking(context.pool(), move |conn: &'_ _| {
574 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
579 // Diesel doesn't allow updates with joins, so this has to be a loop
580 let comments = blocking(context.pool(), move |conn| {
581 CommentQueryBuilder::create(conn)
582 .for_creator_id(banned_user_id)
583 .for_community_id(community_id)
584 .limit(std::i64::MAX)
589 for comment in &comments {
590 let comment_id = comment.id;
591 blocking(context.pool(), move |conn: &'_ _| {
592 Comment::update_removed(conn, comment_id, remove_data)
599 // TODO eventually do correct expires
600 let expires = match data.expires {
601 Some(time) => Some(naive_from_unix(time)),
605 let form = ModBanFromCommunityForm {
606 mod_user_id: user.id,
607 other_user_id: data.user_id,
608 community_id: data.community_id,
609 reason: data.reason.to_owned(),
610 banned: Some(data.ban),
613 blocking(context.pool(), move |conn| {
614 ModBanFromCommunity::create(conn, &form)
618 let user_id = data.user_id;
619 let user_view = blocking(context.pool(), move |conn| {
620 UserView::get_user_secure(conn, user_id)
624 let res = BanFromCommunityResponse {
629 context.chat_server().do_send(SendCommunityRoomMessage {
630 op: UserOperation::BanFromCommunity,
631 response: res.clone(),
640 #[async_trait::async_trait(?Send)]
641 impl Perform for AddModToCommunity {
642 type Response = AddModToCommunityResponse;
646 context: &Data<LemmyContext>,
647 websocket_id: Option<ConnectionId>,
648 ) -> Result<AddModToCommunityResponse, LemmyError> {
649 let data: &AddModToCommunity = &self;
650 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
652 let community_moderator_form = CommunityModeratorForm {
653 community_id: data.community_id,
654 user_id: data.user_id,
657 let community_id = data.community_id;
659 // Verify that only mods or admins can add mod
660 is_mod_or_admin(context.pool(), user.id, community_id).await?;
663 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
664 if blocking(context.pool(), join).await?.is_err() {
665 return Err(APIError::err("community_moderator_already_exists").into());
668 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
669 if blocking(context.pool(), leave).await?.is_err() {
670 return Err(APIError::err("community_moderator_already_exists").into());
675 let form = ModAddCommunityForm {
676 mod_user_id: user.id,
677 other_user_id: data.user_id,
678 community_id: data.community_id,
679 removed: Some(!data.added),
681 blocking(context.pool(), move |conn| {
682 ModAddCommunity::create(conn, &form)
686 let community_id = data.community_id;
687 let moderators = blocking(context.pool(), move |conn| {
688 CommunityModeratorView::for_community(conn, community_id)
692 let res = AddModToCommunityResponse { moderators };
694 context.chat_server().do_send(SendCommunityRoomMessage {
695 op: UserOperation::AddModToCommunity,
696 response: res.clone(),
705 #[async_trait::async_trait(?Send)]
706 impl Perform for TransferCommunity {
707 type Response = GetCommunityResponse;
711 context: &Data<LemmyContext>,
712 _websocket_id: Option<ConnectionId>,
713 ) -> Result<GetCommunityResponse, LemmyError> {
714 let data: &TransferCommunity = &self;
715 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
717 let community_id = data.community_id;
718 let read_community = blocking(context.pool(), move |conn| {
719 Community::read(conn, community_id)
723 let site_creator_id = blocking(context.pool(), move |conn| {
724 Site::read(conn, 1).map(|s| s.creator_id)
728 let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
730 let creator_index = admins
732 .position(|r| r.id == site_creator_id)
733 .context(location_info!())?;
734 let creator_user = admins.remove(creator_index);
735 admins.insert(0, creator_user);
737 // Make sure user is the creator, or an admin
738 if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
739 return Err(APIError::err("not_an_admin").into());
742 let community_id = data.community_id;
743 let new_creator = data.user_id;
744 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
745 if blocking(context.pool(), update).await?.is_err() {
746 return Err(APIError::err("couldnt_update_community").into());
749 // You also have to re-do the community_moderator table, reordering it.
750 let community_id = data.community_id;
751 let mut community_mods = blocking(context.pool(), move |conn| {
752 CommunityModeratorView::for_community(conn, community_id)
755 let creator_index = community_mods
757 .position(|r| r.user_id == data.user_id)
758 .context(location_info!())?;
759 let creator_user = community_mods.remove(creator_index);
760 community_mods.insert(0, creator_user);
762 let community_id = data.community_id;
763 blocking(context.pool(), move |conn| {
764 CommunityModerator::delete_for_community(conn, community_id)
768 // TODO: this should probably be a bulk operation
769 for cmod in &community_mods {
770 let community_moderator_form = CommunityModeratorForm {
771 community_id: cmod.community_id,
772 user_id: cmod.user_id,
775 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
776 if blocking(context.pool(), join).await?.is_err() {
777 return Err(APIError::err("community_moderator_already_exists").into());
782 let form = ModAddCommunityForm {
783 mod_user_id: user.id,
784 other_user_id: data.user_id,
785 community_id: data.community_id,
786 removed: Some(false),
788 blocking(context.pool(), move |conn| {
789 ModAddCommunity::create(conn, &form)
793 let community_id = data.community_id;
794 let user_id = user.id;
795 let community_view = match blocking(context.pool(), move |conn| {
796 CommunityView::read(conn, community_id, Some(user_id))
800 Ok(community) => community,
801 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
804 let community_id = data.community_id;
805 let moderators = match blocking(context.pool(), move |conn| {
806 CommunityModeratorView::for_community(conn, community_id)
810 Ok(moderators) => moderators,
811 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
815 Ok(GetCommunityResponse {
816 community: community_view,
823 pub fn send_community_websocket(
824 res: &CommunityResponse,
825 context: &Data<LemmyContext>,
826 websocket_id: Option<ConnectionId>,
829 // Strip out the user id and subscribed when sending to others
830 let mut res_sent = res.clone();
831 res_sent.community.user_id = None;
832 res_sent.community.subscribed = None;
834 context.chat_server().do_send(SendCommunityRoomMessage {
837 community_id: res.community.id,
842 #[async_trait::async_trait(?Send)]
843 impl Perform for CommunityJoin {
844 type Response = CommunityJoinResponse;
848 context: &Data<LemmyContext>,
849 websocket_id: Option<ConnectionId>,
850 ) -> Result<CommunityJoinResponse, LemmyError> {
851 let data: &CommunityJoin = &self;
853 if let Some(ws_id) = websocket_id {
854 context.chat_server().do_send(JoinCommunityRoom {
855 community_id: data.community_id,
860 Ok(CommunityJoinResponse { joined: true })