2 api::{get_user_from_jwt, get_user_from_jwt_opt, is_admin, is_mod_or_admin, Perform},
5 messages::{GetCommunityUsersOnline, JoinCommunityRoom, SendCommunityRoomMessage},
10 use actix_web::web::Data;
12 use lemmy_api_structs::{blocking, community::*};
15 comment_view::CommentQueryBuilder,
18 diesel_option_overwrite,
31 apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
33 utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
38 use std::str::FromStr;
40 #[async_trait::async_trait(?Send)]
41 impl Perform for GetCommunity {
42 type Response = GetCommunityResponse;
46 context: &Data<LemmyContext>,
47 websocket_id: Option<ConnectionId>,
48 ) -> Result<GetCommunityResponse, LemmyError> {
49 let data: &GetCommunity = &self;
50 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
51 let user_id = user.map(|u| u.id);
53 let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
54 let community = match data.id {
55 Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
56 None => match blocking(context.pool(), move |conn| {
57 Community::read_from_name(conn, &name)
61 Ok(community) => community,
62 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
66 let community_id = community.id;
67 let community_view = match blocking(context.pool(), move |conn| {
68 CommunityView::read(conn, community_id, user_id)
72 Ok(community) => community,
73 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
76 let community_id = community.id;
77 let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
78 CommunityModeratorView::for_community(conn, community_id)
82 Ok(moderators) => moderators,
83 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
86 if let Some(id) = websocket_id {
89 .do_send(JoinCommunityRoom { community_id, id });
94 .send(GetCommunityUsersOnline { community_id })
98 let res = GetCommunityResponse {
99 community: community_view,
109 #[async_trait::async_trait(?Send)]
110 impl Perform for CreateCommunity {
111 type Response = CommunityResponse;
115 context: &Data<LemmyContext>,
116 _websocket_id: Option<ConnectionId>,
117 ) -> Result<CommunityResponse, LemmyError> {
118 let data: &CreateCommunity = &self;
119 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
121 check_slurs(&data.name)?;
122 check_slurs(&data.title)?;
123 check_slurs_opt(&data.description)?;
125 if !is_valid_community_name(&data.name) {
126 return Err(APIError::err("invalid_community_name").into());
129 // Double check for duplicate community actor_ids
130 let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
131 let actor_id_cloned = actor_id.to_owned();
132 let community_dupe = blocking(context.pool(), move |conn| {
133 Community::read_from_actor_id(conn, &actor_id_cloned)
136 if community_dupe.is_ok() {
137 return Err(APIError::err("community_already_exists").into());
140 // When you create a community, make sure the user becomes a moderator and a follower
141 let keypair = generate_actor_keypair()?;
143 let community_form = CommunityForm {
144 name: data.name.to_owned(),
145 title: data.title.to_owned(),
146 description: data.description.to_owned(),
147 icon: Some(data.icon.to_owned()),
148 banner: Some(data.banner.to_owned()),
149 category_id: data.category_id,
155 actor_id: Some(actor_id),
157 private_key: Some(keypair.private_key),
158 public_key: Some(keypair.public_key),
159 last_refreshed_at: None,
163 let inserted_community = match blocking(context.pool(), move |conn| {
164 Community::create(conn, &community_form)
168 Ok(community) => community,
169 Err(_e) => return Err(APIError::err("community_already_exists").into()),
172 let community_moderator_form = CommunityModeratorForm {
173 community_id: inserted_community.id,
177 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
178 if blocking(context.pool(), join).await?.is_err() {
179 return Err(APIError::err("community_moderator_already_exists").into());
182 let community_follower_form = CommunityFollowerForm {
183 community_id: inserted_community.id,
187 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
188 if blocking(context.pool(), follow).await?.is_err() {
189 return Err(APIError::err("community_follower_already_exists").into());
192 let user_id = user.id;
193 let community_view = blocking(context.pool(), move |conn| {
194 CommunityView::read(conn, inserted_community.id, Some(user_id))
198 Ok(CommunityResponse {
199 community: community_view,
204 #[async_trait::async_trait(?Send)]
205 impl Perform for EditCommunity {
206 type Response = CommunityResponse;
210 context: &Data<LemmyContext>,
211 websocket_id: Option<ConnectionId>,
212 ) -> Result<CommunityResponse, LemmyError> {
213 let data: &EditCommunity = &self;
214 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
216 check_slurs(&data.title)?;
217 check_slurs_opt(&data.description)?;
219 // Verify its a mod (only mods can edit it)
220 let edit_id = data.edit_id;
221 let mods: Vec<i32> = blocking(context.pool(), move |conn| {
222 CommunityModeratorView::for_community(conn, edit_id)
223 .map(|v| v.into_iter().map(|m| m.user_id).collect())
226 if !mods.contains(&user.id) {
227 return Err(APIError::err("not_a_moderator").into());
230 let edit_id = data.edit_id;
232 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
234 let icon = diesel_option_overwrite(&data.icon);
235 let banner = diesel_option_overwrite(&data.banner);
237 let community_form = CommunityForm {
238 name: read_community.name,
239 title: data.title.to_owned(),
240 description: data.description.to_owned(),
243 category_id: data.category_id.to_owned(),
244 creator_id: read_community.creator_id,
245 removed: Some(read_community.removed),
246 deleted: Some(read_community.deleted),
248 updated: Some(naive_now()),
249 actor_id: Some(read_community.actor_id),
250 local: read_community.local,
251 private_key: read_community.private_key,
252 public_key: read_community.public_key,
253 last_refreshed_at: None,
257 let edit_id = data.edit_id;
258 match blocking(context.pool(), move |conn| {
259 Community::update(conn, edit_id, &community_form)
263 Ok(community) => community,
264 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
267 // TODO there needs to be some kind of an apub update
268 // process for communities and users
270 let edit_id = data.edit_id;
271 let user_id = user.id;
272 let community_view = blocking(context.pool(), move |conn| {
273 CommunityView::read(conn, edit_id, Some(user_id))
277 let res = CommunityResponse {
278 community: community_view,
281 send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
287 #[async_trait::async_trait(?Send)]
288 impl Perform for DeleteCommunity {
289 type Response = CommunityResponse;
293 context: &Data<LemmyContext>,
294 websocket_id: Option<ConnectionId>,
295 ) -> Result<CommunityResponse, LemmyError> {
296 let data: &DeleteCommunity = &self;
297 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
299 // Verify its the creator (only a creator can delete the community)
300 let edit_id = data.edit_id;
302 blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
303 if read_community.creator_id != user.id {
304 return Err(APIError::err("no_community_edit_allowed").into());
308 let edit_id = data.edit_id;
309 let deleted = data.deleted;
310 let updated_community = match blocking(context.pool(), move |conn| {
311 Community::update_deleted(conn, edit_id, deleted)
315 Ok(community) => community,
316 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
319 // Send apub messages
321 updated_community.send_delete(&user, context).await?;
323 updated_community.send_undo_delete(&user, context).await?;
326 let edit_id = data.edit_id;
327 let user_id = user.id;
328 let community_view = blocking(context.pool(), move |conn| {
329 CommunityView::read(conn, edit_id, Some(user_id))
333 let res = CommunityResponse {
334 community: community_view,
337 send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
343 #[async_trait::async_trait(?Send)]
344 impl Perform for RemoveCommunity {
345 type Response = CommunityResponse;
349 context: &Data<LemmyContext>,
350 websocket_id: Option<ConnectionId>,
351 ) -> Result<CommunityResponse, LemmyError> {
352 let data: &RemoveCommunity = &self;
353 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
355 // Verify its an admin (only an admin can remove a community)
356 is_admin(context.pool(), user.id).await?;
359 let edit_id = data.edit_id;
360 let removed = data.removed;
361 let updated_community = match blocking(context.pool(), move |conn| {
362 Community::update_removed(conn, edit_id, removed)
366 Ok(community) => community,
367 Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
371 let expires = match data.expires {
372 Some(time) => Some(naive_from_unix(time)),
375 let form = ModRemoveCommunityForm {
376 mod_user_id: user.id,
377 community_id: data.edit_id,
378 removed: Some(removed),
379 reason: data.reason.to_owned(),
382 blocking(context.pool(), move |conn| {
383 ModRemoveCommunity::create(conn, &form)
389 updated_community.send_remove(&user, context).await?;
391 updated_community.send_undo_remove(&user, context).await?;
394 let edit_id = data.edit_id;
395 let user_id = user.id;
396 let community_view = blocking(context.pool(), move |conn| {
397 CommunityView::read(conn, edit_id, Some(user_id))
401 let res = CommunityResponse {
402 community: community_view,
405 send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
411 #[async_trait::async_trait(?Send)]
412 impl Perform for ListCommunities {
413 type Response = ListCommunitiesResponse;
417 context: &Data<LemmyContext>,
418 _websocket_id: Option<ConnectionId>,
419 ) -> Result<ListCommunitiesResponse, LemmyError> {
420 let data: &ListCommunities = &self;
421 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
423 let user_id = match &user {
424 Some(user) => Some(user.id),
428 let show_nsfw = match &user {
429 Some(user) => user.show_nsfw,
433 let sort = SortType::from_str(&data.sort)?;
435 let page = data.page;
436 let limit = data.limit;
437 let communities = blocking(context.pool(), move |conn| {
438 CommunityQueryBuilder::create(conn)
441 .show_nsfw(show_nsfw)
449 Ok(ListCommunitiesResponse { communities })
453 #[async_trait::async_trait(?Send)]
454 impl Perform for FollowCommunity {
455 type Response = CommunityResponse;
459 context: &Data<LemmyContext>,
460 _websocket_id: Option<ConnectionId>,
461 ) -> Result<CommunityResponse, LemmyError> {
462 let data: &FollowCommunity = &self;
463 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
465 let community_id = data.community_id;
466 let community = blocking(context.pool(), move |conn| {
467 Community::read(conn, community_id)
470 let community_follower_form = CommunityFollowerForm {
471 community_id: data.community_id,
477 let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
478 if blocking(context.pool(), follow).await?.is_err() {
479 return Err(APIError::err("community_follower_already_exists").into());
483 move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
484 if blocking(context.pool(), unfollow).await?.is_err() {
485 return Err(APIError::err("community_follower_already_exists").into());
488 } else if data.follow {
489 // Dont actually add to the community followers here, because you need
490 // to wait for the accept
491 user.send_follow(&community.actor_id()?, context).await?;
493 user.send_unfollow(&community.actor_id()?, context).await?;
494 let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
495 if blocking(context.pool(), unfollow).await?.is_err() {
496 return Err(APIError::err("community_follower_already_exists").into());
499 // TODO: this needs to return a "pending" state, until Accept is received from the remote server
501 let community_id = data.community_id;
502 let user_id = user.id;
503 let community_view = blocking(context.pool(), move |conn| {
504 CommunityView::read(conn, community_id, Some(user_id))
508 Ok(CommunityResponse {
509 community: community_view,
514 #[async_trait::async_trait(?Send)]
515 impl Perform for GetFollowedCommunities {
516 type Response = GetFollowedCommunitiesResponse;
520 context: &Data<LemmyContext>,
521 _websocket_id: Option<ConnectionId>,
522 ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
523 let data: &GetFollowedCommunities = &self;
524 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
526 let user_id = user.id;
527 let communities = match blocking(context.pool(), move |conn| {
528 CommunityFollowerView::for_user(conn, user_id)
532 Ok(communities) => communities,
533 _ => return Err(APIError::err("system_err_login").into()),
537 Ok(GetFollowedCommunitiesResponse { communities })
541 #[async_trait::async_trait(?Send)]
542 impl Perform for BanFromCommunity {
543 type Response = BanFromCommunityResponse;
547 context: &Data<LemmyContext>,
548 websocket_id: Option<ConnectionId>,
549 ) -> Result<BanFromCommunityResponse, LemmyError> {
550 let data: &BanFromCommunity = &self;
551 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
553 let community_id = data.community_id;
554 let banned_user_id = data.user_id;
556 // Verify that only mods or admins can ban
557 is_mod_or_admin(context.pool(), user.id, community_id).await?;
559 let community_user_ban_form = CommunityUserBanForm {
560 community_id: data.community_id,
561 user_id: data.user_id,
565 let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
566 if blocking(context.pool(), ban).await?.is_err() {
567 return Err(APIError::err("community_user_already_banned").into());
570 let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
571 if blocking(context.pool(), unban).await?.is_err() {
572 return Err(APIError::err("community_user_already_banned").into());
576 // Remove/Restore their data if that's desired
577 if let Some(remove_data) = data.remove_data {
579 blocking(context.pool(), move |conn: &'_ _| {
580 Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
585 // Diesel doesn't allow updates with joins, so this has to be a loop
586 let comments = blocking(context.pool(), move |conn| {
587 CommentQueryBuilder::create(conn)
588 .for_creator_id(banned_user_id)
589 .for_community_id(community_id)
590 .limit(std::i64::MAX)
595 for comment in &comments {
596 let comment_id = comment.id;
597 blocking(context.pool(), move |conn: &'_ _| {
598 Comment::update_removed(conn, comment_id, remove_data)
605 // TODO eventually do correct expires
606 let expires = match data.expires {
607 Some(time) => Some(naive_from_unix(time)),
611 let form = ModBanFromCommunityForm {
612 mod_user_id: user.id,
613 other_user_id: data.user_id,
614 community_id: data.community_id,
615 reason: data.reason.to_owned(),
616 banned: Some(data.ban),
619 blocking(context.pool(), move |conn| {
620 ModBanFromCommunity::create(conn, &form)
624 let user_id = data.user_id;
625 let user_view = blocking(context.pool(), move |conn| {
626 UserView::get_user_secure(conn, user_id)
630 let res = BanFromCommunityResponse {
635 context.chat_server().do_send(SendCommunityRoomMessage {
636 op: UserOperation::BanFromCommunity,
637 response: res.clone(),
646 #[async_trait::async_trait(?Send)]
647 impl Perform for AddModToCommunity {
648 type Response = AddModToCommunityResponse;
652 context: &Data<LemmyContext>,
653 websocket_id: Option<ConnectionId>,
654 ) -> Result<AddModToCommunityResponse, LemmyError> {
655 let data: &AddModToCommunity = &self;
656 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
658 let community_moderator_form = CommunityModeratorForm {
659 community_id: data.community_id,
660 user_id: data.user_id,
663 let community_id = data.community_id;
665 // Verify that only mods or admins can add mod
666 is_mod_or_admin(context.pool(), user.id, community_id).await?;
669 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
670 if blocking(context.pool(), join).await?.is_err() {
671 return Err(APIError::err("community_moderator_already_exists").into());
674 let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
675 if blocking(context.pool(), leave).await?.is_err() {
676 return Err(APIError::err("community_moderator_already_exists").into());
681 let form = ModAddCommunityForm {
682 mod_user_id: user.id,
683 other_user_id: data.user_id,
684 community_id: data.community_id,
685 removed: Some(!data.added),
687 blocking(context.pool(), move |conn| {
688 ModAddCommunity::create(conn, &form)
692 let community_id = data.community_id;
693 let moderators = blocking(context.pool(), move |conn| {
694 CommunityModeratorView::for_community(conn, community_id)
698 let res = AddModToCommunityResponse { moderators };
700 context.chat_server().do_send(SendCommunityRoomMessage {
701 op: UserOperation::AddModToCommunity,
702 response: res.clone(),
711 #[async_trait::async_trait(?Send)]
712 impl Perform for TransferCommunity {
713 type Response = GetCommunityResponse;
717 context: &Data<LemmyContext>,
718 _websocket_id: Option<ConnectionId>,
719 ) -> Result<GetCommunityResponse, LemmyError> {
720 let data: &TransferCommunity = &self;
721 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
723 let community_id = data.community_id;
724 let read_community = blocking(context.pool(), move |conn| {
725 Community::read(conn, community_id)
729 let site_creator_id = blocking(context.pool(), move |conn| {
730 Site::read(conn, 1).map(|s| s.creator_id)
734 let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
736 let creator_index = admins
738 .position(|r| r.id == site_creator_id)
739 .context(location_info!())?;
740 let creator_user = admins.remove(creator_index);
741 admins.insert(0, creator_user);
743 // Make sure user is the creator, or an admin
744 if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
745 return Err(APIError::err("not_an_admin").into());
748 let community_id = data.community_id;
749 let new_creator = data.user_id;
750 let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
751 if blocking(context.pool(), update).await?.is_err() {
752 return Err(APIError::err("couldnt_update_community").into());
755 // You also have to re-do the community_moderator table, reordering it.
756 let community_id = data.community_id;
757 let mut community_mods = blocking(context.pool(), move |conn| {
758 CommunityModeratorView::for_community(conn, community_id)
761 let creator_index = community_mods
763 .position(|r| r.user_id == data.user_id)
764 .context(location_info!())?;
765 let creator_user = community_mods.remove(creator_index);
766 community_mods.insert(0, creator_user);
768 let community_id = data.community_id;
769 blocking(context.pool(), move |conn| {
770 CommunityModerator::delete_for_community(conn, community_id)
774 // TODO: this should probably be a bulk operation
775 for cmod in &community_mods {
776 let community_moderator_form = CommunityModeratorForm {
777 community_id: cmod.community_id,
778 user_id: cmod.user_id,
781 let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
782 if blocking(context.pool(), join).await?.is_err() {
783 return Err(APIError::err("community_moderator_already_exists").into());
788 let form = ModAddCommunityForm {
789 mod_user_id: user.id,
790 other_user_id: data.user_id,
791 community_id: data.community_id,
792 removed: Some(false),
794 blocking(context.pool(), move |conn| {
795 ModAddCommunity::create(conn, &form)
799 let community_id = data.community_id;
800 let user_id = user.id;
801 let community_view = match blocking(context.pool(), move |conn| {
802 CommunityView::read(conn, community_id, Some(user_id))
806 Ok(community) => community,
807 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
810 let community_id = data.community_id;
811 let moderators = match blocking(context.pool(), move |conn| {
812 CommunityModeratorView::for_community(conn, community_id)
816 Ok(moderators) => moderators,
817 Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
821 Ok(GetCommunityResponse {
822 community: community_view,
829 pub fn send_community_websocket(
830 res: &CommunityResponse,
831 context: &Data<LemmyContext>,
832 websocket_id: Option<ConnectionId>,
835 // Strip out the user id and subscribed when sending to others
836 let mut res_sent = res.clone();
837 res_sent.community.user_id = None;
838 res_sent.community.subscribed = None;
840 context.chat_server().do_send(SendCommunityRoomMessage {
843 community_id: res.community.id,