]> Untitled Git - lemmy.git/commitdiff
Implement restricted community (only mods can post) (fixes #187) (#2235)
authorNutomic <me@nutomic.com>
Thu, 28 Apr 2022 20:32:32 +0000 (22:32 +0200)
committerGitHub <noreply@github.com>
Thu, 28 Apr 2022 20:32:32 +0000 (20:32 +0000)
* Implement restricted community (only mods can post) (fixes #187)

* review fixes

* fix tests

22 files changed:
Cargo.lock
crates/api/src/community/hide.rs [new file with mode: 0644]
crates/api/src/community/mod.rs
crates/api_crud/src/community/update.rs
crates/api_crud/src/post/create.rs
crates/apub/assets/lemmy/activities/community/update_community.json
crates/apub/assets/lemmy/objects/group.json
crates/apub/src/objects/community.rs
crates/apub/src/protocol/objects/group.rs
crates/apub_lib/Cargo.toml
crates/db_schema/src/impls/community.rs
crates/db_schema/src/schema.rs
crates/db_schema/src/source/community.rs
crates/db_views/src/comment_report_view.rs
crates/db_views/src/comment_view.rs
crates/db_views/src/post_report_view.rs
crates/db_views/src/post_view.rs
crates/db_views_actor/src/community_view.rs
migrations/2022-04-26-105145_only_mod_can_post/down.sql [new file with mode: 0644]
migrations/2022-04-26-105145_only_mod_can_post/up.sql [new file with mode: 0644]
src/api_routes.rs
src/code_migrations.rs

index 1ca618775e0ba2c1af41728739a0e121eb0e3dfc..2d8136477e4ee95bfbe1b8822f2b2e5d953ff764 100644 (file)
@@ -1928,6 +1928,7 @@ dependencies = [
  "reqwest",
  "reqwest-middleware",
  "serde",
+ "serde_json",
  "sha2",
  "tracing",
  "url",
diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs
new file mode 100644 (file)
index 0000000..a391001
--- /dev/null
@@ -0,0 +1,82 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  community::{CommunityResponse, HideCommunity},
+  get_local_user_view_from_jwt,
+  is_admin,
+};
+use lemmy_apub::protocol::activities::community::update::UpdateCommunity;
+use lemmy_db_schema::{
+  naive_now,
+  source::{
+    community::{Community, CommunityForm},
+    moderator::{ModHideCommunity, ModHideCommunityForm},
+  },
+  traits::Crud,
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{send::send_community_ws_message, LemmyContext, UserOperationCrud};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for HideCommunity {
+  type Response = CommunityResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &HideCommunity = self;
+
+    // Verify its a admin (only admin can hide or unhide it)
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+    is_admin(&local_user_view)?;
+
+    let community_id = data.community_id;
+    let read_community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+
+    let community_form = CommunityForm {
+      name: read_community.name,
+      title: read_community.title,
+      description: read_community.description.to_owned(),
+      hidden: Some(data.hidden),
+      updated: Some(naive_now()),
+      ..CommunityForm::default()
+    };
+
+    let mod_hide_community_form = ModHideCommunityForm {
+      community_id: data.community_id,
+      mod_person_id: local_user_view.person.id,
+      reason: data.reason.clone(),
+      hidden: Some(data.hidden),
+    };
+
+    let community_id = data.community_id;
+    let updated_community = blocking(context.pool(), move |conn| {
+      Community::update(conn, community_id, &community_form)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_community_hidden_status"))?;
+
+    blocking(context.pool(), move |conn| {
+      ModHideCommunity::create(conn, &mod_hide_community_form)
+    })
+    .await??;
+
+    UpdateCommunity::send(
+      updated_community.into(),
+      &local_user_view.person.into(),
+      context,
+    )
+    .await?;
+
+    let op = UserOperationCrud::EditCommunity;
+    send_community_ws_message(data.community_id, op, websocket_id, None, context).await
+  }
+}
index 8bf2ed5461ce55d55079e98336aa3ffd3b1e6337..fda08265760792fabfeca9137244cab06d3630fd 100644 (file)
@@ -2,4 +2,5 @@ mod add_mod;
 mod ban;
 mod block;
 mod follow;
+mod hide;
 mod transfer;
index b3b89368eec538507add3d2dad6ce7fb7fb34b3f..6a98180b4fa9f31bfd99521a3b66e0c4814a6fa8 100644 (file)
@@ -3,19 +3,15 @@ use actix_web::web::Data;
 use lemmy_api_common::{
   blocking,
   check_image_has_local_domain,
-  community::{CommunityResponse, EditCommunity, HideCommunity},
+  community::{CommunityResponse, EditCommunity},
   get_local_user_view_from_jwt,
-  is_admin,
 };
 use lemmy_apub::protocol::activities::community::update::UpdateCommunity;
 use lemmy_db_schema::{
   diesel_option_overwrite_to_url,
   naive_now,
   newtypes::PersonId,
-  source::{
-    community::{Community, CommunityForm},
-    moderator::{ModHideCommunity, ModHideCommunityForm},
-  },
+  source::community::{Community, CommunityForm},
   traits::Crud,
 };
 use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
@@ -65,11 +61,9 @@ impl PerformCrud for EditCommunity {
       name: read_community.name,
       title: data.title.to_owned().unwrap_or(read_community.title),
       description: data.description.to_owned(),
-      public_key: read_community.public_key,
       icon,
       banner,
       nsfw: data.nsfw,
-      hidden: Some(read_community.hidden),
       updated: Some(naive_now()),
       ..CommunityForm::default()
     };
@@ -92,70 +86,3 @@ impl PerformCrud for EditCommunity {
     send_community_ws_message(data.community_id, op, websocket_id, None, context).await
   }
 }
-
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for HideCommunity {
-  type Response = CommunityResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommunityResponse, LemmyError> {
-    let data: &HideCommunity = self;
-
-    // Verify its a admin (only admin can hide or unhide it)
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-    is_admin(&local_user_view)?;
-
-    let community_id = data.community_id;
-    let read_community = blocking(context.pool(), move |conn| {
-      Community::read(conn, community_id)
-    })
-    .await??;
-
-    let community_form = CommunityForm {
-      name: read_community.name,
-      title: read_community.title,
-      description: read_community.description.to_owned(),
-      public_key: read_community.public_key,
-      icon: Some(read_community.icon),
-      banner: Some(read_community.banner),
-      nsfw: Some(read_community.nsfw),
-      updated: Some(naive_now()),
-      hidden: Some(data.hidden),
-      ..CommunityForm::default()
-    };
-
-    let mod_hide_community_form = ModHideCommunityForm {
-      community_id: data.community_id,
-      mod_person_id: local_user_view.person.id,
-      reason: data.reason.clone(),
-      hidden: Some(data.hidden),
-    };
-
-    let community_id = data.community_id;
-    let updated_community = blocking(context.pool(), move |conn| {
-      Community::update(conn, community_id, &community_form)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_community_hidden_status"))?;
-
-    blocking(context.pool(), move |conn| {
-      ModHideCommunity::create(conn, &mod_hide_community_form)
-    })
-    .await??;
-
-    UpdateCommunity::send(
-      updated_community.into(),
-      &local_user_view.person.into(),
-      context,
-    )
-    .await?;
-
-    let op = UserOperationCrud::EditCommunity;
-    send_community_ws_message(data.community_id, op, websocket_id, None, context).await
-  }
-}
index 4739666067fff93b0d7d6b2c78d467a1ae93bd11..eb162e01310c9a049bb39c0c52649036c4c65a78 100644 (file)
@@ -16,9 +16,13 @@ use lemmy_apub::{
   EndpointType,
 };
 use lemmy_db_schema::{
-  source::post::{Post, PostForm, PostLike, PostLikeForm},
+  source::{
+    community::Community,
+    post::{Post, PostForm, PostLike, PostLikeForm},
+  },
   traits::{Crud, Likeable},
 };
+use lemmy_db_views_actor::community_view::CommunityView;
 use lemmy_utils::{
   request::fetch_site_data,
   utils::{
@@ -62,6 +66,22 @@ impl PerformCrud for CreatePost {
     check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
     check_community_deleted_or_removed(data.community_id, context.pool()).await?;
 
+    let community_id = data.community_id;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+    if community.posting_restricted_to_mods {
+      let community_id = data.community_id;
+      let is_mod = blocking(context.pool(), move |conn| {
+        CommunityView::is_mod_or_admin(conn, local_user_view.local_user.person_id, community_id)
+      })
+      .await?;
+      if !is_mod {
+        return Err(LemmyError::from_message("only_mods_can_post_in_community"));
+      }
+    }
+
     // Fetch post links and pictrs cached image
     let data_url = data.url.as_ref();
     let (metadata_res, pictrs_thumbnail) =
index 275d6d2ba3dd3561dc7a010cb02784ae9774fe4e..bddae0f7d0d8b0736761dea504a94f92b40b8542 100644 (file)
@@ -15,6 +15,7 @@
     },
     "sensitive": false,
     "moderators": "http://enterprise.lemmy.ml/c/main/moderators",
+    "postingRestrictedToMods": false,
     "inbox": "http://enterprise.lemmy.ml/c/main/inbox",
     "outbox": "http://enterprise.lemmy.ml/c/main/outbox",
     "followers": "http://enterprise.lemmy.ml/c/main/followers",
index 7eddd86bdf9a9478f71ede189e74c99a1324f073..67ddd9556121deae1e36782cd394b24eb0927d6c 100644 (file)
@@ -20,6 +20,7 @@
   "inbox": "https://enterprise.lemmy.ml/c/tenforward/inbox",
   "followers": "https://enterprise.lemmy.ml/c/tenforward/followers",
   "moderators": "https://enterprise.lemmy.ml/c/tenforward/moderators",
+  "postingRestrictedToMods": false,
   "endpoints": {
     "sharedInbox": "https://enterprise.lemmy.ml/inbox"
   },
index 342a4080e198b5d18a336fe275693c942fbf9589..59489d3e4795cd91a3737f84416b58a771c533f1 100644 (file)
@@ -103,6 +103,7 @@ impl ApubObject for ApubCommunity {
       public_key: self.get_public_key()?,
       published: Some(convert_datetime(self.published)),
       updated: self.updated.map(convert_datetime),
+      posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
     };
     Ok(group)
   }
index 410c9e87f6148cc89d57f9e5c0affd990816a37f..57d0ae157a6daff0a2ddaf197eeedd97479a25de 100644 (file)
@@ -50,6 +50,8 @@ pub struct Group {
   pub(crate) sensitive: Option<bool>,
   // lemmy extension
   pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
+  // lemmy extension
+  pub(crate) posting_restricted_to_mods: Option<bool>,
   pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
   pub(crate) endpoints: Option<Endpoints>,
   pub(crate) published: Option<DateTime<FixedOffset>>,
@@ -96,6 +98,7 @@ impl Group {
       followers_url: Some(self.followers.into()),
       inbox_url: Some(self.inbox.into()),
       shared_inbox_url: Some(self.endpoints.map(|e| e.shared_inbox.into())),
+      posting_restricted_to_mods: self.posting_restricted_to_mods,
     }
   }
 }
index d3642c046e57c98d82b17fd32d4c4b8ea66d9010..1b2cabbf4566d778cdd5f74a60be7e90fad90aa4 100644 (file)
@@ -14,6 +14,7 @@ chrono = "0.4.19"
 serde = { version = "1.0.136", features = ["derive"] }
 async-trait = "0.1.53"
 url = { version = "2.2.2", features = ["serde"] }
+serde_json = { version = "1.0.79", features = ["preserve_order"] }
 anyhow = "1.0.56"
 reqwest = { version = "0.11.10", features = ["json"] }
 reqwest-middleware = "0.1.5"
index a7b50996398c59f0ec12f2a851796caab081e579..fb8bd6e4a1cbdb6d8be34bcf16a2d6a6218d0fd3 100644 (file)
@@ -43,6 +43,7 @@ mod safe_type {
     icon,
     banner,
     hidden,
+    posting_restricted_to_mods,
   );
 
   impl ToSafe for Community {
@@ -63,6 +64,7 @@ mod safe_type {
         icon,
         banner,
         hidden,
+        posting_restricted_to_mods,
       )
     }
   }
@@ -373,6 +375,7 @@ mod tests {
       inbox_url: inserted_community.inbox_url.to_owned(),
       shared_inbox_url: None,
       hidden: false,
+      posting_restricted_to_mods: false,
     };
 
     let community_follower_form = CommunityFollowerForm {
index 3ff563175d046e872d74d6bf3f4a6fd278e26649..3662d09598c54588d5a9cd1c6cb76b3b7f724cad 100644 (file)
@@ -94,6 +94,7 @@ table! {
         inbox_url -> Varchar,
         shared_inbox_url -> Nullable<Varchar>,
         hidden -> Bool,
+        posting_restricted_to_mods -> Bool,
     }
 }
 
index 35b695db0448b0453a43e11b034d3738eb9447c8..3e8bbf17122c19e7b0aee6959e71d1ab74a03fe8 100644 (file)
@@ -27,6 +27,7 @@ pub struct Community {
   pub inbox_url: DbUrl,
   pub shared_inbox_url: Option<DbUrl>,
   pub hidden: bool,
+  pub posting_restricted_to_mods: bool,
 }
 
 /// A safe representation of community, without the sensitive info
@@ -47,6 +48,7 @@ pub struct CommunitySafe {
   pub icon: Option<DbUrl>,
   pub banner: Option<DbUrl>,
   pub hidden: bool,
+  pub posting_restricted_to_mods: bool,
 }
 
 #[derive(Insertable, AsChangeset, Debug, Default)]
@@ -71,6 +73,7 @@ pub struct CommunityForm {
   pub inbox_url: Option<DbUrl>,
   pub shared_inbox_url: Option<Option<DbUrl>>,
   pub hidden: Option<bool>,
+  pub posting_restricted_to_mods: Option<bool>,
 }
 
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
index 63701db93707637fc5a21d845f37dd0ec3bc2f4e..73224be599394dba7b321f902f0c0dc8fc105c1d 100644 (file)
@@ -431,6 +431,7 @@ mod tests {
         updated: None,
         banner: None,
         hidden: false,
+        posting_restricted_to_mods: false,
         published: inserted_community.published,
       },
       creator: PersonSafe {
index 4a7f96b6acece8cfe782dd869bc007d695654bf2..da42bb134db2a6880a223c82b0f85f3fc52ab1fe 100644 (file)
@@ -709,6 +709,7 @@ mod tests {
         updated: None,
         banner: None,
         hidden: false,
+        posting_restricted_to_mods: false,
         published: inserted_community.published,
       },
       counts: CommentAggregates {
index 66341d67bf8197e54e3583baa5d53b595c3c3cca..0f56c2620b6ee8cd707be871d33006515b433b35 100644 (file)
@@ -413,6 +413,7 @@ mod tests {
         updated: None,
         banner: None,
         hidden: false,
+        posting_restricted_to_mods: false,
         published: inserted_community.published,
       },
       creator: PersonSafe {
index a3df66afa9c5034ea7b31557ab9c8983a5d95ad9..dd97cd8d493b344f0ead38f98ace9b70c4c4a18b 100644 (file)
@@ -703,6 +703,7 @@ mod tests {
         updated: None,
         banner: None,
         hidden: false,
+        posting_restricted_to_mods: false,
         published: inserted_community.published,
       },
       counts: PostAggregates {
index a7b711b43726c805e6d541200b86889f2b63bc25..2063fb14fb7cf90160aae4d16742036a0ec8f806 100644 (file)
@@ -74,28 +74,29 @@ impl CommunityView {
     })
   }
 
-  // TODO: this function is only used by is_mod_or_admin() below, can probably be merged
-  fn community_mods_and_admins(
-    conn: &PgConnection,
-    community_id: CommunityId,
-  ) -> Result<Vec<PersonId>, Error> {
-    let mut mods_and_admins: Vec<PersonId> = Vec::new();
-    mods_and_admins.append(
-      &mut CommunityModeratorView::for_community(conn, community_id)
-        .map(|v| v.into_iter().map(|m| m.moderator.id).collect())?,
-    );
-    mods_and_admins.append(
-      &mut PersonViewSafe::admins(conn).map(|v| v.into_iter().map(|a| a.person.id).collect())?,
-    );
-    Ok(mods_and_admins)
-  }
-
   pub fn is_mod_or_admin(
     conn: &PgConnection,
     person_id: PersonId,
     community_id: CommunityId,
   ) -> bool {
-    Self::community_mods_and_admins(conn, community_id)
+    let is_mod = CommunityModeratorView::for_community(conn, community_id)
+      .map(|v| {
+        v.into_iter()
+          .map(|m| m.moderator.id)
+          .collect::<Vec<PersonId>>()
+      })
+      .unwrap_or_default()
+      .contains(&person_id);
+    if is_mod {
+      return true;
+    }
+
+    PersonViewSafe::admins(conn)
+      .map(|v| {
+        v.into_iter()
+          .map(|a| a.person.id)
+          .collect::<Vec<PersonId>>()
+      })
       .unwrap_or_default()
       .contains(&person_id)
   }
diff --git a/migrations/2022-04-26-105145_only_mod_can_post/down.sql b/migrations/2022-04-26-105145_only_mod_can_post/down.sql
new file mode 100644 (file)
index 0000000..a9c95bf
--- /dev/null
@@ -0,0 +1 @@
+alter table community drop column posting_restricted_to_mods;
\ No newline at end of file
diff --git a/migrations/2022-04-26-105145_only_mod_can_post/up.sql b/migrations/2022-04-26-105145_only_mod_can_post/up.sql
new file mode 100644 (file)
index 0000000..fbc5698
--- /dev/null
@@ -0,0 +1 @@
+alter table community add column posting_restricted_to_mods boolean default false;
\ No newline at end of file
index 757e40a1102e8088d84bb19808f0601bd9e8c236..36310386018cdf914ca11811ce37d7fda37036fd 100644 (file)
@@ -49,7 +49,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .wrap(rate_limit.message())
           .route("", web::get().to(route_get_crud::<GetCommunity>))
           .route("", web::put().to(route_post_crud::<EditCommunity>))
-          .route("/hide", web::put().to(route_post_crud::<HideCommunity>))
+          .route("/hide", web::put().to(route_post::<HideCommunity>))
           .route("/list", web::get().to(route_get_crud::<ListCommunities>))
           .route("/follow", web::post().to(route_post::<FollowCommunity>))
           .route("/block", web::post().to(route_post::<BlockCommunity>))
index d626c7de70c066ae40ead53be39accf56d931352..69e419161648456d9961e54c68d2ac7a7dc9b80d 100644 (file)
@@ -107,22 +107,15 @@ fn community_updates_2020_04_02(
       name: ccommunity.name.to_owned(),
       title: ccommunity.title.to_owned(),
       description: ccommunity.description.to_owned(),
-      removed: None,
-      deleted: None,
-      nsfw: None,
-      updated: None,
       hidden: Some(false),
       actor_id: Some(community_actor_id.to_owned()),
       local: Some(ccommunity.local),
       private_key: Some(Some(keypair.private_key)),
       public_key: keypair.public_key,
       last_refreshed_at: Some(naive_now()),
-      published: None,
       icon: Some(ccommunity.icon.to_owned()),
       banner: Some(ccommunity.banner.to_owned()),
-      followers_url: None,
-      inbox_url: None,
-      shared_inbox_url: None,
+      ..Default::default()
     };
 
     Community::update(conn, ccommunity.id, &form)?;