9 use actix_web::web::Data;
11 use lemmy_apub::ActorType;
12 use lemmy_db_queries::{
13 diesel_option_overwrite,
16 community::{CommunityModerator_, Community_},
27 use lemmy_db_schema::{
29 source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
31 use lemmy_db_views::comment_view::CommentQueryBuilder;
32 use lemmy_db_views_actor::{
33 community_follower_view::CommunityFollowerView,
34 community_moderator_view::CommunityModeratorView,
35 community_view::{CommunityQueryBuilder, CommunityView},
36 user_view::UserViewSafe,
38 use lemmy_structs::{blocking, community::*};
40 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
42 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
47 use lemmy_websocket::{
48 messages::{GetCommunityUsersOnline, SendCommunityRoomMessage},
52 use std::str::FromStr;
54 #[async_trait::async_trait(?Send)]
55 impl Perform for GetCommunity {
56 type Response = GetCommunityResponse;
60 context: &Data<LemmyContext>,
61 _websocket_id: Option<ConnectionId>,
62 ) -> Result<GetCommunityResponse, LemmyError> {
63 let data: &GetCommunity = &self;
64 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
65 let user_id = user.map(|u| u.id);
67 let community_id = match data.id {
70 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
71 match blocking(context.pool(), move |conn| {
72 Community::read_from_name(conn, &name)
76 Ok(community) => community,
77 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
83 let community_view = match blocking(context.pool(), move |conn| {
84 CommunityView::read(conn, community_id, user_id)
88 Ok(community) => community,
89 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
92 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
93 CommunityModeratorView::for_community(conn, community_id)
97 Ok(moderators) => moderators,
98 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
103 .send(GetCommunityUsersOnline { community_id })
107 let res = GetCommunityResponse {
118 #[async_trait::async_trait(?Send)]
119 impl Perform for CreateCommunity {
120 type Response = CommunityResponse;
124 context: &Data<LemmyContext>,
125 _websocket_id: Option<ConnectionId>,
126 ) -> Result<CommunityResponse, LemmyError> {
127 let data: &CreateCommunity = &self;
128 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
130 check_slurs(&data.name)?;
131 check_slurs(&data.title)?;
132 check_slurs_opt(&data.description)?;
134 if !is_valid_community_name(&data.name) {
135 return Err(APIError::err("invalid_community_name").into());
138 // Double check for duplicate community actor_ids
139 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name);
140 let actor_id_cloned = actor_id.to_owned();
141 let community_dupe = blocking(context.pool(), move |conn| {
142 Community::read_from_apub_id(conn, &actor_id_cloned.into())
145 if community_dupe.is_ok() {
146 return Err(APIError::err("community_already_exists").into());
149 // Check to make sure the icon and banners are urls
150 let icon = diesel_option_overwrite(&data.icon);
151 let banner = diesel_option_overwrite(&data.banner);
153 check_optional_url(&icon)?;
154 check_optional_url(&banner)?;
156 // When you create a community, make sure the user becomes a moderator and a follower
157 let keypair = generate_actor_keypair()?;
159 let community_form = CommunityForm {
160 name: data.name.to_owned(),
161 title: data.title.to_owned(),
162 description: data.description.to_owned(),
165 category_id: data.category_id,
171 actor_id: Some(actor_id.into()),
173 private_key: Some(keypair.private_key),
174 public_key: Some(keypair.public_key),
175 last_refreshed_at: None,
179 let inserted_community = match blocking(context.pool(), move |conn| {
180 Community::create(conn, &community_form)
184 Ok(community) => community,
185 Err(_e) => return Err(APIError::err("community_already_exists").into()),
188 // The community creator becomes a moderator
189 let community_moderator_form = CommunityModeratorForm {
190 community_id: inserted_community.id,
194 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
195 if blocking(context.pool(), join).await?.is_err() {
196 return Err(APIError::err("community_moderator_already_exists").into());
199 // Follow your own community
200 let community_follower_form = CommunityFollowerForm {
201 community_id: inserted_community.id,
206 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
207 if blocking(context.pool(), follow).await?.is_err() {
208 return Err(APIError::err("community_follower_already_exists").into());
211 let user_id = user.id;
212 let community_view = blocking(context.pool(), move |conn| {
213 CommunityView::read(conn, inserted_community.id, Some(user_id))
217 Ok(CommunityResponse { community_view })
221 #[async_trait::async_trait(?Send)]
222 impl Perform for EditCommunity {
223 type Response = CommunityResponse;
227 context: &Data<LemmyContext>,
228 websocket_id: Option<ConnectionId>,
229 ) -> Result<CommunityResponse, LemmyError> {
230 let data: &EditCommunity = &self;
231 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
233 check_slurs(&data.title)?;
234 check_slurs_opt(&data.description)?;
236 // Verify its a mod (only mods can edit it)
237 let community_id = data.community_id;
238 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
239 CommunityModeratorView::for_community(conn, community_id)
240 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
243 if !mods.contains(&user.id) {
244 return Err(APIError::err("not_a_moderator").into());
247 let community_id = data.community_id;
248 let read_community = blocking(context.pool(), move |conn| {
249 Community::read(conn, community_id)
253 let icon = diesel_option_overwrite(&data.icon);
254 let banner = diesel_option_overwrite(&data.banner);
256 check_optional_url(&icon)?;
257 check_optional_url(&banner)?;
259 let community_form = CommunityForm {
260 name: read_community.name,
261 title: data.title.to_owned(),
262 description: data.description.to_owned(),
265 category_id: data.category_id.to_owned(),
266 creator_id: read_community.creator_id,
267 removed: Some(read_community.removed),
268 deleted: Some(read_community.deleted),
270 updated: Some(naive_now()),
271 actor_id: Some(read_community.actor_id),
272 local: read_community.local,
273 private_key: read_community.private_key,
274 public_key: read_community.public_key,
275 last_refreshed_at: None,
279 let community_id = data.community_id;
280 match blocking(context.pool(), move |conn| {
281 Community::update(conn, community_id, &community_form)
285 Ok(community) => community,
286 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
289 // TODO there needs to be some kind of an apub update
290 // process for communities and users
292 let community_id = data.community_id;
293 let user_id = user.id;
294 let community_view = blocking(context.pool(), move |conn| {
295 CommunityView::read(conn, community_id, Some(user_id))
299 let res = CommunityResponse { community_view };
301 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
307 #[async_trait::async_trait(?Send)]
308 impl Perform for DeleteCommunity {
309 type Response = CommunityResponse;
313 context: &Data<LemmyContext>,
314 websocket_id: Option<ConnectionId>,
315 ) -> Result<CommunityResponse, LemmyError> {
316 let data: &DeleteCommunity = &self;
317 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
319 // Verify its the creator (only a creator can delete the community)
320 let community_id = data.community_id;
321 let read_community = blocking(context.pool(), move |conn| {
322 Community::read(conn, community_id)
325 if read_community.creator_id != user.id {
326 return Err(APIError::err("no_community_edit_allowed").into());
330 let community_id = data.community_id;
331 let deleted = data.deleted;
332 let updated_community = match blocking(context.pool(), move |conn| {
333 Community::update_deleted(conn, community_id, deleted)
337 Ok(community) => community,
338 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
341 // Send apub messages
343 updated_community.send_delete(context).await?;
345 updated_community.send_undo_delete(context).await?;
348 let community_id = data.community_id;
349 let user_id = user.id;
350 let community_view = blocking(context.pool(), move |conn| {
351 CommunityView::read(conn, community_id, Some(user_id))
355 let res = CommunityResponse { community_view };
357 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
363 #[async_trait::async_trait(?Send)]
364 impl Perform for RemoveCommunity {
365 type Response = CommunityResponse;
369 context: &Data<LemmyContext>,
370 websocket_id: Option<ConnectionId>,
371 ) -> Result<CommunityResponse, LemmyError> {
372 let data: &RemoveCommunity = &self;
373 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
375 // Verify its an admin (only an admin can remove a community)
376 is_admin(context.pool(), user.id).await?;
379 let community_id = data.community_id;
380 let removed = data.removed;
381 let updated_community = match blocking(context.pool(), move |conn| {
382 Community::update_removed(conn, community_id, removed)
386 Ok(community) => community,
387 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
391 let expires = match data.expires {
392 Some(time) => Some(naive_from_unix(time)),
395 let form = ModRemoveCommunityForm {
396 mod_user_id: user.id,
397 community_id: data.community_id,
398 removed: Some(removed),
399 reason: data.reason.to_owned(),
402 blocking(context.pool(), move |conn| {
403 ModRemoveCommunity::create(conn, &form)
409 updated_community.send_remove(context).await?;
411 updated_community.send_undo_remove(context).await?;
414 let community_id = data.community_id;
415 let user_id = user.id;
416 let community_view = blocking(context.pool(), move |conn| {
417 CommunityView::read(conn, community_id, Some(user_id))
421 let res = CommunityResponse { community_view };
423 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
429 #[async_trait::async_trait(?Send)]
430 impl Perform for ListCommunities {
431 type Response = ListCommunitiesResponse;
435 context: &Data<LemmyContext>,
436 _websocket_id: Option<ConnectionId>,
437 ) -> Result<ListCommunitiesResponse, LemmyError> {
438 let data: &ListCommunities = &self;
439 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
441 let user_id = match &user {
442 Some(user) => Some(user.id),
446 let show_nsfw = match &user {
447 Some(user) => user.show_nsfw,
451 let type_ = ListingType::from_str(&data.type_)?;
452 let sort = SortType::from_str(&data.sort)?;
454 let page = data.page;
455 let limit = data.limit;
456 let communities = blocking(context.pool(), move |conn| {
457 CommunityQueryBuilder::create(conn)
458 .listing_type(&type_)
460 .show_nsfw(show_nsfw)
469 Ok(ListCommunitiesResponse { communities })
473 #[async_trait::async_trait(?Send)]
474 impl Perform for FollowCommunity {
475 type Response = CommunityResponse;
479 context: &Data<LemmyContext>,
480 _websocket_id: Option<ConnectionId>,
481 ) -> Result<CommunityResponse, LemmyError> {
482 let data: &FollowCommunity = &self;
483 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
485 let community_id = data.community_id;
486 let community = blocking(context.pool(), move |conn| {
487 Community::read(conn, community_id)
490 let community_follower_form = CommunityFollowerForm {
491 community_id: data.community_id,
498 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
499 if blocking(context.pool(), follow).await?.is_err() {
500 return Err(APIError::err("community_follower_already_exists").into());
504 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
505 if blocking(context.pool(), unfollow).await?.is_err() {
506 return Err(APIError::err("community_follower_already_exists").into());
509 } else if data.follow {
510 // Dont actually add to the community followers here, because you need
511 // to wait for the accept
512 user.send_follow(&community.actor_id(), context).await?;
514 user.send_unfollow(&community.actor_id(), context).await?;
515 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
516 if blocking(context.pool(), unfollow).await?.is_err() {
517 return Err(APIError::err("community_follower_already_exists").into());
521 let community_id = data.community_id;
522 let user_id = user.id;
523 let mut community_view = blocking(context.pool(), move |conn| {
524 CommunityView::read(conn, community_id, Some(user_id))
528 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
529 // For now, just assume that remote follows are accepted.
530 // Otherwise, the subscribed will be null
531 if !community.local {
532 community_view.subscribed = data.follow;
535 Ok(CommunityResponse { community_view })
539 #[async_trait::async_trait(?Send)]
540 impl Perform for GetFollowedCommunities {
541 type Response = GetFollowedCommunitiesResponse;
545 context: &Data<LemmyContext>,
546 _websocket_id: Option<ConnectionId>,
547 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
548 let data: &GetFollowedCommunities = &self;
549 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
551 let user_id = user.id;
552 let communities = match blocking(context.pool(), move |conn| {
553 CommunityFollowerView::for_user(conn, user_id)
557 Ok(communities) => communities,
558 _ => return Err(APIError::err("system_err_login").into()),
562 Ok(GetFollowedCommunitiesResponse { communities })
566 #[async_trait::async_trait(?Send)]
567 impl Perform for BanFromCommunity {
568 type Response = BanFromCommunityResponse;
572 context: &Data<LemmyContext>,
573 websocket_id: Option<ConnectionId>,
574 ) -> Result<BanFromCommunityResponse, LemmyError> {
575 let data: &BanFromCommunity = &self;
576 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
578 let community_id = data.community_id;
579 let banned_user_id = data.user_id;
581 // Verify that only mods or admins can ban
582 is_mod_or_admin(context.pool(), user.id, community_id).await?;
584 let community_user_ban_form = CommunityUserBanForm {
585 community_id: data.community_id,
586 user_id: data.user_id,
590 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
591 if blocking(context.pool(), ban).await?.is_err() {
592 return Err(APIError::err("community_user_already_banned").into());
595 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
596 if blocking(context.pool(), unban).await?.is_err() {
597 return Err(APIError::err("community_user_already_banned").into());
601 // Remove/Restore their data if that's desired
602 if data.remove_data {
604 blocking(context.pool(), move |conn: &'_ _| {
605 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), true)
610 // TODO Diesel doesn't allow updates with joins, so this has to be a loop
611 let comments = blocking(context.pool(), move |conn| {
612 CommentQueryBuilder::create(conn)
613 .creator_id(banned_user_id)
614 .community_id(community_id)
615 .limit(std::i64::MAX)
620 for comment_view in &comments {
621 let comment_id = comment_view.comment.id;
622 blocking(context.pool(), move |conn: &'_ _| {
623 Comment::update_removed(conn, comment_id, true)
630 // TODO eventually do correct expires
631 let expires = match data.expires {
632 Some(time) => Some(naive_from_unix(time)),
636 let form = ModBanFromCommunityForm {
637 mod_user_id: user.id,
638 other_user_id: data.user_id,
639 community_id: data.community_id,
640 reason: data.reason.to_owned(),
641 banned: Some(data.ban),
644 blocking(context.pool(), move |conn| {
645 ModBanFromCommunity::create(conn, &form)
649 let user_id = data.user_id;
650 let user_view = blocking(context.pool(), move |conn| {
651 UserViewSafe::read(conn, user_id)
655 let res = BanFromCommunityResponse {
660 context.chat_server().do_send(SendCommunityRoomMessage {
661 op: UserOperation::BanFromCommunity,
662 response: res.clone(),
671 #[async_trait::async_trait(?Send)]
672 impl Perform for AddModToCommunity {
673 type Response = AddModToCommunityResponse;
677 context: &Data<LemmyContext>,
678 websocket_id: Option<ConnectionId>,
679 ) -> Result<AddModToCommunityResponse, LemmyError> {
680 let data: &AddModToCommunity = &self;
681 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
683 let community_moderator_form = CommunityModeratorForm {
684 community_id: data.community_id,
685 user_id: data.user_id,
688 let community_id = data.community_id;
690 // Verify that only mods or admins can add mod
691 is_mod_or_admin(context.pool(), user.id, community_id).await?;
694 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
695 if blocking(context.pool(), join).await?.is_err() {
696 return Err(APIError::err("community_moderator_already_exists").into());
699 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
700 if blocking(context.pool(), leave).await?.is_err() {
701 return Err(APIError::err("community_moderator_already_exists").into());
706 let form = ModAddCommunityForm {
707 mod_user_id: user.id,
708 other_user_id: data.user_id,
709 community_id: data.community_id,
710 removed: Some(!data.added),
712 blocking(context.pool(), move |conn| {
713 ModAddCommunity::create(conn, &form)
717 let community_id = data.community_id;
718 let moderators = blocking(context.pool(), move |conn| {
719 CommunityModeratorView::for_community(conn, community_id)
723 let res = AddModToCommunityResponse { moderators };
725 context.chat_server().do_send(SendCommunityRoomMessage {
726 op: UserOperation::AddModToCommunity,
727 response: res.clone(),
736 #[async_trait::async_trait(?Send)]
737 impl Perform for TransferCommunity {
738 type Response = GetCommunityResponse;
742 context: &Data<LemmyContext>,
743 _websocket_id: Option<ConnectionId>,
744 ) -> Result<GetCommunityResponse, LemmyError> {
745 let data: &TransferCommunity = &self;
746 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
748 let community_id = data.community_id;
749 let read_community = blocking(context.pool(), move |conn| {
750 Community::read(conn, community_id)
754 let site_creator_id = blocking(context.pool(), move |conn| {
755 Site::read(conn, 1).map(|s| s.creator_id)
759 let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
761 // Making sure the creator, if an admin, is at the top
762 let creator_index = admins
764 .position(|r| r.user.id == site_creator_id)
765 .context(location_info!())?;
766 let creator_user = admins.remove(creator_index);
767 admins.insert(0, creator_user);
769 // Make sure user is the creator, or an admin
770 if user.id != read_community.creator_id
771 && !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
773 return Err(APIError::err("not_an_admin").into());
776 let community_id = data.community_id;
777 let new_creator = data.user_id;
778 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
779 if blocking(context.pool(), update).await?.is_err() {
780 return Err(APIError::err("couldnt_update_community").into());
783 // You also have to re-do the community_moderator table, reordering it.
784 let community_id = data.community_id;
785 let mut community_mods = blocking(context.pool(), move |conn| {
786 CommunityModeratorView::for_community(conn, community_id)
789 let creator_index = community_mods
791 .position(|r| r.moderator.id == data.user_id)
792 .context(location_info!())?;
793 let creator_user = community_mods.remove(creator_index);
794 community_mods.insert(0, creator_user);
796 let community_id = data.community_id;
797 blocking(context.pool(), move |conn| {
798 CommunityModerator::delete_for_community(conn, community_id)
802 // TODO: this should probably be a bulk operation
803 for cmod in &community_mods {
804 let community_moderator_form = CommunityModeratorForm {
805 community_id: cmod.community.id,
806 user_id: cmod.moderator.id,
809 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
810 if blocking(context.pool(), join).await?.is_err() {
811 return Err(APIError::err("community_moderator_already_exists").into());
816 let form = ModAddCommunityForm {
817 mod_user_id: user.id,
818 other_user_id: data.user_id,
819 community_id: data.community_id,
820 removed: Some(false),
822 blocking(context.pool(), move |conn| {
823 ModAddCommunity::create(conn, &form)
827 let community_id = data.community_id;
828 let user_id = user.id;
829 let community_view = match blocking(context.pool(), move |conn| {
830 CommunityView::read(conn, community_id, Some(user_id))
834 Ok(community) => community,
835 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
838 let community_id = data.community_id;
839 let moderators = match blocking(context.pool(), move |conn| {
840 CommunityModeratorView::for_community(conn, community_id)
844 Ok(moderators) => moderators,
845 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
849 Ok(GetCommunityResponse {
857 fn send_community_websocket(
858 res: &CommunityResponse,
859 context: &Data<LemmyContext>,
860 websocket_id: Option<ConnectionId>,
863 // Strip out the user id and subscribed when sending to others
864 let mut res_sent = res.clone();
865 res_sent.community_view.subscribed = false;
867 context.chat_server().do_send(SendCommunityRoomMessage {
870 community_id: res.community_view.community.id,