9 use actix_web::web::Data;
11 use lemmy_apub::ActorType;
13 diesel_option_overwrite,
16 community::{CommunityModerator_, Community_},
20 comment_view::CommentQueryBuilder,
22 community_follower_view::CommunityFollowerView,
23 community_moderator_view::CommunityModeratorView,
24 community_view::{CommunityQueryBuilder, CommunityView},
26 user_view::UserViewSafe,
35 use lemmy_db_schema::{
37 source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
39 use lemmy_structs::{blocking, community::*};
41 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
43 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
48 use lemmy_websocket::{
49 messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
53 use std::str::FromStr;
55 #[async_trait::async_trait(?Send)]
56 impl Perform for GetCommunity {
57 type Response = GetCommunityResponse;
61 context: &Data<LemmyContext>,
62 _websocket_id: Option<ConnectionId>,
63 ) -> Result<GetCommunityResponse, LemmyError> {
64 let data: &GetCommunity = &self;
65 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
66 let user_id = user.map(|u| u.id);
68 let community_id = match data.id {
71 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
72 match blocking(context.pool(), move |conn| {
73 Community::read_from_name(conn, &name)
77 Ok(community) => community,
78 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
84 let community_view = match blocking(context.pool(), move |conn| {
85 CommunityView::read(conn, community_id, user_id)
89 Ok(community) => community,
90 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
93 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
94 CommunityModeratorView::for_community(conn, community_id)
98 Ok(moderators) => moderators,
99 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
104 .send(GetCommunityUsersOnline { community_id })
108 let res = GetCommunityResponse {
119 #[async_trait::async_trait(?Send)]
120 impl Perform for CreateCommunity {
121 type Response = CommunityResponse;
125 context: &Data<LemmyContext>,
126 _websocket_id: Option<ConnectionId>,
127 ) -> Result<CommunityResponse, LemmyError> {
128 let data: &CreateCommunity = &self;
129 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
131 check_slurs(&data.name)?;
132 check_slurs(&data.title)?;
133 check_slurs_opt(&data.description)?;
135 if !is_valid_community_name(&data.name) {
136 return Err(APIError::err("invalid_community_name").into());
139 // Double check for duplicate community actor_ids
140 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
141 let actor_id_cloned = actor_id.to_owned();
142 let community_dupe = blocking(context.pool(), move |conn| {
143 Community::read_from_apub_id(conn, &actor_id_cloned)
146 if community_dupe.is_ok() {
147 return Err(APIError::err("community_already_exists").into());
150 // Check to make sure the icon and banners are urls
151 let icon = diesel_option_overwrite(&data.icon);
152 let banner = diesel_option_overwrite(&data.banner);
154 check_optional_url(&icon)?;
155 check_optional_url(&banner)?;
157 // When you create a community, make sure the user becomes a moderator and a follower
158 let keypair = generate_actor_keypair()?;
160 let community_form = CommunityForm {
161 name: data.name.to_owned(),
162 title: data.title.to_owned(),
163 description: data.description.to_owned(),
166 category_id: data.category_id,
172 actor_id: Some(actor_id),
174 private_key: Some(keypair.private_key),
175 public_key: Some(keypair.public_key),
176 last_refreshed_at: None,
180 let inserted_community = match blocking(context.pool(), move |conn| {
181 Community::create(conn, &community_form)
185 Ok(community) => community,
186 Err(_e) => return Err(APIError::err("community_already_exists").into()),
189 // The community creator becomes a moderator
190 let community_moderator_form = CommunityModeratorForm {
191 community_id: inserted_community.id,
195 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
196 if blocking(context.pool(), join).await?.is_err() {
197 return Err(APIError::err("community_moderator_already_exists").into());
200 // Follow your own community
201 let community_follower_form = CommunityFollowerForm {
202 community_id: inserted_community.id,
207 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
208 if blocking(context.pool(), follow).await?.is_err() {
209 return Err(APIError::err("community_follower_already_exists").into());
212 let user_id = user.id;
213 let community_view = blocking(context.pool(), move |conn| {
214 CommunityView::read(conn, inserted_community.id, Some(user_id))
218 Ok(CommunityResponse { community_view })
222 #[async_trait::async_trait(?Send)]
223 impl Perform for EditCommunity {
224 type Response = CommunityResponse;
228 context: &Data<LemmyContext>,
229 websocket_id: Option<ConnectionId>,
230 ) -> Result<CommunityResponse, LemmyError> {
231 let data: &EditCommunity = &self;
232 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
234 check_slurs(&data.title)?;
235 check_slurs_opt(&data.description)?;
237 // Verify its a mod (only mods can edit it)
238 let edit_id = data.edit_id;
239 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
240 CommunityModeratorView::for_community(conn, edit_id)
241 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
244 if !mods.contains(&user.id) {
245 return Err(APIError::err("not_a_moderator").into());
248 let edit_id = data.edit_id;
250 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
252 let icon = diesel_option_overwrite(&data.icon);
253 let banner = diesel_option_overwrite(&data.banner);
255 check_optional_url(&icon)?;
256 check_optional_url(&banner)?;
258 let community_form = CommunityForm {
259 name: read_community.name,
260 title: data.title.to_owned(),
261 description: data.description.to_owned(),
264 category_id: data.category_id.to_owned(),
265 creator_id: read_community.creator_id,
266 removed: Some(read_community.removed),
267 deleted: Some(read_community.deleted),
269 updated: Some(naive_now()),
270 actor_id: Some(read_community.actor_id),
271 local: read_community.local,
272 private_key: read_community.private_key,
273 public_key: read_community.public_key,
274 last_refreshed_at: None,
278 let edit_id = data.edit_id;
279 match blocking(context.pool(), move |conn| {
280 Community::update(conn, edit_id, &community_form)
284 Ok(community) => community,
285 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
288 // TODO there needs to be some kind of an apub update
289 // process for communities and users
291 let edit_id = data.edit_id;
292 let user_id = user.id;
293 let community_view = blocking(context.pool(), move |conn| {
294 CommunityView::read(conn, edit_id, Some(user_id))
298 let res = CommunityResponse { community_view };
300 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
306 #[async_trait::async_trait(?Send)]
307 impl Perform for DeleteCommunity {
308 type Response = CommunityResponse;
312 context: &Data<LemmyContext>,
313 websocket_id: Option<ConnectionId>,
314 ) -> Result<CommunityResponse, LemmyError> {
315 let data: &DeleteCommunity = &self;
316 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
318 // Verify its the creator (only a creator can delete the community)
319 let edit_id = data.edit_id;
321 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
322 if read_community.creator_id != user.id {
323 return Err(APIError::err("no_community_edit_allowed").into());
327 let edit_id = data.edit_id;
328 let deleted = data.deleted;
329 let updated_community = match blocking(context.pool(), move |conn| {
330 Community::update_deleted(conn, edit_id, deleted)
334 Ok(community) => community,
335 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
338 // Send apub messages
340 updated_community.send_delete(context).await?;
342 updated_community.send_undo_delete(context).await?;
345 let edit_id = data.edit_id;
346 let user_id = user.id;
347 let community_view = blocking(context.pool(), move |conn| {
348 CommunityView::read(conn, edit_id, Some(user_id))
352 let res = CommunityResponse { community_view };
354 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
360 #[async_trait::async_trait(?Send)]
361 impl Perform for RemoveCommunity {
362 type Response = CommunityResponse;
366 context: &Data<LemmyContext>,
367 websocket_id: Option<ConnectionId>,
368 ) -> Result<CommunityResponse, LemmyError> {
369 let data: &RemoveCommunity = &self;
370 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
372 // Verify its an admin (only an admin can remove a community)
373 is_admin(context.pool(), user.id).await?;
376 let edit_id = data.edit_id;
377 let removed = data.removed;
378 let updated_community = match blocking(context.pool(), move |conn| {
379 Community::update_removed(conn, edit_id, removed)
383 Ok(community) => community,
384 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
388 let expires = match data.expires {
389 Some(time) => Some(naive_from_unix(time)),
392 let form = ModRemoveCommunityForm {
393 mod_user_id: user.id,
394 community_id: data.edit_id,
395 removed: Some(removed),
396 reason: data.reason.to_owned(),
399 blocking(context.pool(), move |conn| {
400 ModRemoveCommunity::create(conn, &form)
406 updated_community.send_remove(context).await?;
408 updated_community.send_undo_remove(context).await?;
411 let edit_id = data.edit_id;
412 let user_id = user.id;
413 let community_view = blocking(context.pool(), move |conn| {
414 CommunityView::read(conn, edit_id, Some(user_id))
418 let res = CommunityResponse { community_view };
420 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
426 #[async_trait::async_trait(?Send)]
427 impl Perform for ListCommunities {
428 type Response = ListCommunitiesResponse;
432 context: &Data<LemmyContext>,
433 _websocket_id: Option<ConnectionId>,
434 ) -> Result<ListCommunitiesResponse, LemmyError> {
435 let data: &ListCommunities = &self;
436 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
438 let user_id = match &user {
439 Some(user) => Some(user.id),
443 let show_nsfw = match &user {
444 Some(user) => user.show_nsfw,
448 let sort = SortType::from_str(&data.sort)?;
450 let page = data.page;
451 let limit = data.limit;
452 let communities = blocking(context.pool(), move |conn| {
453 CommunityQueryBuilder::create(conn)
455 .show_nsfw(show_nsfw)
464 Ok(ListCommunitiesResponse { communities })
468 #[async_trait::async_trait(?Send)]
469 impl Perform for FollowCommunity {
470 type Response = CommunityResponse;
474 context: &Data<LemmyContext>,
475 _websocket_id: Option<ConnectionId>,
476 ) -> Result<CommunityResponse, LemmyError> {
477 let data: &FollowCommunity = &self;
478 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
480 let community_id = data.community_id;
481 let community = blocking(context.pool(), move |conn| {
482 Community::read(conn, community_id)
485 let community_follower_form = CommunityFollowerForm {
486 community_id: data.community_id,
493 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
494 if blocking(context.pool(), follow).await?.is_err() {
495 return Err(APIError::err("community_follower_already_exists").into());
499 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
500 if blocking(context.pool(), unfollow).await?.is_err() {
501 return Err(APIError::err("community_follower_already_exists").into());
504 } else if data.follow {
505 // Dont actually add to the community followers here, because you need
506 // to wait for the accept
507 user.send_follow(&community.actor_id()?, context).await?;
509 user.send_unfollow(&community.actor_id()?, context).await?;
510 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
511 if blocking(context.pool(), unfollow).await?.is_err() {
512 return Err(APIError::err("community_follower_already_exists").into());
516 let community_id = data.community_id;
517 let user_id = user.id;
518 let mut community_view = blocking(context.pool(), move |conn| {
519 CommunityView::read(conn, community_id, Some(user_id))
523 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
524 // For now, just assume that remote follows are accepted.
525 // Otherwise, the subscribed will be null
526 if !community.local {
527 community_view.subscribed = data.follow;
530 Ok(CommunityResponse { community_view })
534 #[async_trait::async_trait(?Send)]
535 impl Perform for GetFollowedCommunities {
536 type Response = GetFollowedCommunitiesResponse;
540 context: &Data<LemmyContext>,
541 _websocket_id: Option<ConnectionId>,
542 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
543 let data: &GetFollowedCommunities = &self;
544 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
546 let user_id = user.id;
547 let communities = match blocking(context.pool(), move |conn| {
548 CommunityFollowerView::for_user(conn, user_id)
552 Ok(communities) => communities,
553 _ => return Err(APIError::err("system_err_login").into()),
557 Ok(GetFollowedCommunitiesResponse { communities })
561 #[async_trait::async_trait(?Send)]
562 impl Perform for BanFromCommunity {
563 type Response = BanFromCommunityResponse;
567 context: &Data<LemmyContext>,
568 websocket_id: Option<ConnectionId>,
569 ) -> Result<BanFromCommunityResponse, LemmyError> {
570 let data: &BanFromCommunity = &self;
571 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
573 let community_id = data.community_id;
574 let banned_user_id = data.user_id;
576 // Verify that only mods or admins can ban
577 is_mod_or_admin(context.pool(), user.id, community_id).await?;
579 let community_user_ban_form = CommunityUserBanForm {
580 community_id: data.community_id,
581 user_id: data.user_id,
585 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
586 if blocking(context.pool(), ban).await?.is_err() {
587 return Err(APIError::err("community_user_already_banned").into());
590 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
591 if blocking(context.pool(), unban).await?.is_err() {
592 return Err(APIError::err("community_user_already_banned").into());
596 // Remove/Restore their data if that's desired
597 if data.remove_data {
599 blocking(context.pool(), move |conn: &'_ _| {
600 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), true)
605 // TODO Diesel doesn't allow updates with joins, so this has to be a loop
606 let comments = blocking(context.pool(), move |conn| {
607 CommentQueryBuilder::create(conn)
608 .creator_id(banned_user_id)
609 .community_id(community_id)
610 .limit(std::i64::MAX)
615 for comment_view in &comments {
616 let comment_id = comment_view.comment.id;
617 blocking(context.pool(), move |conn: &'_ _| {
618 Comment::update_removed(conn, comment_id, true)
625 // TODO eventually do correct expires
626 let expires = match data.expires {
627 Some(time) => Some(naive_from_unix(time)),
631 let form = ModBanFromCommunityForm {
632 mod_user_id: user.id,
633 other_user_id: data.user_id,
634 community_id: data.community_id,
635 reason: data.reason.to_owned(),
636 banned: Some(data.ban),
639 blocking(context.pool(), move |conn| {
640 ModBanFromCommunity::create(conn, &form)
644 let user_id = data.user_id;
645 let user_view = blocking(context.pool(), move |conn| {
646 UserViewSafe::read(conn, user_id)
650 let res = BanFromCommunityResponse {
655 context.chat_server().do_send(SendCommunityRoomMessage {
656 op: UserOperation::BanFromCommunity,
657 response: res.clone(),
666 #[async_trait::async_trait(?Send)]
667 impl Perform for AddModToCommunity {
668 type Response = AddModToCommunityResponse;
672 context: &Data<LemmyContext>,
673 websocket_id: Option<ConnectionId>,
674 ) -> Result<AddModToCommunityResponse, LemmyError> {
675 let data: &AddModToCommunity = &self;
676 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
678 let community_moderator_form = CommunityModeratorForm {
679 community_id: data.community_id,
680 user_id: data.user_id,
683 let community_id = data.community_id;
685 // Verify that only mods or admins can add mod
686 is_mod_or_admin(context.pool(), user.id, community_id).await?;
689 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
690 if blocking(context.pool(), join).await?.is_err() {
691 return Err(APIError::err("community_moderator_already_exists").into());
694 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
695 if blocking(context.pool(), leave).await?.is_err() {
696 return Err(APIError::err("community_moderator_already_exists").into());
701 let form = ModAddCommunityForm {
702 mod_user_id: user.id,
703 other_user_id: data.user_id,
704 community_id: data.community_id,
705 removed: Some(!data.added),
707 blocking(context.pool(), move |conn| {
708 ModAddCommunity::create(conn, &form)
712 let community_id = data.community_id;
713 let moderators = blocking(context.pool(), move |conn| {
714 CommunityModeratorView::for_community(conn, community_id)
718 let res = AddModToCommunityResponse { moderators };
720 context.chat_server().do_send(SendCommunityRoomMessage {
721 op: UserOperation::AddModToCommunity,
722 response: res.clone(),
731 #[async_trait::async_trait(?Send)]
732 impl Perform for TransferCommunity {
733 type Response = GetCommunityResponse;
737 context: &Data<LemmyContext>,
738 _websocket_id: Option<ConnectionId>,
739 ) -> Result<GetCommunityResponse, LemmyError> {
740 let data: &TransferCommunity = &self;
741 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
743 let community_id = data.community_id;
744 let read_community = blocking(context.pool(), move |conn| {
745 Community::read(conn, community_id)
749 let site_creator_id = blocking(context.pool(), move |conn| {
750 Site::read(conn, 1).map(|s| s.creator_id)
754 let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
756 // Making sure the creator, if an admin, is at the top
757 let creator_index = admins
759 .position(|r| r.user.id == site_creator_id)
760 .context(location_info!())?;
761 let creator_user = admins.remove(creator_index);
762 admins.insert(0, creator_user);
764 // Make sure user is the creator, or an admin
765 if user.id != read_community.creator_id
766 && !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
768 return Err(APIError::err("not_an_admin").into());
771 let community_id = data.community_id;
772 let new_creator = data.user_id;
773 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
774 if blocking(context.pool(), update).await?.is_err() {
775 return Err(APIError::err("couldnt_update_community").into());
778 // You also have to re-do the community_moderator table, reordering it.
779 let community_id = data.community_id;
780 let mut community_mods = blocking(context.pool(), move |conn| {
781 CommunityModeratorView::for_community(conn, community_id)
784 let creator_index = community_mods
786 .position(|r| r.moderator.id == data.user_id)
787 .context(location_info!())?;
788 let creator_user = community_mods.remove(creator_index);
789 community_mods.insert(0, creator_user);
791 let community_id = data.community_id;
792 blocking(context.pool(), move |conn| {
793 CommunityModerator::delete_for_community(conn, community_id)
797 // TODO: this should probably be a bulk operation
798 for cmod in &community_mods {
799 let community_moderator_form = CommunityModeratorForm {
800 community_id: cmod.community.id,
801 user_id: cmod.moderator.id,
804 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
805 if blocking(context.pool(), join).await?.is_err() {
806 return Err(APIError::err("community_moderator_already_exists").into());
811 let form = ModAddCommunityForm {
812 mod_user_id: user.id,
813 other_user_id: data.user_id,
814 community_id: data.community_id,
815 removed: Some(false),
817 blocking(context.pool(), move |conn| {
818 ModAddCommunity::create(conn, &form)
822 let community_id = data.community_id;
823 let user_id = user.id;
824 let community_view = match blocking(context.pool(), move |conn| {
825 CommunityView::read(conn, community_id, Some(user_id))
829 Ok(community) => community,
830 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
833 let community_id = data.community_id;
834 let moderators = match blocking(context.pool(), move |conn| {
835 CommunityModeratorView::for_community(conn, community_id)
839 Ok(moderators) => moderators,
840 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
844 Ok(GetCommunityResponse {
852 fn send_community_websocket(
853 res: &CommunityResponse,
854 context: &Data<LemmyContext>,
855 websocket_id: Option<ConnectionId>,
858 // TODO is there any way around this?
859 // Strip out the user id and subscribed when sending to others
860 // let mut res_sent = res.clone();
861 // res_sent.community_view.user_id = None;
862 // res_sent.community.subscribed = None;
864 context.chat_server().do_send(SendCommunityRoomMessage {
866 response: res.to_owned(),
867 community_id: res.community_view.community.id,
872 #[async_trait::async_trait(?Send)]
873 impl Perform for CommunityJoin {
874 type Response = CommunityJoinResponse;
878 context: &Data<LemmyContext>,
879 websocket_id: Option<ConnectionId>,
880 ) -> Result<CommunityJoinResponse, LemmyError> {
881 let data: &CommunityJoin = &self;
883 if let Some(ws_id) = websocket_id {
884 context.chat_server().do_send(JoinCommunityRoom {
885 community_id: data.community_id,
890 Ok(CommunityJoinResponse { joined: true })
894 #[async_trait::async_trait(?Send)]
895 impl Perform for ModJoin {
896 type Response = ModJoinResponse;
900 context: &Data<LemmyContext>,
901 websocket_id: Option<ConnectionId>,
902 ) -> Result<ModJoinResponse, LemmyError> {
903 let data: &ModJoin = &self;
905 if let Some(ws_id) = websocket_id {
906 context.chat_server().do_send(JoinModRoom {
907 community_id: data.community_id,
912 Ok(ModJoinResponse { joined: true })