2 api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform},
6 use actix_web::web::Data;
10 comment_view::CommentQueryBuilder,
13 diesel_option_overwrite,
29 GetCommunityUsersOnline,
31 SendCommunityRoomMessage,
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 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 // When you create a community, make sure the user becomes a moderator and a follower
140 let keypair = generate_actor_keypair()?;
142 let community_form = CommunityForm {
143 name: data.name.to_owned(),
144 title: data.title.to_owned(),
145 description: data.description.to_owned(),
146 icon: Some(data.icon.to_owned()),
147 banner: Some(data.banner.to_owned()),
148 category_id: data.category_id,
154 actor_id: Some(actor_id),
156 private_key: Some(keypair.private_key),
157 public_key: Some(keypair.public_key),
158 last_refreshed_at: None,
162 let inserted_community = match blocking(context.pool(), move |conn| {
163 Community::create(conn, &community_form)
167 Ok(community) => community,
168 Err(_e) => return Err(APIError::err("community_already_exists").into()),
171 let community_moderator_form = CommunityModeratorForm {
172 community_id: inserted_community.id,
176 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
177 if blocking(context.pool(), join).await?.is_err() {
178 return Err(APIError::err("community_moderator_already_exists").into());
181 let community_follower_form = CommunityFollowerForm {
182 community_id: inserted_community.id,
186 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
187 if blocking(context.pool(), follow).await?.is_err() {
188 return Err(APIError::err("community_follower_already_exists").into());
191 let user_id = user.id;
192 let community_view = blocking(context.pool(), move |conn| {
193 CommunityView::read(conn, inserted_community.id, Some(user_id))
197 Ok(CommunityResponse {
198 community: community_view,
203 #[async_trait::async_trait(?Send)]
204 impl Perform for EditCommunity {
205 type Response = CommunityResponse;
209 context: &Data<LemmyContext>,
210 websocket_id: Option<ConnectionId>,
211 ) -> Result<CommunityResponse, LemmyError> {
212 let data: &EditCommunity = &self;
213 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
215 check_slurs(&data.title)?;
216 check_slurs_opt(&data.description)?;
218 // Verify its a mod (only mods can edit it)
219 let edit_id = data.edit_id;
220 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
221 CommunityModeratorView::for_community(conn, edit_id)
222 .map(|v| v.into_iter().map(|m| m.user_id).collect())
225 if !mods.contains(&user.id) {
226 return Err(APIError::err("not_a_moderator").into());
229 let edit_id = data.edit_id;
231 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
233 let icon = diesel_option_overwrite(&data.icon);
234 let banner = diesel_option_overwrite(&data.banner);
236 let community_form = CommunityForm {
237 name: read_community.name,
238 title: data.title.to_owned(),
239 description: data.description.to_owned(),
242 category_id: data.category_id.to_owned(),
243 creator_id: read_community.creator_id,
244 removed: Some(read_community.removed),
245 deleted: Some(read_community.deleted),
247 updated: Some(naive_now()),
248 actor_id: Some(read_community.actor_id),
249 local: read_community.local,
250 private_key: read_community.private_key,
251 public_key: read_community.public_key,
252 last_refreshed_at: None,
256 let edit_id = data.edit_id;
257 match blocking(context.pool(), move |conn| {
258 Community::update(conn, edit_id, &community_form)
262 Ok(community) => community,
263 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
266 // TODO there needs to be some kind of an apub update
267 // process for communities and users
269 let edit_id = data.edit_id;
270 let user_id = user.id;
271 let community_view = blocking(context.pool(), move |conn| {
272 CommunityView::read(conn, edit_id, Some(user_id))
276 let res = CommunityResponse {
277 community: community_view,
280 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
286 #[async_trait::async_trait(?Send)]
287 impl Perform for DeleteCommunity {
288 type Response = CommunityResponse;
292 context: &Data<LemmyContext>,
293 websocket_id: Option<ConnectionId>,
294 ) -> Result<CommunityResponse, LemmyError> {
295 let data: &DeleteCommunity = &self;
296 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
298 // Verify its the creator (only a creator can delete the community)
299 let edit_id = data.edit_id;
301 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
302 if read_community.creator_id != user.id {
303 return Err(APIError::err("no_community_edit_allowed").into());
307 let edit_id = data.edit_id;
308 let deleted = data.deleted;
309 let updated_community = match blocking(context.pool(), move |conn| {
310 Community::update_deleted(conn, edit_id, deleted)
314 Ok(community) => community,
315 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
318 // Send apub messages
320 updated_community.send_delete(&user, context).await?;
322 updated_community.send_undo_delete(&user, context).await?;
325 let edit_id = data.edit_id;
326 let user_id = user.id;
327 let community_view = blocking(context.pool(), move |conn| {
328 CommunityView::read(conn, edit_id, Some(user_id))
332 let res = CommunityResponse {
333 community: community_view,
336 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
342 #[async_trait::async_trait(?Send)]
343 impl Perform for RemoveCommunity {
344 type Response = CommunityResponse;
348 context: &Data<LemmyContext>,
349 websocket_id: Option<ConnectionId>,
350 ) -> Result<CommunityResponse, LemmyError> {
351 let data: &RemoveCommunity = &self;
352 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
354 // Verify its an admin (only an admin can remove a community)
355 is_admin(context.pool(), user.id).await?;
358 let edit_id = data.edit_id;
359 let removed = data.removed;
360 let updated_community = match blocking(context.pool(), move |conn| {
361 Community::update_removed(conn, edit_id, removed)
365 Ok(community) => community,
366 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
370 let expires = match data.expires {
371 Some(time) => Some(naive_from_unix(time)),
374 let form = ModRemoveCommunityForm {
375 mod_user_id: user.id,
376 community_id: data.edit_id,
377 removed: Some(removed),
378 reason: data.reason.to_owned(),
381 blocking(context.pool(), move |conn| {
382 ModRemoveCommunity::create(conn, &form)
388 updated_community.send_remove(&user, context).await?;
390 updated_community.send_undo_remove(&user, context).await?;
393 let edit_id = data.edit_id;
394 let user_id = user.id;
395 let community_view = blocking(context.pool(), move |conn| {
396 CommunityView::read(conn, edit_id, Some(user_id))
400 let res = CommunityResponse {
401 community: community_view,
404 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
410 #[async_trait::async_trait(?Send)]
411 impl Perform for ListCommunities {
412 type Response = ListCommunitiesResponse;
416 context: &Data<LemmyContext>,
417 _websocket_id: Option<ConnectionId>,
418 ) -> Result<ListCommunitiesResponse, LemmyError> {
419 let data: &ListCommunities = &self;
420 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
422 let user_id = match &user {
423 Some(user) => Some(user.id),
427 let show_nsfw = match &user {
428 Some(user) => user.show_nsfw,
432 let sort = SortType::from_str(&data.sort)?;
434 let page = data.page;
435 let limit = data.limit;
436 let communities = blocking(context.pool(), move |conn| {
437 CommunityQueryBuilder::create(conn)
440 .show_nsfw(show_nsfw)
448 Ok(ListCommunitiesResponse { communities })
452 #[async_trait::async_trait(?Send)]
453 impl Perform for FollowCommunity {
454 type Response = CommunityResponse;
458 context: &Data<LemmyContext>,
459 _websocket_id: Option<ConnectionId>,
460 ) -> Result<CommunityResponse, LemmyError> {
461 let data: &FollowCommunity = &self;
462 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
464 let community_id = data.community_id;
465 let community = blocking(context.pool(), move |conn| {
466 Community::read(conn, community_id)
469 let community_follower_form = CommunityFollowerForm {
470 community_id: data.community_id,
476 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
477 if blocking(context.pool(), follow).await?.is_err() {
478 return Err(APIError::err("community_follower_already_exists").into());
482 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
483 if blocking(context.pool(), unfollow).await?.is_err() {
484 return Err(APIError::err("community_follower_already_exists").into());
487 } else if data.follow {
488 // Dont actually add to the community followers here, because you need
489 // to wait for the accept
490 user.send_follow(&community.actor_id()?, context).await?;
492 user.send_unfollow(&community.actor_id()?, context).await?;
493 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
494 if blocking(context.pool(), unfollow).await?.is_err() {
495 return Err(APIError::err("community_follower_already_exists").into());
499 let community_id = data.community_id;
500 let user_id = user.id;
501 let mut community_view = blocking(context.pool(), move |conn| {
502 CommunityView::read(conn, community_id, Some(user_id))
506 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
507 // For now, just assume that remote follows are accepted.
508 // Otherwise, the subscribed will be null
509 if !community.local {
510 community_view.subscribed = Some(data.follow);
513 Ok(CommunityResponse {
514 community: community_view,
519 #[async_trait::async_trait(?Send)]
520 impl Perform for GetFollowedCommunities {
521 type Response = GetFollowedCommunitiesResponse;
525 context: &Data<LemmyContext>,
526 _websocket_id: Option<ConnectionId>,
527 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
528 let data: &GetFollowedCommunities = &self;
529 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
531 let user_id = user.id;
532 let communities = match blocking(context.pool(), move |conn| {
533 CommunityFollowerView::for_user(conn, user_id)
537 Ok(communities) => communities,
538 _ => return Err(APIError::err("system_err_login").into()),
542 Ok(GetFollowedCommunitiesResponse { communities })
546 #[async_trait::async_trait(?Send)]
547 impl Perform for BanFromCommunity {
548 type Response = BanFromCommunityResponse;
552 context: &Data<LemmyContext>,
553 websocket_id: Option<ConnectionId>,
554 ) -> Result<BanFromCommunityResponse, LemmyError> {
555 let data: &BanFromCommunity = &self;
556 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
558 let community_id = data.community_id;
559 let banned_user_id = data.user_id;
561 // Verify that only mods or admins can ban
562 is_mod_or_admin(context.pool(), user.id, community_id).await?;
564 let community_user_ban_form = CommunityUserBanForm {
565 community_id: data.community_id,
566 user_id: data.user_id,
570 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
571 if blocking(context.pool(), ban).await?.is_err() {
572 return Err(APIError::err("community_user_already_banned").into());
575 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
576 if blocking(context.pool(), unban).await?.is_err() {
577 return Err(APIError::err("community_user_already_banned").into());
581 // Remove/Restore their data if that's desired
582 if let Some(remove_data) = data.remove_data {
584 blocking(context.pool(), move |conn: &'_ _| {
585 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
590 // Diesel doesn't allow updates with joins, so this has to be a loop
591 let comments = blocking(context.pool(), move |conn| {
592 CommentQueryBuilder::create(conn)
593 .for_creator_id(banned_user_id)
594 .for_community_id(community_id)
595 .limit(std::i64::MAX)
600 for comment in &comments {
601 let comment_id = comment.id;
602 blocking(context.pool(), move |conn: &'_ _| {
603 Comment::update_removed(conn, comment_id, remove_data)
610 // TODO eventually do correct expires
611 let expires = match data.expires {
612 Some(time) => Some(naive_from_unix(time)),
616 let form = ModBanFromCommunityForm {
617 mod_user_id: user.id,
618 other_user_id: data.user_id,
619 community_id: data.community_id,
620 reason: data.reason.to_owned(),
621 banned: Some(data.ban),
624 blocking(context.pool(), move |conn| {
625 ModBanFromCommunity::create(conn, &form)
629 let user_id = data.user_id;
630 let user_view = blocking(context.pool(), move |conn| {
631 UserView::get_user_secure(conn, user_id)
635 let res = BanFromCommunityResponse {
640 context.chat_server().do_send(SendCommunityRoomMessage {
641 op: UserOperation::BanFromCommunity,
642 response: res.clone(),
651 #[async_trait::async_trait(?Send)]
652 impl Perform for AddModToCommunity {
653 type Response = AddModToCommunityResponse;
657 context: &Data<LemmyContext>,
658 websocket_id: Option<ConnectionId>,
659 ) -> Result<AddModToCommunityResponse, LemmyError> {
660 let data: &AddModToCommunity = &self;
661 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
663 let community_moderator_form = CommunityModeratorForm {
664 community_id: data.community_id,
665 user_id: data.user_id,
668 let community_id = data.community_id;
670 // Verify that only mods or admins can add mod
671 is_mod_or_admin(context.pool(), user.id, community_id).await?;
674 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
675 if blocking(context.pool(), join).await?.is_err() {
676 return Err(APIError::err("community_moderator_already_exists").into());
679 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
680 if blocking(context.pool(), leave).await?.is_err() {
681 return Err(APIError::err("community_moderator_already_exists").into());
686 let form = ModAddCommunityForm {
687 mod_user_id: user.id,
688 other_user_id: data.user_id,
689 community_id: data.community_id,
690 removed: Some(!data.added),
692 blocking(context.pool(), move |conn| {
693 ModAddCommunity::create(conn, &form)
697 let community_id = data.community_id;
698 let moderators = blocking(context.pool(), move |conn| {
699 CommunityModeratorView::for_community(conn, community_id)
703 let res = AddModToCommunityResponse { moderators };
705 context.chat_server().do_send(SendCommunityRoomMessage {
706 op: UserOperation::AddModToCommunity,
707 response: res.clone(),
716 #[async_trait::async_trait(?Send)]
717 impl Perform for TransferCommunity {
718 type Response = GetCommunityResponse;
722 context: &Data<LemmyContext>,
723 _websocket_id: Option<ConnectionId>,
724 ) -> Result<GetCommunityResponse, LemmyError> {
725 let data: &TransferCommunity = &self;
726 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
728 let community_id = data.community_id;
729 let read_community = blocking(context.pool(), move |conn| {
730 Community::read(conn, community_id)
734 let site_creator_id = blocking(context.pool(), move |conn| {
735 Site::read(conn, 1).map(|s| s.creator_id)
739 let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
741 let creator_index = admins
743 .position(|r| r.id == site_creator_id)
744 .context(location_info!())?;
745 let creator_user = admins.remove(creator_index);
746 admins.insert(0, creator_user);
748 // Make sure user is the creator, or an admin
749 if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
750 return Err(APIError::err("not_an_admin").into());
753 let community_id = data.community_id;
754 let new_creator = data.user_id;
755 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
756 if blocking(context.pool(), update).await?.is_err() {
757 return Err(APIError::err("couldnt_update_community").into());
760 // You also have to re-do the community_moderator table, reordering it.
761 let community_id = data.community_id;
762 let mut community_mods = blocking(context.pool(), move |conn| {
763 CommunityModeratorView::for_community(conn, community_id)
766 let creator_index = community_mods
768 .position(|r| r.user_id == data.user_id)
769 .context(location_info!())?;
770 let creator_user = community_mods.remove(creator_index);
771 community_mods.insert(0, creator_user);
773 let community_id = data.community_id;
774 blocking(context.pool(), move |conn| {
775 CommunityModerator::delete_for_community(conn, community_id)
779 // TODO: this should probably be a bulk operation
780 for cmod in &community_mods {
781 let community_moderator_form = CommunityModeratorForm {
782 community_id: cmod.community_id,
783 user_id: cmod.user_id,
786 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
787 if blocking(context.pool(), join).await?.is_err() {
788 return Err(APIError::err("community_moderator_already_exists").into());
793 let form = ModAddCommunityForm {
794 mod_user_id: user.id,
795 other_user_id: data.user_id,
796 community_id: data.community_id,
797 removed: Some(false),
799 blocking(context.pool(), move |conn| {
800 ModAddCommunity::create(conn, &form)
804 let community_id = data.community_id;
805 let user_id = user.id;
806 let community_view = match blocking(context.pool(), move |conn| {
807 CommunityView::read(conn, community_id, Some(user_id))
811 Ok(community) => community,
812 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
815 let community_id = data.community_id;
816 let moderators = match blocking(context.pool(), move |conn| {
817 CommunityModeratorView::for_community(conn, community_id)
821 Ok(moderators) => moderators,
822 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
826 Ok(GetCommunityResponse {
827 community: community_view,
834 pub fn send_community_websocket(
835 res: &CommunityResponse,
836 context: &Data<LemmyContext>,
837 websocket_id: Option<ConnectionId>,
840 // Strip out the user id and subscribed when sending to others
841 let mut res_sent = res.clone();
842 res_sent.community.user_id = None;
843 res_sent.community.subscribed = None;
845 context.chat_server().do_send(SendCommunityRoomMessage {
848 community_id: res.community.id,
853 #[async_trait::async_trait(?Send)]
854 impl Perform for CommunityJoin {
855 type Response = CommunityJoinResponse;
859 context: &Data<LemmyContext>,
860 websocket_id: Option<ConnectionId>,
861 ) -> Result<CommunityJoinResponse, LemmyError> {
862 let data: &CommunityJoin = &self;
864 if let Some(ws_id) = websocket_id {
865 context.chat_server().do_send(JoinCommunityRoom {
866 community_id: data.community_id,
871 Ok(CommunityJoinResponse { joined: true })