]> Untitled Git - lemmy.git/blobdiff - crates/db_schema/src/impls/community.rs
Implement federated user following (fixes #752) (#2577)
[lemmy.git] / crates / db_schema / src / impls / community.rs
index b2b3a6d8188d85c67d36d6ad8590c8d6c8f61245..5f29f1359b84afa93909fb31d5aaa6d24b6f530c 100644 (file)
@@ -1,33 +1,51 @@
 use crate::{
-  functions::lower,
-  naive_now,
   newtypes::{CommunityId, DbUrl, PersonId},
-  source::community::{
-    Community,
-    CommunityFollower,
-    CommunityFollowerForm,
-    CommunityForm,
-    CommunityModerator,
-    CommunityModeratorForm,
-    CommunityPersonBan,
-    CommunityPersonBanForm,
-    CommunitySafe,
+  schema::community::dsl::{actor_id, community, deleted, local, name, removed},
+  source::{
+    actor_language::{CommunityLanguage, SiteLanguage},
+    community::{
+      Community,
+      CommunityFollower,
+      CommunityFollowerForm,
+      CommunityInsertForm,
+      CommunityModerator,
+      CommunityModeratorForm,
+      CommunityPersonBan,
+      CommunityPersonBanForm,
+      CommunitySafe,
+      CommunityUpdateForm,
+    },
   },
   traits::{ApubActor, Bannable, Crud, DeleteableOrRemoveable, Followable, Joinable},
+  utils::{functions::lower, get_conn, DbPool},
+  SubscribedType,
 };
-use diesel::{
-  dsl::*,
-  result::Error,
-  ExpressionMethods,
-  PgConnection,
-  QueryDsl,
-  RunQueryDsl,
-  TextExpressionMethods,
-};
-use url::Url;
+use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods};
+use diesel_async::RunQueryDsl;
 
 mod safe_type {
-  use crate::{schema::community::*, source::community::Community, traits::ToSafe};
+  use crate::{
+    schema::community::{
+      actor_id,
+      banner,
+      deleted,
+      description,
+      hidden,
+      icon,
+      id,
+      instance_id,
+      local,
+      name,
+      nsfw,
+      posting_restricted_to_mods,
+      published,
+      removed,
+      title,
+      updated,
+    },
+    source::community::Community,
+    traits::ToSafe,
+  };
 
   type Columns = (
     id,
@@ -43,6 +61,9 @@ mod safe_type {
     local,
     icon,
     banner,
+    hidden,
+    posting_restricted_to_mods,
+    instance_id,
   );
 
   impl ToSafe for Community {
@@ -62,111 +83,100 @@ mod safe_type {
         local,
         icon,
         banner,
+        hidden,
+        posting_restricted_to_mods,
+        instance_id,
       )
     }
   }
 }
 
+#[async_trait]
 impl Crud for Community {
-  type Form = CommunityForm;
+  type InsertForm = CommunityInsertForm;
+  type UpdateForm = CommunityUpdateForm;
   type IdType = CommunityId;
-  fn read(conn: &PgConnection, community_id: CommunityId) -> Result<Self, Error> {
-    use crate::schema::community::dsl::*;
-    community.find(community_id).first::<Self>(conn)
+  async fn read(pool: &DbPool, community_id: CommunityId) -> Result<Self, Error> {
+    let conn = &mut get_conn(pool).await?;
+    community.find(community_id).first::<Self>(conn).await
   }
 
-  fn delete(conn: &PgConnection, community_id: CommunityId) -> Result<usize, Error> {
-    use crate::schema::community::dsl::*;
-    diesel::delete(community.find(community_id)).execute(conn)
+  async fn delete(pool: &DbPool, community_id: CommunityId) -> Result<usize, Error> {
+    let conn = &mut get_conn(pool).await?;
+    diesel::delete(community.find(community_id))
+      .execute(conn)
+      .await
   }
 
-  fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result<Self, Error> {
-    use crate::schema::community::dsl::*;
-    insert_into(community)
-      .values(new_community)
+  async fn create(pool: &DbPool, form: &Self::InsertForm) -> Result<Self, Error> {
+    let conn = &mut get_conn(pool).await?;
+    let community_ = insert_into(community)
+      .values(form)
+      .on_conflict(actor_id)
+      .do_update()
+      .set(form)
       .get_result::<Self>(conn)
+      .await?;
+
+    let site_languages = SiteLanguage::read_local(pool).await;
+    if let Ok(langs) = site_languages {
+      // if site exists, init user with site languages
+      CommunityLanguage::update(pool, langs, community_.id).await?;
+    } else {
+      // otherwise, init with all languages (this only happens during tests)
+      CommunityLanguage::update(pool, vec![], community_.id).await?;
+    }
+
+    Ok(community_)
   }
 
-  fn update(
-    conn: &PgConnection,
+  async fn update(
+    pool: &DbPool,
     community_id: CommunityId,
-    new_community: &CommunityForm,
+    form: &Self::UpdateForm,
   ) -> Result<Self, Error> {
-    use crate::schema::community::dsl::*;
+    let conn = &mut get_conn(pool).await?;
     diesel::update(community.find(community_id))
-      .set(new_community)
-      .get_result::<Self>(conn)
-  }
-}
-
-impl Community {
-  pub fn update_deleted(
-    conn: &PgConnection,
-    community_id: CommunityId,
-    new_deleted: bool,
-  ) -> Result<Community, Error> {
-    use crate::schema::community::dsl::*;
-    diesel::update(community.find(community_id))
-      .set((deleted.eq(new_deleted), updated.eq(naive_now())))
-      .get_result::<Self>(conn)
-  }
-
-  pub fn update_removed(
-    conn: &PgConnection,
-    community_id: CommunityId,
-    new_removed: bool,
-  ) -> Result<Community, Error> {
-    use crate::schema::community::dsl::*;
-    diesel::update(community.find(community_id))
-      .set((removed.eq(new_removed), updated.eq(naive_now())))
-      .get_result::<Self>(conn)
-  }
-
-  pub fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<DbUrl>, Error> {
-    use crate::schema::community::dsl::*;
-    community.select(actor_id).distinct().load::<DbUrl>(conn)
-  }
-
-  pub fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
-    use crate::schema::community::dsl::*;
-    insert_into(community)
-      .values(community_form)
-      .on_conflict(actor_id)
-      .do_update()
-      .set(community_form)
+      .set(form)
       .get_result::<Self>(conn)
+      .await
   }
 }
 
+#[async_trait]
 impl Joinable for CommunityModerator {
   type Form = CommunityModeratorForm;
-  fn join(
-    conn: &PgConnection,
+  async fn join(
+    pool: &DbPool,
     community_moderator_form: &CommunityModeratorForm,
   ) -> Result<Self, Error> {
-    use crate::schema::community_moderator::dsl::*;
+    use crate::schema::community_moderator::dsl::community_moderator;
+    let conn = &mut get_conn(pool).await?;
     insert_into(community_moderator)
       .values(community_moderator_form)
       .get_result::<Self>(conn)
+      .await
   }
 
-  fn leave(
-    conn: &PgConnection,
+  async fn leave(
+    pool: &DbPool,
     community_moderator_form: &CommunityModeratorForm,
   ) -> Result<usize, Error> {
-    use crate::schema::community_moderator::dsl::*;
+    use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id};
+    let conn = &mut get_conn(pool).await?;
     diesel::delete(
       community_moderator
         .filter(community_id.eq(community_moderator_form.community_id))
         .filter(person_id.eq(community_moderator_form.person_id)),
     )
     .execute(conn)
+    .await
   }
 }
 
 impl DeleteableOrRemoveable for CommunitySafe {
   fn blank_out_deleted_or_removed_info(mut self) -> Self {
-    self.title = "".into();
+    self.title = String::new();
     self.description = None;
     self.icon = None;
     self.banner = None;
@@ -176,7 +186,7 @@ impl DeleteableOrRemoveable for CommunitySafe {
 
 impl DeleteableOrRemoveable for Community {
   fn blank_out_deleted_or_removed_info(mut self) -> Self {
-    self.title = "".into();
+    self.title = String::new();
     self.description = None;
     self.icon = None;
     self.banner = None;
@@ -185,172 +195,220 @@ impl DeleteableOrRemoveable for Community {
 }
 
 impl CommunityModerator {
-  pub fn delete_for_community(
-    conn: &PgConnection,
+  pub async fn delete_for_community(
+    pool: &DbPool,
     for_community_id: CommunityId,
   ) -> Result<usize, Error> {
-    use crate::schema::community_moderator::dsl::*;
-    diesel::delete(community_moderator.filter(community_id.eq(for_community_id))).execute(conn)
+    use crate::schema::community_moderator::dsl::{community_id, community_moderator};
+    let conn = &mut get_conn(pool).await?;
+
+    diesel::delete(community_moderator.filter(community_id.eq(for_community_id)))
+      .execute(conn)
+      .await
   }
 
-  pub fn get_person_moderated_communities(
-    conn: &PgConnection,
+  pub async fn get_person_moderated_communities(
+    pool: &DbPool,
     for_person_id: PersonId,
   ) -> Result<Vec<CommunityId>, Error> {
-    use crate::schema::community_moderator::dsl::*;
+    use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id};
+    let conn = &mut get_conn(pool).await?;
     community_moderator
       .filter(person_id.eq(for_person_id))
       .select(community_id)
       .load::<CommunityId>(conn)
+      .await
   }
 }
 
+#[async_trait]
 impl Bannable for CommunityPersonBan {
   type Form = CommunityPersonBanForm;
-  fn ban(
-    conn: &PgConnection,
+  async fn ban(
+    pool: &DbPool,
     community_person_ban_form: &CommunityPersonBanForm,
   ) -> Result<Self, Error> {
-    use crate::schema::community_person_ban::dsl::*;
+    use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id};
+    let conn = &mut get_conn(pool).await?;
     insert_into(community_person_ban)
       .values(community_person_ban_form)
       .on_conflict((community_id, person_id))
       .do_update()
       .set(community_person_ban_form)
       .get_result::<Self>(conn)
+      .await
   }
 
-  fn unban(
-    conn: &PgConnection,
+  async fn unban(
+    pool: &DbPool,
     community_person_ban_form: &CommunityPersonBanForm,
   ) -> Result<usize, Error> {
-    use crate::schema::community_person_ban::dsl::*;
+    use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id};
+    let conn = &mut get_conn(pool).await?;
     diesel::delete(
       community_person_ban
         .filter(community_id.eq(community_person_ban_form.community_id))
         .filter(person_id.eq(community_person_ban_form.person_id)),
     )
     .execute(conn)
+    .await
+  }
+}
+
+impl CommunityFollower {
+  pub fn to_subscribed_type(follower: &Option<Self>) -> SubscribedType {
+    match follower {
+      Some(f) => {
+        if f.pending {
+          SubscribedType::Pending
+        } else {
+          SubscribedType::Subscribed
+        }
+      }
+      // If the row doesn't exist, the person isn't a follower.
+      None => SubscribedType::NotSubscribed,
+    }
   }
 }
 
+#[async_trait]
 impl Followable for CommunityFollower {
   type Form = CommunityFollowerForm;
-  fn follow(
-    conn: &PgConnection,
-    community_follower_form: &CommunityFollowerForm,
-  ) -> Result<Self, Error> {
-    use crate::schema::community_follower::dsl::*;
+  async fn follow(pool: &DbPool, form: &CommunityFollowerForm) -> Result<Self, Error> {
+    use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
+    let conn = &mut get_conn(pool).await?;
     insert_into(community_follower)
-      .values(community_follower_form)
+      .values(form)
       .on_conflict((community_id, person_id))
       .do_update()
-      .set(community_follower_form)
+      .set(form)
       .get_result::<Self>(conn)
+      .await
   }
-  fn follow_accepted(
-    conn: &PgConnection,
+  async fn follow_accepted(
+    pool: &DbPool,
     community_id_: CommunityId,
     person_id_: PersonId,
-  ) -> Result<Self, Error>
-  where
-    Self: Sized,
-  {
-    use crate::schema::community_follower::dsl::*;
+  ) -> Result<Self, Error> {
+    use crate::schema::community_follower::dsl::{
+      community_follower,
+      community_id,
+      pending,
+      person_id,
+    };
+    let conn = &mut get_conn(pool).await?;
     diesel::update(
       community_follower
         .filter(community_id.eq(community_id_))
         .filter(person_id.eq(person_id_)),
     )
-    .set(pending.eq(true))
+    .set(pending.eq(false))
     .get_result::<Self>(conn)
+    .await
   }
-  fn unfollow(
-    conn: &PgConnection,
-    community_follower_form: &CommunityFollowerForm,
-  ) -> Result<usize, Error> {
-    use crate::schema::community_follower::dsl::*;
+  async fn unfollow(pool: &DbPool, form: &CommunityFollowerForm) -> Result<usize, Error> {
+    use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
+    let conn = &mut get_conn(pool).await?;
     diesel::delete(
       community_follower
-        .filter(community_id.eq(&community_follower_form.community_id))
-        .filter(person_id.eq(&community_follower_form.person_id)),
+        .filter(community_id.eq(&form.community_id))
+        .filter(person_id.eq(&form.person_id)),
     )
     .execute(conn)
-  }
-  // TODO: this function name only makes sense if you call it with a remote community. for a local
-  //       community, it will also return true if only remote followers exist
-  fn has_local_followers(conn: &PgConnection, community_id_: CommunityId) -> Result<bool, Error> {
-    use crate::schema::community_follower::dsl::*;
-    diesel::select(exists(
-      community_follower.filter(community_id.eq(community_id_)),
-    ))
-    .get_result(conn)
+    .await
   }
 }
 
+#[async_trait]
 impl ApubActor for Community {
-  fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result<Option<Self>, Error> {
-    use crate::schema::community::dsl::*;
-    let object_id: DbUrl = object_id.into();
+  async fn read_from_apub_id(pool: &DbPool, object_id: &DbUrl) -> Result<Option<Self>, Error> {
+    let conn = &mut get_conn(pool).await?;
     Ok(
       community
         .filter(actor_id.eq(object_id))
         .first::<Community>(conn)
+        .await
         .ok()
         .map(Into::into),
     )
   }
 
-  fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Community, Error> {
-    use crate::schema::community::dsl::*;
-    community
+  async fn read_from_name(
+    pool: &DbPool,
+    community_name: &str,
+    include_deleted: bool,
+  ) -> Result<Community, Error> {
+    let conn = &mut get_conn(pool).await?;
+    let mut q = community
+      .into_boxed()
       .filter(local.eq(true))
-      .filter(lower(name).eq(lower(community_name)))
-      .first::<Self>(conn)
+      .filter(lower(name).eq(lower(community_name)));
+    if !include_deleted {
+      q = q.filter(deleted.eq(false)).filter(removed.eq(false));
+    }
+    q.first::<Self>(conn).await
   }
 
-  fn read_from_name_and_domain(
-    conn: &PgConnection,
+  async fn read_from_name_and_domain(
+    pool: &DbPool,
     community_name: &str,
     protocol_domain: &str,
   ) -> Result<Community, Error> {
-    use crate::schema::community::dsl::*;
+    let conn = &mut get_conn(pool).await?;
     community
       .filter(lower(name).eq(lower(community_name)))
       .filter(actor_id.like(format!("{}%", protocol_domain)))
       .first::<Self>(conn)
+      .await
   }
 }
 
 #[cfg(test)]
 mod tests {
   use crate::{
-    establish_unpooled_connection,
-    source::{community::*, person::*},
+    source::{
+      community::{
+        Community,
+        CommunityFollower,
+        CommunityFollowerForm,
+        CommunityInsertForm,
+        CommunityModerator,
+        CommunityModeratorForm,
+        CommunityPersonBan,
+        CommunityPersonBanForm,
+        CommunityUpdateForm,
+      },
+      instance::Instance,
+      person::{Person, PersonInsertForm},
+    },
     traits::{Bannable, Crud, Followable, Joinable},
+    utils::build_db_pool_for_tests,
   };
   use serial_test::serial;
 
-  #[test]
+  #[tokio::test]
   #[serial]
-  fn test_crud() {
-    let conn = establish_unpooled_connection();
+  async fn test_crud() {
+    let pool = &build_db_pool_for_tests().await;
 
-    let new_person = PersonForm {
-      name: "bobbee".into(),
-      ..PersonForm::default()
-    };
+    let inserted_instance = Instance::create(pool, "my_domain.tld").await.unwrap();
 
-    let inserted_person = Person::create(&conn, &new_person).unwrap();
+    let new_person = PersonInsertForm::builder()
+      .name("bobbee".into())
+      .public_key("pubkey".to_string())
+      .instance_id(inserted_instance.id)
+      .build();
 
-    let new_community = CommunityForm {
-      name: "TIL".into(),
-      title: "nada".to_owned(),
-      public_key: "nada".to_owned(),
-      ..CommunityForm::default()
-    };
+    let inserted_person = Person::create(pool, &new_person).await.unwrap();
+
+    let new_community = CommunityInsertForm::builder()
+      .name("TIL".into())
+      .title("nada".to_owned())
+      .public_key("pubkey".to_string())
+      .instance_id(inserted_instance.id)
+      .build();
 
-    let inserted_community = Community::create(&conn, &new_community).unwrap();
+    let inserted_community = Community::create(pool, &new_community).await.unwrap();
 
     let expected_community = Community {
       id: inserted_community.id,
@@ -362,16 +420,19 @@ mod tests {
       deleted: false,
       published: inserted_community.published,
       updated: None,
-      actor_id: inserted_community.actor_id.to_owned(),
+      actor_id: inserted_community.actor_id.clone(),
       local: true,
       private_key: None,
-      public_key: "nada".to_owned(),
+      public_key: "pubkey".to_owned(),
       last_refreshed_at: inserted_community.published,
       icon: None,
       banner: None,
-      followers_url: inserted_community.followers_url.to_owned(),
-      inbox_url: inserted_community.inbox_url.to_owned(),
+      followers_url: inserted_community.followers_url.clone(),
+      inbox_url: inserted_community.inbox_url.clone(),
       shared_inbox_url: None,
+      hidden: false,
+      posting_restricted_to_mods: false,
+      instance_id: inserted_instance.id,
     };
 
     let community_follower_form = CommunityFollowerForm {
@@ -380,14 +441,15 @@ mod tests {
       pending: false,
     };
 
-    let inserted_community_follower =
-      CommunityFollower::follow(&conn, &community_follower_form).unwrap();
+    let inserted_community_follower = CommunityFollower::follow(pool, &community_follower_form)
+      .await
+      .unwrap();
 
     let expected_community_follower = CommunityFollower {
       id: inserted_community_follower.id,
       community_id: inserted_community.id,
       person_id: inserted_person.id,
-      pending: Some(false),
+      pending: false,
       published: inserted_community_follower.published,
     };
 
@@ -396,8 +458,9 @@ mod tests {
       person_id: inserted_person.id,
     };
 
-    let inserted_community_moderator =
-      CommunityModerator::join(&conn, &community_moderator_form).unwrap();
+    let inserted_community_moderator = CommunityModerator::join(pool, &community_moderator_form)
+      .await
+      .unwrap();
 
     let expected_community_moderator = CommunityModerator {
       id: inserted_community_moderator.id,
@@ -412,8 +475,9 @@ mod tests {
       expires: None,
     };
 
-    let inserted_community_person_ban =
-      CommunityPersonBan::ban(&conn, &community_person_ban_form).unwrap();
+    let inserted_community_person_ban = CommunityPersonBan::ban(pool, &community_person_ban_form)
+      .await
+      .unwrap();
 
     let expected_community_person_ban = CommunityPersonBan {
       id: inserted_community_person_ban.id,
@@ -423,14 +487,29 @@ mod tests {
       expires: None,
     };
 
-    let read_community = Community::read(&conn, inserted_community.id).unwrap();
-    let updated_community =
-      Community::update(&conn, inserted_community.id, &new_community).unwrap();
-    let ignored_community = CommunityFollower::unfollow(&conn, &community_follower_form).unwrap();
-    let left_community = CommunityModerator::leave(&conn, &community_moderator_form).unwrap();
-    let unban = CommunityPersonBan::unban(&conn, &community_person_ban_form).unwrap();
-    let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
-    Person::delete(&conn, inserted_person.id).unwrap();
+    let read_community = Community::read(pool, inserted_community.id).await.unwrap();
+
+    let update_community_form = CommunityUpdateForm::builder()
+      .title(Some("nada".to_owned()))
+      .build();
+    let updated_community = Community::update(pool, inserted_community.id, &update_community_form)
+      .await
+      .unwrap();
+
+    let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form)
+      .await
+      .unwrap();
+    let left_community = CommunityModerator::leave(pool, &community_moderator_form)
+      .await
+      .unwrap();
+    let unban = CommunityPersonBan::unban(pool, &community_person_ban_form)
+      .await
+      .unwrap();
+    let num_deleted = Community::delete(pool, inserted_community.id)
+      .await
+      .unwrap();
+    Person::delete(pool, inserted_person.id).await.unwrap();
+    Instance::delete(pool, inserted_instance.id).await.unwrap();
 
     assert_eq!(expected_community, read_community);
     assert_eq!(expected_community, inserted_community);