9 use actix_web::web::Data;
11 use lemmy_apub::ActorType;
13 diesel_option_overwrite,
15 source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
17 comment_view::CommentQueryBuilder,
19 community_follower_view::CommunityFollowerView,
20 community_moderator_view::CommunityModeratorView,
21 community_view::{CommunityQueryBuilder, CommunityView},
23 user_view::UserViewSafe,
32 use lemmy_structs::{blocking, community::*};
34 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
36 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
41 use lemmy_websocket::{
42 messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
46 use std::str::FromStr;
48 #[async_trait::async_trait(?Send)]
49 impl Perform for GetCommunity {
50 type Response = GetCommunityResponse;
54 context: &Data<LemmyContext>,
55 _websocket_id: Option<ConnectionId>,
56 ) -> Result<GetCommunityResponse, LemmyError> {
57 let data: &GetCommunity = &self;
58 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
59 let user_id = user.map(|u| u.id);
61 let community_id = match data.id {
64 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
65 match blocking(context.pool(), move |conn| {
66 Community::read_from_name(conn, &name)
70 Ok(community) => community,
71 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
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 moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
87 CommunityModeratorView::for_community(conn, community_id)
91 Ok(moderators) => moderators,
92 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
97 .send(GetCommunityUsersOnline { community_id })
101 let res = GetCommunityResponse {
112 #[async_trait::async_trait(?Send)]
113 impl Perform for CreateCommunity {
114 type Response = CommunityResponse;
118 context: &Data<LemmyContext>,
119 _websocket_id: Option<ConnectionId>,
120 ) -> Result<CommunityResponse, LemmyError> {
121 let data: &CreateCommunity = &self;
122 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
124 check_slurs(&data.name)?;
125 check_slurs(&data.title)?;
126 check_slurs_opt(&data.description)?;
128 if !is_valid_community_name(&data.name) {
129 return Err(APIError::err("invalid_community_name").into());
132 // Double check for duplicate community actor_ids
133 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
134 let actor_id_cloned = actor_id.to_owned();
135 let community_dupe = blocking(context.pool(), move |conn| {
136 Community::read_from_apub_id(conn, &actor_id_cloned)
139 if community_dupe.is_ok() {
140 return Err(APIError::err("community_already_exists").into());
143 // Check to make sure the icon and banners are urls
144 let icon = diesel_option_overwrite(&data.icon);
145 let banner = diesel_option_overwrite(&data.banner);
147 check_optional_url(&icon)?;
148 check_optional_url(&banner)?;
150 // When you create a community, make sure the user becomes a moderator and a follower
151 let keypair = generate_actor_keypair()?;
153 let community_form = CommunityForm {
154 name: data.name.to_owned(),
155 title: data.title.to_owned(),
156 description: data.description.to_owned(),
159 category_id: data.category_id,
165 actor_id: Some(actor_id),
167 private_key: Some(keypair.private_key),
168 public_key: Some(keypair.public_key),
169 last_refreshed_at: None,
173 let inserted_community = match blocking(context.pool(), move |conn| {
174 Community::create(conn, &community_form)
178 Ok(community) => community,
179 Err(_e) => return Err(APIError::err("community_already_exists").into()),
182 // The community creator becomes a moderator
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 // Follow your own community
194 let community_follower_form = CommunityFollowerForm {
195 community_id: inserted_community.id,
200 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
201 if blocking(context.pool(), follow).await?.is_err() {
202 return Err(APIError::err("community_follower_already_exists").into());
205 let user_id = user.id;
206 let community_view = blocking(context.pool(), move |conn| {
207 CommunityView::read(conn, inserted_community.id, Some(user_id))
211 Ok(CommunityResponse { community_view })
215 #[async_trait::async_trait(?Send)]
216 impl Perform for EditCommunity {
217 type Response = CommunityResponse;
221 context: &Data<LemmyContext>,
222 websocket_id: Option<ConnectionId>,
223 ) -> Result<CommunityResponse, LemmyError> {
224 let data: &EditCommunity = &self;
225 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
227 check_slurs(&data.title)?;
228 check_slurs_opt(&data.description)?;
230 // Verify its a mod (only mods can edit it)
231 let edit_id = data.edit_id;
232 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
233 CommunityModeratorView::for_community(conn, edit_id)
234 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
237 if !mods.contains(&user.id) {
238 return Err(APIError::err("not_a_moderator").into());
241 let edit_id = data.edit_id;
243 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
245 let icon = diesel_option_overwrite(&data.icon);
246 let banner = diesel_option_overwrite(&data.banner);
248 check_optional_url(&icon)?;
249 check_optional_url(&banner)?;
251 let community_form = CommunityForm {
252 name: read_community.name,
253 title: data.title.to_owned(),
254 description: data.description.to_owned(),
257 category_id: data.category_id.to_owned(),
258 creator_id: read_community.creator_id,
259 removed: Some(read_community.removed),
260 deleted: Some(read_community.deleted),
262 updated: Some(naive_now()),
263 actor_id: Some(read_community.actor_id),
264 local: read_community.local,
265 private_key: read_community.private_key,
266 public_key: read_community.public_key,
267 last_refreshed_at: None,
271 let edit_id = data.edit_id;
272 match blocking(context.pool(), move |conn| {
273 Community::update(conn, edit_id, &community_form)
277 Ok(community) => community,
278 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
281 // TODO there needs to be some kind of an apub update
282 // process for communities and users
284 let edit_id = data.edit_id;
285 let user_id = user.id;
286 let community_view = blocking(context.pool(), move |conn| {
287 CommunityView::read(conn, edit_id, Some(user_id))
291 let res = CommunityResponse { community_view };
293 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
299 #[async_trait::async_trait(?Send)]
300 impl Perform for DeleteCommunity {
301 type Response = CommunityResponse;
305 context: &Data<LemmyContext>,
306 websocket_id: Option<ConnectionId>,
307 ) -> Result<CommunityResponse, LemmyError> {
308 let data: &DeleteCommunity = &self;
309 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
311 // Verify its the creator (only a creator can delete the community)
312 let edit_id = data.edit_id;
314 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
315 if read_community.creator_id != user.id {
316 return Err(APIError::err("no_community_edit_allowed").into());
320 let edit_id = data.edit_id;
321 let deleted = data.deleted;
322 let updated_community = match blocking(context.pool(), move |conn| {
323 Community::update_deleted(conn, edit_id, deleted)
327 Ok(community) => community,
328 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
331 // Send apub messages
333 updated_community.send_delete(context).await?;
335 updated_community.send_undo_delete(context).await?;
338 let edit_id = data.edit_id;
339 let user_id = user.id;
340 let community_view = blocking(context.pool(), move |conn| {
341 CommunityView::read(conn, edit_id, Some(user_id))
345 let res = CommunityResponse { community_view };
347 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
353 #[async_trait::async_trait(?Send)]
354 impl Perform for RemoveCommunity {
355 type Response = CommunityResponse;
359 context: &Data<LemmyContext>,
360 websocket_id: Option<ConnectionId>,
361 ) -> Result<CommunityResponse, LemmyError> {
362 let data: &RemoveCommunity = &self;
363 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
365 // Verify its an admin (only an admin can remove a community)
366 is_admin(context.pool(), user.id).await?;
369 let edit_id = data.edit_id;
370 let removed = data.removed;
371 let updated_community = match blocking(context.pool(), move |conn| {
372 Community::update_removed(conn, edit_id, removed)
376 Ok(community) => community,
377 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
381 let expires = match data.expires {
382 Some(time) => Some(naive_from_unix(time)),
385 let form = ModRemoveCommunityForm {
386 mod_user_id: user.id,
387 community_id: data.edit_id,
388 removed: Some(removed),
389 reason: data.reason.to_owned(),
392 blocking(context.pool(), move |conn| {
393 ModRemoveCommunity::create(conn, &form)
399 updated_community.send_remove(context).await?;
401 updated_community.send_undo_remove(context).await?;
404 let edit_id = data.edit_id;
405 let user_id = user.id;
406 let community_view = blocking(context.pool(), move |conn| {
407 CommunityView::read(conn, edit_id, Some(user_id))
411 let res = CommunityResponse { community_view };
413 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
419 #[async_trait::async_trait(?Send)]
420 impl Perform for ListCommunities {
421 type Response = ListCommunitiesResponse;
425 context: &Data<LemmyContext>,
426 _websocket_id: Option<ConnectionId>,
427 ) -> Result<ListCommunitiesResponse, LemmyError> {
428 let data: &ListCommunities = &self;
429 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
431 let user_id = match &user {
432 Some(user) => Some(user.id),
436 let show_nsfw = match &user {
437 Some(user) => user.show_nsfw,
441 let sort = SortType::from_str(&data.sort)?;
443 let page = data.page;
444 let limit = data.limit;
445 let communities = blocking(context.pool(), move |conn| {
446 CommunityQueryBuilder::create(conn)
448 .show_nsfw(show_nsfw)
457 Ok(ListCommunitiesResponse { communities })
461 #[async_trait::async_trait(?Send)]
462 impl Perform for FollowCommunity {
463 type Response = CommunityResponse;
467 context: &Data<LemmyContext>,
468 _websocket_id: Option<ConnectionId>,
469 ) -> Result<CommunityResponse, LemmyError> {
470 let data: &FollowCommunity = &self;
471 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
473 let community_id = data.community_id;
474 let community = blocking(context.pool(), move |conn| {
475 Community::read(conn, community_id)
478 let community_follower_form = CommunityFollowerForm {
479 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 = data.follow;
523 Ok(CommunityResponse { community_view })
527 #[async_trait::async_trait(?Send)]
528 impl Perform for GetFollowedCommunities {
529 type Response = GetFollowedCommunitiesResponse;
533 context: &Data<LemmyContext>,
534 _websocket_id: Option<ConnectionId>,
535 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
536 let data: &GetFollowedCommunities = &self;
537 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
539 let user_id = user.id;
540 let communities = match blocking(context.pool(), move |conn| {
541 CommunityFollowerView::for_user(conn, user_id)
545 Ok(communities) => communities,
546 _ => return Err(APIError::err("system_err_login").into()),
550 Ok(GetFollowedCommunitiesResponse { communities })
554 #[async_trait::async_trait(?Send)]
555 impl Perform for BanFromCommunity {
556 type Response = BanFromCommunityResponse;
560 context: &Data<LemmyContext>,
561 websocket_id: Option<ConnectionId>,
562 ) -> Result<BanFromCommunityResponse, LemmyError> {
563 let data: &BanFromCommunity = &self;
564 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
566 let community_id = data.community_id;
567 let banned_user_id = data.user_id;
569 // Verify that only mods or admins can ban
570 is_mod_or_admin(context.pool(), user.id, community_id).await?;
572 let community_user_ban_form = CommunityUserBanForm {
573 community_id: data.community_id,
574 user_id: data.user_id,
578 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
579 if blocking(context.pool(), ban).await?.is_err() {
580 return Err(APIError::err("community_user_already_banned").into());
583 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
584 if blocking(context.pool(), unban).await?.is_err() {
585 return Err(APIError::err("community_user_already_banned").into());
589 // Remove/Restore their data if that's desired
590 if data.remove_data {
592 blocking(context.pool(), move |conn: &'_ _| {
593 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), true)
598 // TODO Diesel doesn't allow updates with joins, so this has to be a loop
599 let comments = blocking(context.pool(), move |conn| {
600 CommentQueryBuilder::create(conn)
601 .creator_id(banned_user_id)
602 .community_id(community_id)
603 .limit(std::i64::MAX)
608 for comment_view in &comments {
609 let comment_id = comment_view.comment.id;
610 blocking(context.pool(), move |conn: &'_ _| {
611 Comment::update_removed(conn, comment_id, true)
618 // TODO eventually do correct expires
619 let expires = match data.expires {
620 Some(time) => Some(naive_from_unix(time)),
624 let form = ModBanFromCommunityForm {
625 mod_user_id: user.id,
626 other_user_id: data.user_id,
627 community_id: data.community_id,
628 reason: data.reason.to_owned(),
629 banned: Some(data.ban),
632 blocking(context.pool(), move |conn| {
633 ModBanFromCommunity::create(conn, &form)
637 let user_id = data.user_id;
638 let user_view = blocking(context.pool(), move |conn| {
639 UserViewSafe::read(conn, user_id)
643 let res = BanFromCommunityResponse {
648 context.chat_server().do_send(SendCommunityRoomMessage {
649 op: UserOperation::BanFromCommunity,
650 response: res.clone(),
659 #[async_trait::async_trait(?Send)]
660 impl Perform for AddModToCommunity {
661 type Response = AddModToCommunityResponse;
665 context: &Data<LemmyContext>,
666 websocket_id: Option<ConnectionId>,
667 ) -> Result<AddModToCommunityResponse, LemmyError> {
668 let data: &AddModToCommunity = &self;
669 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
671 let community_moderator_form = CommunityModeratorForm {
672 community_id: data.community_id,
673 user_id: data.user_id,
676 let community_id = data.community_id;
678 // Verify that only mods or admins can add mod
679 is_mod_or_admin(context.pool(), user.id, community_id).await?;
682 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
683 if blocking(context.pool(), join).await?.is_err() {
684 return Err(APIError::err("community_moderator_already_exists").into());
687 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
688 if blocking(context.pool(), leave).await?.is_err() {
689 return Err(APIError::err("community_moderator_already_exists").into());
694 let form = ModAddCommunityForm {
695 mod_user_id: user.id,
696 other_user_id: data.user_id,
697 community_id: data.community_id,
698 removed: Some(!data.added),
700 blocking(context.pool(), move |conn| {
701 ModAddCommunity::create(conn, &form)
705 let community_id = data.community_id;
706 let moderators = blocking(context.pool(), move |conn| {
707 CommunityModeratorView::for_community(conn, community_id)
711 let res = AddModToCommunityResponse { moderators };
713 context.chat_server().do_send(SendCommunityRoomMessage {
714 op: UserOperation::AddModToCommunity,
715 response: res.clone(),
724 #[async_trait::async_trait(?Send)]
725 impl Perform for TransferCommunity {
726 type Response = GetCommunityResponse;
730 context: &Data<LemmyContext>,
731 _websocket_id: Option<ConnectionId>,
732 ) -> Result<GetCommunityResponse, LemmyError> {
733 let data: &TransferCommunity = &self;
734 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
736 let community_id = data.community_id;
737 let read_community = blocking(context.pool(), move |conn| {
738 Community::read(conn, community_id)
742 let site_creator_id = blocking(context.pool(), move |conn| {
743 Site::read(conn, 1).map(|s| s.creator_id)
747 let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
749 // Making sure the creator, if an admin, is at the top
750 let creator_index = admins
752 .position(|r| r.user.id == site_creator_id)
753 .context(location_info!())?;
754 let creator_user = admins.remove(creator_index);
755 admins.insert(0, creator_user);
757 // Make sure user is the creator, or an admin
758 if user.id != read_community.creator_id
759 && !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
761 return Err(APIError::err("not_an_admin").into());
764 let community_id = data.community_id;
765 let new_creator = data.user_id;
766 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
767 if blocking(context.pool(), update).await?.is_err() {
768 return Err(APIError::err("couldnt_update_community").into());
771 // You also have to re-do the community_moderator table, reordering it.
772 let community_id = data.community_id;
773 let mut community_mods = blocking(context.pool(), move |conn| {
774 CommunityModeratorView::for_community(conn, community_id)
777 let creator_index = community_mods
779 .position(|r| r.moderator.id == data.user_id)
780 .context(location_info!())?;
781 let creator_user = community_mods.remove(creator_index);
782 community_mods.insert(0, creator_user);
784 let community_id = data.community_id;
785 blocking(context.pool(), move |conn| {
786 CommunityModerator::delete_for_community(conn, community_id)
790 // TODO: this should probably be a bulk operation
791 for cmod in &community_mods {
792 let community_moderator_form = CommunityModeratorForm {
793 community_id: cmod.community.id,
794 user_id: cmod.moderator.id,
797 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
798 if blocking(context.pool(), join).await?.is_err() {
799 return Err(APIError::err("community_moderator_already_exists").into());
804 let form = ModAddCommunityForm {
805 mod_user_id: user.id,
806 other_user_id: data.user_id,
807 community_id: data.community_id,
808 removed: Some(false),
810 blocking(context.pool(), move |conn| {
811 ModAddCommunity::create(conn, &form)
815 let community_id = data.community_id;
816 let user_id = user.id;
817 let community_view = match blocking(context.pool(), move |conn| {
818 CommunityView::read(conn, community_id, Some(user_id))
822 Ok(community) => community,
823 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
826 let community_id = data.community_id;
827 let moderators = match blocking(context.pool(), move |conn| {
828 CommunityModeratorView::for_community(conn, community_id)
832 Ok(moderators) => moderators,
833 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
837 Ok(GetCommunityResponse {
845 fn send_community_websocket(
846 res: &CommunityResponse,
847 context: &Data<LemmyContext>,
848 websocket_id: Option<ConnectionId>,
851 // TODO is there any way around this?
852 // Strip out the user id and subscribed when sending to others
853 // let mut res_sent = res.clone();
854 // res_sent.community_view.user_id = None;
855 // res_sent.community.subscribed = None;
857 context.chat_server().do_send(SendCommunityRoomMessage {
859 response: res.to_owned(),
860 community_id: res.community_view.community.id,
865 #[async_trait::async_trait(?Send)]
866 impl Perform for CommunityJoin {
867 type Response = CommunityJoinResponse;
871 context: &Data<LemmyContext>,
872 websocket_id: Option<ConnectionId>,
873 ) -> Result<CommunityJoinResponse, LemmyError> {
874 let data: &CommunityJoin = &self;
876 if let Some(ws_id) = websocket_id {
877 context.chat_server().do_send(JoinCommunityRoom {
878 community_id: data.community_id,
883 Ok(CommunityJoinResponse { joined: true })
887 #[async_trait::async_trait(?Send)]
888 impl Perform for ModJoin {
889 type Response = ModJoinResponse;
893 context: &Data<LemmyContext>,
894 websocket_id: Option<ConnectionId>,
895 ) -> Result<ModJoinResponse, LemmyError> {
896 let data: &ModJoin = &self;
898 if let Some(ws_id) = websocket_id {
899 context.chat_server().do_send(JoinModRoom {
900 community_id: data.community_id,
905 Ok(ModJoinResponse { joined: true })