3 get_local_user_view_from_jwt,
4 get_local_user_view_from_jwt_opt,
9 use actix_web::web::Data;
11 use lemmy_api_structs::{blocking, community::*};
13 generate_apub_endpoint,
14 generate_followers_url,
16 generate_shared_inbox_url,
22 use lemmy_db_queries::{
23 diesel_option_overwrite_to_url,
26 community::{CommunityModerator_, Community_},
37 use lemmy_db_schema::{
39 source::{comment::Comment, community::*, moderator::*, person::Person, post::Post, site::*},
42 use lemmy_db_views::comment_view::CommentQueryBuilder;
43 use lemmy_db_views_actor::{
44 community_follower_view::CommunityFollowerView,
45 community_moderator_view::CommunityModeratorView,
46 community_view::{CommunityQueryBuilder, CommunityView},
47 person_view::PersonViewSafe,
50 apub::generate_actor_keypair,
52 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
57 use lemmy_websocket::{
58 messages::{GetCommunityUsersOnline, SendCommunityRoomMessage},
62 use std::str::FromStr;
64 #[async_trait::async_trait(?Send)]
65 impl Perform for GetCommunity {
66 type Response = GetCommunityResponse;
70 context: &Data<LemmyContext>,
71 _websocket_id: Option<ConnectionId>,
72 ) -> Result<GetCommunityResponse, LemmyError> {
73 let data: &GetCommunity = &self;
74 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
75 let person_id = local_user_view.map(|u| u.person.id);
77 let community_id = match data.id {
80 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
81 match blocking(context.pool(), move |conn| {
82 Community::read_from_name(conn, &name)
86 Ok(community) => community,
87 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
93 let community_view = match blocking(context.pool(), move |conn| {
94 CommunityView::read(conn, community_id, person_id)
98 Ok(community) => community,
99 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
102 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
103 CommunityModeratorView::for_community(conn, community_id)
107 Ok(moderators) => moderators,
108 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
113 .send(GetCommunityUsersOnline { community_id })
117 let res = GetCommunityResponse {
128 #[async_trait::async_trait(?Send)]
129 impl Perform for CreateCommunity {
130 type Response = CommunityResponse;
134 context: &Data<LemmyContext>,
135 _websocket_id: Option<ConnectionId>,
136 ) -> Result<CommunityResponse, LemmyError> {
137 let data: &CreateCommunity = &self;
138 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
140 check_slurs(&data.name)?;
141 check_slurs(&data.title)?;
142 check_slurs_opt(&data.description)?;
144 if !is_valid_community_name(&data.name) {
145 return Err(ApiError::err("invalid_community_name").into());
148 // Double check for duplicate community actor_ids
149 let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?;
150 let actor_id_cloned = community_actor_id.to_owned();
151 let community_dupe = blocking(context.pool(), move |conn| {
152 Community::read_from_apub_id(conn, &actor_id_cloned)
155 if community_dupe.is_ok() {
156 return Err(ApiError::err("community_already_exists").into());
159 // Check to make sure the icon and banners are urls
160 let icon = diesel_option_overwrite_to_url(&data.icon)?;
161 let banner = diesel_option_overwrite_to_url(&data.banner)?;
163 // When you create a community, make sure the user becomes a moderator and a follower
164 let keypair = generate_actor_keypair()?;
166 let community_form = CommunityForm {
167 name: data.name.to_owned(),
168 title: data.title.to_owned(),
169 description: data.description.to_owned(),
172 creator_id: local_user_view.person.id,
177 actor_id: Some(community_actor_id.to_owned()),
179 private_key: Some(keypair.private_key),
180 public_key: Some(keypair.public_key),
181 last_refreshed_at: None,
183 followers_url: Some(generate_followers_url(&community_actor_id)?),
184 inbox_url: Some(generate_inbox_url(&community_actor_id)?),
185 shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)),
188 let inserted_community = match blocking(context.pool(), move |conn| {
189 Community::create(conn, &community_form)
193 Ok(community) => community,
194 Err(_e) => return Err(ApiError::err("community_already_exists").into()),
197 // The community creator becomes a moderator
198 let community_moderator_form = CommunityModeratorForm {
199 community_id: inserted_community.id,
200 person_id: local_user_view.person.id,
203 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
204 if blocking(context.pool(), join).await?.is_err() {
205 return Err(ApiError::err("community_moderator_already_exists").into());
208 // Follow your own community
209 let community_follower_form = CommunityFollowerForm {
210 community_id: inserted_community.id,
211 person_id: local_user_view.person.id,
215 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
216 if blocking(context.pool(), follow).await?.is_err() {
217 return Err(ApiError::err("community_follower_already_exists").into());
220 let person_id = local_user_view.person.id;
221 let community_view = blocking(context.pool(), move |conn| {
222 CommunityView::read(conn, inserted_community.id, Some(person_id))
226 Ok(CommunityResponse { community_view })
230 #[async_trait::async_trait(?Send)]
231 impl Perform for EditCommunity {
232 type Response = CommunityResponse;
236 context: &Data<LemmyContext>,
237 websocket_id: Option<ConnectionId>,
238 ) -> Result<CommunityResponse, LemmyError> {
239 let data: &EditCommunity = &self;
240 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
242 check_slurs(&data.title)?;
243 check_slurs_opt(&data.description)?;
245 // Verify its a mod (only mods can edit it)
246 let community_id = data.community_id;
247 let mods: Vec<PersonId> = blocking(context.pool(), move |conn| {
248 CommunityModeratorView::for_community(conn, community_id)
249 .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
252 if !mods.contains(&local_user_view.person.id) {
253 return Err(ApiError::err("not_a_moderator").into());
256 let community_id = data.community_id;
257 let read_community = blocking(context.pool(), move |conn| {
258 Community::read(conn, community_id)
262 let icon = diesel_option_overwrite_to_url(&data.icon)?;
263 let banner = diesel_option_overwrite_to_url(&data.banner)?;
265 let community_form = CommunityForm {
266 name: read_community.name,
267 title: data.title.to_owned(),
268 description: data.description.to_owned(),
271 creator_id: read_community.creator_id,
272 removed: Some(read_community.removed),
273 deleted: Some(read_community.deleted),
275 updated: Some(naive_now()),
276 actor_id: Some(read_community.actor_id),
277 local: read_community.local,
278 private_key: read_community.private_key,
279 public_key: read_community.public_key,
280 last_refreshed_at: None,
284 shared_inbox_url: None,
287 let community_id = data.community_id;
288 match blocking(context.pool(), move |conn| {
289 Community::update(conn, community_id, &community_form)
293 Ok(community) => community,
294 Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
297 // TODO there needs to be some kind of an apub update
298 // process for communities and users
300 let community_id = data.community_id;
301 let person_id = local_user_view.person.id;
302 let community_view = blocking(context.pool(), move |conn| {
303 CommunityView::read(conn, community_id, Some(person_id))
307 let res = CommunityResponse { community_view };
309 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
315 #[async_trait::async_trait(?Send)]
316 impl Perform for DeleteCommunity {
317 type Response = CommunityResponse;
321 context: &Data<LemmyContext>,
322 websocket_id: Option<ConnectionId>,
323 ) -> Result<CommunityResponse, LemmyError> {
324 let data: &DeleteCommunity = &self;
325 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
327 // Verify its the creator (only a creator can delete the community)
328 let community_id = data.community_id;
329 let read_community = blocking(context.pool(), move |conn| {
330 Community::read(conn, community_id)
333 if read_community.creator_id != local_user_view.person.id {
334 return Err(ApiError::err("no_community_edit_allowed").into());
338 let community_id = data.community_id;
339 let deleted = data.deleted;
340 let updated_community = match blocking(context.pool(), move |conn| {
341 Community::update_deleted(conn, community_id, deleted)
345 Ok(community) => community,
346 Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
349 // Send apub messages
351 updated_community.send_delete(context).await?;
353 updated_community.send_undo_delete(context).await?;
356 let community_id = data.community_id;
357 let person_id = local_user_view.person.id;
358 let community_view = blocking(context.pool(), move |conn| {
359 CommunityView::read(conn, community_id, Some(person_id))
363 let res = CommunityResponse { community_view };
365 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
371 #[async_trait::async_trait(?Send)]
372 impl Perform for RemoveCommunity {
373 type Response = CommunityResponse;
377 context: &Data<LemmyContext>,
378 websocket_id: Option<ConnectionId>,
379 ) -> Result<CommunityResponse, LemmyError> {
380 let data: &RemoveCommunity = &self;
381 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
383 // Verify its an admin (only an admin can remove a community)
384 is_admin(&local_user_view)?;
387 let community_id = data.community_id;
388 let removed = data.removed;
389 let updated_community = match blocking(context.pool(), move |conn| {
390 Community::update_removed(conn, community_id, removed)
394 Ok(community) => community,
395 Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
399 let expires = match data.expires {
400 Some(time) => Some(naive_from_unix(time)),
403 let form = ModRemoveCommunityForm {
404 mod_person_id: local_user_view.person.id,
405 community_id: data.community_id,
406 removed: Some(removed),
407 reason: data.reason.to_owned(),
410 blocking(context.pool(), move |conn| {
411 ModRemoveCommunity::create(conn, &form)
417 updated_community.send_remove(context).await?;
419 updated_community.send_undo_remove(context).await?;
422 let community_id = data.community_id;
423 let person_id = local_user_view.person.id;
424 let community_view = blocking(context.pool(), move |conn| {
425 CommunityView::read(conn, community_id, Some(person_id))
429 let res = CommunityResponse { community_view };
431 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
437 #[async_trait::async_trait(?Send)]
438 impl Perform for ListCommunities {
439 type Response = ListCommunitiesResponse;
443 context: &Data<LemmyContext>,
444 _websocket_id: Option<ConnectionId>,
445 ) -> Result<ListCommunitiesResponse, LemmyError> {
446 let data: &ListCommunities = &self;
447 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
449 let person_id = match &local_user_view {
450 Some(uv) => Some(uv.person.id),
454 // Don't show NSFW by default
455 let show_nsfw = match &local_user_view {
456 Some(uv) => uv.local_user.show_nsfw,
460 let type_ = ListingType::from_str(&data.type_)?;
461 let sort = SortType::from_str(&data.sort)?;
463 let page = data.page;
464 let limit = data.limit;
465 let communities = blocking(context.pool(), move |conn| {
466 CommunityQueryBuilder::create(conn)
467 .listing_type(&type_)
469 .show_nsfw(show_nsfw)
470 .my_person_id(person_id)
478 Ok(ListCommunitiesResponse { communities })
482 #[async_trait::async_trait(?Send)]
483 impl Perform for FollowCommunity {
484 type Response = CommunityResponse;
488 context: &Data<LemmyContext>,
489 _websocket_id: Option<ConnectionId>,
490 ) -> Result<CommunityResponse, LemmyError> {
491 let data: &FollowCommunity = &self;
492 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
494 let community_id = data.community_id;
495 let community = blocking(context.pool(), move |conn| {
496 Community::read(conn, community_id)
499 let community_follower_form = CommunityFollowerForm {
500 community_id: data.community_id,
501 person_id: local_user_view.person.id,
507 check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
509 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
510 if blocking(context.pool(), follow).await?.is_err() {
511 return Err(ApiError::err("community_follower_already_exists").into());
515 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());
520 } else if data.follow {
521 // Dont actually add to the community followers here, because you need
522 // to wait for the accept
525 .send_follow(&community.actor_id(), context)
530 .send_unfollow(&community.actor_id(), context)
532 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
533 if blocking(context.pool(), unfollow).await?.is_err() {
534 return Err(ApiError::err("community_follower_already_exists").into());
538 let community_id = data.community_id;
539 let person_id = local_user_view.person.id;
540 let mut community_view = blocking(context.pool(), move |conn| {
541 CommunityView::read(conn, community_id, Some(person_id))
545 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
546 // For now, just assume that remote follows are accepted.
547 // Otherwise, the subscribed will be null
548 if !community.local {
549 community_view.subscribed = data.follow;
552 Ok(CommunityResponse { community_view })
556 #[async_trait::async_trait(?Send)]
557 impl Perform for GetFollowedCommunities {
558 type Response = GetFollowedCommunitiesResponse;
562 context: &Data<LemmyContext>,
563 _websocket_id: Option<ConnectionId>,
564 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
565 let data: &GetFollowedCommunities = &self;
566 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
568 let person_id = local_user_view.person.id;
569 let communities = match blocking(context.pool(), move |conn| {
570 CommunityFollowerView::for_person(conn, person_id)
574 Ok(communities) => communities,
575 _ => return Err(ApiError::err("system_err_login").into()),
579 Ok(GetFollowedCommunitiesResponse { communities })
583 #[async_trait::async_trait(?Send)]
584 impl Perform for BanFromCommunity {
585 type Response = BanFromCommunityResponse;
589 context: &Data<LemmyContext>,
590 websocket_id: Option<ConnectionId>,
591 ) -> Result<BanFromCommunityResponse, LemmyError> {
592 let data: &BanFromCommunity = &self;
593 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
595 let community_id = data.community_id;
596 let banned_person_id = data.person_id;
598 // Verify that only mods or admins can ban
599 is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
601 let community_user_ban_form = CommunityPersonBanForm {
602 community_id: data.community_id,
603 person_id: data.person_id,
607 let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form);
608 if blocking(context.pool(), ban).await?.is_err() {
609 return Err(ApiError::err("community_user_already_banned").into());
612 // Also unsubscribe them from the community, if they are subscribed
613 let community_follower_form = CommunityFollowerForm {
614 community_id: data.community_id,
615 person_id: banned_person_id,
618 blocking(context.pool(), move |conn: &'_ _| {
619 CommunityFollower::unfollow(conn, &community_follower_form)
624 let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
625 if blocking(context.pool(), unban).await?.is_err() {
626 return Err(ApiError::err("community_user_already_banned").into());
630 // Remove/Restore their data if that's desired
631 if data.remove_data {
633 blocking(context.pool(), move |conn: &'_ _| {
634 Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
639 // TODO Diesel doesn't allow updates with joins, so this has to be a loop
640 let comments = blocking(context.pool(), move |conn| {
641 CommentQueryBuilder::create(conn)
642 .creator_id(banned_person_id)
643 .community_id(community_id)
644 .limit(std::i64::MAX)
649 for comment_view in &comments {
650 let comment_id = comment_view.comment.id;
651 blocking(context.pool(), move |conn: &'_ _| {
652 Comment::update_removed(conn, comment_id, true)
659 // TODO eventually do correct expires
660 let expires = match data.expires {
661 Some(time) => Some(naive_from_unix(time)),
665 let form = ModBanFromCommunityForm {
666 mod_person_id: local_user_view.person.id,
667 other_person_id: data.person_id,
668 community_id: data.community_id,
669 reason: data.reason.to_owned(),
670 banned: Some(data.ban),
673 blocking(context.pool(), move |conn| {
674 ModBanFromCommunity::create(conn, &form)
678 let person_id = data.person_id;
679 let person_view = blocking(context.pool(), move |conn| {
680 PersonViewSafe::read(conn, person_id)
684 let res = BanFromCommunityResponse {
689 context.chat_server().do_send(SendCommunityRoomMessage {
690 op: UserOperation::BanFromCommunity,
691 response: res.clone(),
700 #[async_trait::async_trait(?Send)]
701 impl Perform for AddModToCommunity {
702 type Response = AddModToCommunityResponse;
706 context: &Data<LemmyContext>,
707 websocket_id: Option<ConnectionId>,
708 ) -> Result<AddModToCommunityResponse, LemmyError> {
709 let data: &AddModToCommunity = &self;
710 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
712 let community_id = data.community_id;
714 // Verify that only mods or admins can add mod
715 is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
717 // Update in local database
718 let community_moderator_form = CommunityModeratorForm {
719 community_id: data.community_id,
720 person_id: data.person_id,
723 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
724 if blocking(context.pool(), join).await?.is_err() {
725 return Err(ApiError::err("community_moderator_already_exists").into());
728 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
729 if blocking(context.pool(), leave).await?.is_err() {
730 return Err(ApiError::err("community_moderator_already_exists").into());
735 let form = ModAddCommunityForm {
736 mod_person_id: local_user_view.person.id,
737 other_person_id: data.person_id,
738 community_id: data.community_id,
739 removed: Some(!data.added),
741 blocking(context.pool(), move |conn| {
742 ModAddCommunity::create(conn, &form)
746 // Send to federated instances
747 let updated_mod_id = data.person_id;
748 let updated_mod = blocking(context.pool(), move |conn| {
749 Person::read(conn, updated_mod_id)
752 let community = blocking(context.pool(), move |conn| {
753 Community::read(conn, community_id)
758 .send_add_mod(&local_user_view.person, updated_mod, context)
762 .send_remove_mod(&local_user_view.person, updated_mod, context)
766 // Note: in case a remote mod is added, this returns the old moderators list, it will only get
767 // updated once we receive an activity from the community (like `Announce/Add/Moderator`)
768 let community_id = data.community_id;
769 let moderators = blocking(context.pool(), move |conn| {
770 CommunityModeratorView::for_community(conn, community_id)
774 let res = AddModToCommunityResponse { moderators };
775 context.chat_server().do_send(SendCommunityRoomMessage {
776 op: UserOperation::AddModToCommunity,
777 response: res.clone(),
785 // TODO: we dont do anything for federation here, it should be updated the next time the community
786 // gets fetched. i hope we can get rid of the community creator role soon.
787 #[async_trait::async_trait(?Send)]
788 impl Perform for TransferCommunity {
789 type Response = GetCommunityResponse;
793 context: &Data<LemmyContext>,
794 _websocket_id: Option<ConnectionId>,
795 ) -> Result<GetCommunityResponse, LemmyError> {
796 let data: &TransferCommunity = &self;
797 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
799 let community_id = data.community_id;
800 let read_community = blocking(context.pool(), move |conn| {
801 Community::read(conn, community_id)
805 let site_creator_id = blocking(context.pool(), move |conn| {
806 Site::read(conn, 1).map(|s| s.creator_id)
810 let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
812 // Making sure the creator, if an admin, is at the top
813 let creator_index = admins
815 .position(|r| r.person.id == site_creator_id)
816 .context(location_info!())?;
817 let creator_person = admins.remove(creator_index);
818 admins.insert(0, creator_person);
820 // Make sure user is the creator, or an admin
821 if local_user_view.person.id != read_community.creator_id
824 .map(|a| a.person.id)
825 .any(|x| x == local_user_view.person.id)
827 return Err(ApiError::err("not_an_admin").into());
830 let community_id = data.community_id;
831 let new_creator = data.person_id;
832 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
833 if blocking(context.pool(), update).await?.is_err() {
834 return Err(ApiError::err("couldnt_update_community").into());
837 // You also have to re-do the community_moderator table, reordering it.
838 let community_id = data.community_id;
839 let mut community_mods = blocking(context.pool(), move |conn| {
840 CommunityModeratorView::for_community(conn, community_id)
843 let creator_index = community_mods
845 .position(|r| r.moderator.id == data.person_id)
846 .context(location_info!())?;
847 let creator_person = community_mods.remove(creator_index);
848 community_mods.insert(0, creator_person);
850 let community_id = data.community_id;
851 blocking(context.pool(), move |conn| {
852 CommunityModerator::delete_for_community(conn, community_id)
856 // TODO: this should probably be a bulk operation
857 for cmod in &community_mods {
858 let community_moderator_form = CommunityModeratorForm {
859 community_id: cmod.community.id,
860 person_id: cmod.moderator.id,
863 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
864 if blocking(context.pool(), join).await?.is_err() {
865 return Err(ApiError::err("community_moderator_already_exists").into());
870 let form = ModAddCommunityForm {
871 mod_person_id: local_user_view.person.id,
872 other_person_id: data.person_id,
873 community_id: data.community_id,
874 removed: Some(false),
876 blocking(context.pool(), move |conn| {
877 ModAddCommunity::create(conn, &form)
881 let community_id = data.community_id;
882 let person_id = local_user_view.person.id;
883 let community_view = match blocking(context.pool(), move |conn| {
884 CommunityView::read(conn, community_id, Some(person_id))
888 Ok(community) => community,
889 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
892 let community_id = data.community_id;
893 let moderators = match blocking(context.pool(), move |conn| {
894 CommunityModeratorView::for_community(conn, community_id)
898 Ok(moderators) => moderators,
899 Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
903 Ok(GetCommunityResponse {
911 fn send_community_websocket(
912 res: &CommunityResponse,
913 context: &Data<LemmyContext>,
914 websocket_id: Option<ConnectionId>,
917 // Strip out the person id and subscribed when sending to others
918 let mut res_sent = res.clone();
919 res_sent.community_view.subscribed = false;
921 context.chat_server().do_send(SendCommunityRoomMessage {
924 community_id: res.community_view.community.id,