]> Untitled Git - lemmy.git/commitdiff
Implement instance actor (#1798)
authorNutomic <me@nutomic.com>
Mon, 7 Feb 2022 19:23:12 +0000 (19:23 +0000)
committerGitHub <noreply@github.com>
Mon, 7 Feb 2022 19:23:12 +0000 (19:23 +0000)
* Implement instance actor

* wip: make site bans federate

* finish implementation and unit tests for federated bans

* start adding api tests

* fix api test

* remve site from GetCommunityResponse

* only federate site bans originating from user's home instance

* dont expose site.private_key in api

55 files changed:
api_tests/src/post.spec.ts
api_tests/src/shared.ts
crates/api/src/community.rs
crates/api/src/local_user.rs
crates/api/src/site.rs
crates/api_common/src/lib.rs
crates/api_crud/src/community/create.rs
crates/api_crud/src/site/create.rs
crates/api_crud/src/site/update.rs
crates/api_crud/src/user/create.rs
crates/apub/assets/lemmy/activities/block/block_user.json [moved from crates/apub/assets/lemmy/activities/community/block_user.json with 89% similarity]
crates/apub/assets/lemmy/activities/block/undo_block_user.json [moved from crates/apub/assets/lemmy/activities/community/undo_block_user.json with 86% similarity]
crates/apub/assets/lemmy/objects/instance.json [new file with mode: 0644]
crates/apub/src/activities/block/block_user.rs [new file with mode: 0644]
crates/apub/src/activities/block/mod.rs [new file with mode: 0644]
crates/apub/src/activities/block/undo_block_user.rs [new file with mode: 0644]
crates/apub/src/activities/community/block_user.rs [deleted file]
crates/apub/src/activities/community/mod.rs
crates/apub/src/activities/community/undo_block_user.rs [deleted file]
crates/apub/src/activities/mod.rs
crates/apub/src/activity_lists.rs
crates/apub/src/collections/community_moderators.rs
crates/apub/src/fetcher/deletable_apub_object.rs [new file with mode: 0644]
crates/apub/src/http/mod.rs
crates/apub/src/http/person.rs
crates/apub/src/http/routes.rs
crates/apub/src/http/site.rs [new file with mode: 0644]
crates/apub/src/lib.rs
crates/apub/src/objects/comment.rs
crates/apub/src/objects/community.rs
crates/apub/src/objects/instance.rs [new file with mode: 0644]
crates/apub/src/objects/mod.rs
crates/apub/src/objects/person.rs
crates/apub/src/objects/post.rs
crates/apub/src/objects/private_message.rs
crates/apub/src/protocol/activities/block/block_user.rs [moved from crates/apub/src/protocol/activities/community/block_user.rs with 65% similarity]
crates/apub/src/protocol/activities/block/mod.rs [new file with mode: 0644]
crates/apub/src/protocol/activities/block/undo_block_user.rs [moved from crates/apub/src/protocol/activities/community/undo_block_user.rs with 78% similarity]
crates/apub/src/protocol/activities/community/mod.rs
crates/apub/src/protocol/activities/mod.rs
crates/apub/src/protocol/collections/empty_outbox.rs [moved from crates/apub/src/protocol/collections/person_outbox.rs with 60% similarity]
crates/apub/src/protocol/collections/mod.rs
crates/apub/src/protocol/mod.rs
crates/apub/src/protocol/objects/instance.rs [new file with mode: 0644]
crates/apub/src/protocol/objects/mod.rs
crates/db_schema/src/aggregates/site_aggregates.rs
crates/db_schema/src/impls/community.rs
crates/db_schema/src/impls/site.rs
crates/db_schema/src/newtypes.rs
crates/db_schema/src/schema.rs
crates/db_schema/src/source/site.rs
crates/db_views/src/site_view.rs
migrations/2022-01-28-104106_instance-actor/down.sql [new file with mode: 0644]
migrations/2022-01-28-104106_instance-actor/up.sql [new file with mode: 0644]
src/code_migrations.rs

index b3b52ee0bef9c935dec6554ecd0ac806f338bd79..3e834dbe4074eb82d1ffbeda4a42f14bb63bdfe7 100644 (file)
@@ -27,6 +27,9 @@ import {
   reportPost,
   listPostReports,
   randomString,
+  registerUser,
+  API,
+  getSite
 } from './shared';
 import { PostView, CommunityView } from 'lemmy-js-client';
 
@@ -297,29 +300,46 @@ test('A and G subscribe to B (center) A posts, it gets announced to G', async ()
 });
 
 test('Enforce site ban for federated user', async () => {
-  let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
-  let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
+  // create a test user
+  let alphaUserJwt = await registerUser(alpha);
+  expect(alphaUserJwt).toBeDefined();
+  let alphaUser: API = {
+      client: alpha.client,
+      auth: alphaUserJwt.jwt,
+  };
+  let alphaUserActorId = (await getSite(alphaUser)).my_user.local_user_view.person.actor_id;
+  expect(alphaUserActorId).toBeDefined();
+  let alphaPerson = (await resolvePerson(alphaUser, alphaUserActorId)).person;
   expect(alphaPerson).toBeDefined();
 
-  // ban alpha from beta site
-  let banAlpha = await banPersonFromSite(beta, alphaPerson.person.id, true);
+  // alpha makes post in beta community, it federates to beta instance
+  let postRes1 = await createPost(alphaUser, betaCommunity.community.id);
+  let searchBeta1 = await searchPostLocal(beta, postRes1.post_view.post);
+  expect(searchBeta1.posts[0]).toBeDefined();
+
+  // ban alpha from its instance
+  let banAlpha = await banPersonFromSite(alpha, alphaPerson.person.id, true, true);
   expect(banAlpha.banned).toBe(true);
 
-  // Alpha makes post on beta
-  let postRes = await createPost(alpha, betaCommunity.community.id);
-  expect(postRes.post_view.post).toBeDefined();
-  expect(postRes.post_view.community.local).toBe(false);
-  expect(postRes.post_view.creator.local).toBe(true);
-  expect(postRes.post_view.counts.score).toBe(1);
+  // alpha ban should be federated to beta
+  let alphaUserOnBeta1 = await resolvePerson(beta, alphaUserActorId);
+  expect(alphaUserOnBeta1.person.person.banned).toBe(true);
 
-  // Make sure that post doesn't make it to beta
-  let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
-  let betaPost = searchBeta.posts[0];
-  expect(betaPost).toBeUndefined();
+  // existing alpha post should be removed on beta
+  let searchBeta2 = await searchPostLocal(beta, postRes1.post_view.post);
+  expect(searchBeta2.posts[0]).toBeUndefined();
 
   // Unban alpha
-  let unBanAlpha = await banPersonFromSite(beta, alphaPerson.person.id, false);
+  let unBanAlpha = await banPersonFromSite(alpha, alphaPerson.person.id, false, false);
   expect(unBanAlpha.banned).toBe(false);
+
+  // alpha makes new post in beta community, it federates
+  let postRes2 = await createPost(alphaUser, betaCommunity.community.id);
+  let searchBeta3 = await searchPostLocal(beta, postRes2.post_view.post);
+  expect(searchBeta3.posts[0]).toBeDefined();
+
+  let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId)
+  expect(alphaUserOnBeta2.person.person.banned).toBe(false);
 });
 
 test('Enforce community ban for federated user', async () => {
@@ -327,33 +347,41 @@ test('Enforce community ban for federated user', async () => {
   let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
   expect(alphaPerson).toBeDefined();
 
-  // ban alpha from beta site
-  await banPersonFromCommunity(beta, alphaPerson.person.id, 2, false);
-  let banAlpha = await banPersonFromCommunity(beta, alphaPerson.person.id, 2, true);
+  // make a post in beta, it goes through
+  let postRes1 = await createPost(alpha, betaCommunity.community.id);
+  let searchBeta1 = await searchPostLocal(beta, postRes1.post_view.post);
+  expect(searchBeta1.posts[0]).toBeDefined();
+
+  // ban alpha from beta community
+  let banAlpha = await banPersonFromCommunity(beta, alphaPerson.person.id, 2, true, true);
   expect(banAlpha.banned).toBe(true);
 
+  // ensure that the post by alpha got removed
+  let searchAlpha1 = await searchPostLocal(alpha, postRes1.post_view.post);
+  expect(searchAlpha1.posts[0]).toBeUndefined();
+
   // Alpha tries to make post on beta, but it fails because of ban
-  let postRes = await createPost(alpha, betaCommunity.community.id);
-  expect(postRes.post_view).toBeUndefined();
+  let postRes2 = await createPost(alpha, betaCommunity.community.id);
+  expect(postRes2.post_view).toBeUndefined();
 
   // Unban alpha
   let unBanAlpha = await banPersonFromCommunity(
     beta,
     alphaPerson.person.id,
     2,
+    false,
     false
   );
   expect(unBanAlpha.banned).toBe(false);
-  let postRes2 = await createPost(alpha, betaCommunity.community.id);
-  expect(postRes2.post_view.post).toBeDefined();
-  expect(postRes2.post_view.community.local).toBe(false);
-  expect(postRes2.post_view.creator.local).toBe(true);
-  expect(postRes2.post_view.counts.score).toBe(1);
+  let postRes3 = await createPost(alpha, betaCommunity.community.id);
+  expect(postRes3.post_view.post).toBeDefined();
+  expect(postRes3.post_view.community.local).toBe(false);
+  expect(postRes3.post_view.creator.local).toBe(true);
+  expect(postRes3.post_view.counts.score).toBe(1);
 
   // Make sure that post makes it to beta community
-  let searchBeta = await searchPostLocal(beta, postRes2.post_view.post);
-  let betaPost = searchBeta.posts[0];
-  expect(betaPost).toBeDefined();
+  let searchBeta2 = await searchPostLocal(beta, postRes3.post_view.post);
+  expect(searchBeta2.posts[0]).toBeDefined();
 });
 
 test('Report a post', async () => {
index 1eec6dcfb3fa38cbb5e164bca70c34c314db6b4b..8c4e08ff4f717cc60bf507ed18383e2a1696acda 100644 (file)
@@ -66,23 +66,23 @@ export interface API {
 }
 
 export let alpha: API = {
-  client: new LemmyHttp('http://localhost:8541'),
+  client: new LemmyHttp('http://127.0.0.1:8541'),
 };
 
 export let beta: API = {
-  client: new LemmyHttp('http://localhost:8551'),
+  client: new LemmyHttp('http://127.0.0.1:8551'),
 };
 
 export let gamma: API = {
-  client: new LemmyHttp('http://localhost:8561'),
+  client: new LemmyHttp('http://127.0.0.1:8561'),
 };
 
 export let delta: API = {
-  client: new LemmyHttp('http://localhost:8571'),
+  client: new LemmyHttp('http://127.0.0.1:8571'),
 };
 
 export let epsilon: API = {
-  client: new LemmyHttp('http://localhost:8581'),
+  client: new LemmyHttp('http://127.0.0.1:8581'),
 };
 
 const password = 'lemmylemmy'
@@ -289,13 +289,14 @@ export async function resolvePerson(
 export async function banPersonFromSite(
   api: API,
   person_id: number,
-  ban: boolean
+  ban: boolean,
+  remove_data: boolean,
 ): Promise<BanPersonResponse> {
   // Make sure lemmy-beta/c/main is cached on lemmy_alpha
   let form: BanPerson = {
     person_id,
     ban,
-    remove_data: false,
+    remove_data,
     auth: api.auth,
   };
   return api.client.banPerson(form);
@@ -305,13 +306,13 @@ export async function banPersonFromCommunity(
   api: API,
   person_id: number,
   community_id: number,
+  remove_data: boolean,
   ban: boolean
 ): Promise<BanFromCommunityResponse> {
-  // Make sure lemmy-beta/c/main is cached on lemmy_alpha
   let form: BanFromCommunity = {
     person_id,
     community_id,
-    remove_data: false,
+    remove_data,
     ban,
     auth: api.auth,
   };
index a1a44f0941462ee9256de042494db40d1b943023..f4f4332345ca21c3a8bae7ae023267d2475cd52e 100644 (file)
@@ -8,22 +8,19 @@ use lemmy_api_common::{
   community::*,
   get_local_user_view_from_jwt,
   is_mod_or_admin,
+  remove_user_data_in_community,
 };
 use lemmy_apub::{
+  activities::block::SiteOrCommunity,
   objects::{community::ApubCommunity, person::ApubPerson},
   protocol::activities::{
-    community::{
-      add_mod::AddMod,
-      block_user::BlockUserFromCommunity,
-      remove_mod::RemoveMod,
-      undo_block_user::UndoBlockUserFromCommunity,
-    },
+    block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+    community::{add_mod::AddMod, remove_mod::RemoveMod},
     following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity},
   },
 };
 use lemmy_db_schema::{
   source::{
-    comment::Comment,
     community::{
       Community,
       CommunityFollower,
@@ -43,11 +40,9 @@ use lemmy_db_schema::{
       ModTransferCommunityForm,
     },
     person::Person,
-    post::Post,
   },
   traits::{Bannable, Blockable, Crud, Followable, Joinable},
 };
-use lemmy_db_views::comment_view::CommentQueryBuilder;
 use lemmy_db_views_actor::{
   community_moderator_view::CommunityModeratorView,
   community_view::CommunityView,
@@ -213,6 +208,7 @@ impl Perform for BanFromCommunity {
 
     let community_id = data.community_id;
     let banned_person_id = data.person_id;
+    let remove_data = data.remove_data.unwrap_or(false);
     let expires = data.expires.map(naive_from_unix);
 
     // Verify that only mods or admins can ban
@@ -254,10 +250,12 @@ impl Perform for BanFromCommunity {
       .await?
       .ok();
 
-      BlockUserFromCommunity::send(
-        &community,
+      BlockUser::send(
+        &SiteOrCommunity::Community(community),
         &banned_person,
         &local_user_view.person.clone().into(),
+        remove_data,
+        data.reason.clone(),
         expires,
         context,
       )
@@ -268,41 +266,19 @@ impl Perform for BanFromCommunity {
         .await?
         .map_err(LemmyError::from)
         .map_err(|e| e.with_message("community_user_already_banned"))?;
-      UndoBlockUserFromCommunity::send(
-        &community,
+      UndoBlockUser::send(
+        &SiteOrCommunity::Community(community),
         &banned_person,
         &local_user_view.person.clone().into(),
+        data.reason.clone(),
         context,
       )
       .await?;
     }
 
     // Remove/Restore their data if that's desired
-    if data.remove_data.unwrap_or(false) {
-      // Posts
-      blocking(context.pool(), move |conn: &'_ _| {
-        Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
-      })
-      .await??;
-
-      // Comments
-      // TODO Diesel doesn't allow updates with joins, so this has to be a loop
-      let comments = blocking(context.pool(), move |conn| {
-        CommentQueryBuilder::create(conn)
-          .creator_id(banned_person_id)
-          .community_id(community_id)
-          .limit(std::i64::MAX)
-          .list()
-      })
-      .await??;
-
-      for comment_view in &comments {
-        let comment_id = comment_view.comment.id;
-        blocking(context.pool(), move |conn: &'_ _| {
-          Comment::update_removed(conn, comment_id, true)
-        })
-        .await??;
-      }
+    if remove_data {
+      remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?;
     }
 
     // Mod tables
index 974beb6eab44371459f8b590ed1836185d0998d0..b4e2bfb190b87246c2e4750b2f5d5a7d17c8b671 100644 (file)
@@ -10,10 +10,15 @@ use lemmy_api_common::{
   is_admin,
   password_length_check,
   person::*,
+  remove_user_data,
   send_email_verification_success,
   send_password_reset_email,
   send_verification_email,
 };
+use lemmy_apub::{
+  activities::block::SiteOrCommunity,
+  protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+};
 use lemmy_db_schema::{
   diesel_option_overwrite,
   diesel_option_overwrite_to_url,
@@ -21,7 +26,6 @@ use lemmy_db_schema::{
   naive_now,
   source::{
     comment::Comment,
-    community::Community,
     email_verification::EmailVerification,
     local_user::{LocalUser, LocalUserForm},
     moderator::*,
@@ -29,7 +33,6 @@ use lemmy_db_schema::{
     person::*,
     person_block::{PersonBlock, PersonBlockForm},
     person_mention::*,
-    post::Post,
     private_message::PrivateMessage,
     site::*,
   },
@@ -44,7 +47,6 @@ use lemmy_db_views::{
   private_message_view::PrivateMessageView,
 };
 use lemmy_db_views_actor::{
-  community_moderator_view::CommunityModeratorView,
   person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
   person_view::PersonViewSafe,
 };
@@ -91,7 +93,7 @@ impl Perform for Login {
       return Err(LemmyError::from_message("password_incorrect"));
     }
 
-    let site = blocking(context.pool(), Site::read_simple).await??;
+    let site = blocking(context.pool(), Site::read_local_site).await??;
     if site.require_email_verification && !local_user_view.local_user.email_verified {
       return Err(LemmyError::from_message("email_not_verified"));
     }
@@ -200,7 +202,7 @@ impl Perform for SaveUserSettings {
 
     // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
     if let Some(email) = &email {
-      let site_fut = blocking(context.pool(), Site::read_simple);
+      let site_fut = blocking(context.pool(), Site::read_local_site);
       if email.is_none() && site_fut.await??.require_email_verification {
         return Err(LemmyError::from_message("email_required"));
       }
@@ -443,45 +445,15 @@ impl Perform for BanPerson {
     let expires = data.expires.map(naive_from_unix);
 
     let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
-    blocking(context.pool(), ban_person)
+    let person = blocking(context.pool(), ban_person)
       .await?
       .map_err(LemmyError::from)
       .map_err(|e| e.with_message("couldnt_update_user"))?;
 
     // Remove their data if that's desired
-    if data.remove_data.unwrap_or(false) {
-      // Posts
-      blocking(context.pool(), move |conn: &'_ _| {
-        Post::update_removed_for_creator(conn, banned_person_id, None, true)
-      })
-      .await??;
-
-      // Communities
-      // Remove all communities where they're the top mod
-      // for now, remove the communities manually
-      let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
-        CommunityModeratorView::get_community_first_mods(conn)
-      })
-      .await??;
-
-      // Filter to only this banned users top communities
-      let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
-        .into_iter()
-        .filter(|fmc| fmc.moderator.id == banned_person_id)
-        .collect();
-
-      for first_mod_community in banned_user_first_communities {
-        blocking(context.pool(), move |conn: &'_ _| {
-          Community::update_removed(conn, first_mod_community.community.id, true)
-        })
-        .await??;
-      }
-
-      // Comments
-      blocking(context.pool(), move |conn: &'_ _| {
-        Comment::update_removed_for_creator(conn, banned_person_id, true)
-      })
-      .await??;
+    let remove_data = data.remove_data.unwrap_or(false);
+    if remove_data {
+      remove_user_data(person.id, context.pool()).await?;
     }
 
     // Mod tables
@@ -501,6 +473,36 @@ impl Perform for BanPerson {
     })
     .await??;
 
+    let site = SiteOrCommunity::Site(
+      blocking(context.pool(), Site::read_local_site)
+        .await??
+        .into(),
+    );
+    // if the action affects a local user, federate to other instances
+    if person.local {
+      if ban {
+        BlockUser::send(
+          &site,
+          &person.into(),
+          &local_user_view.person.into(),
+          remove_data,
+          data.reason.clone(),
+          expires,
+          context,
+        )
+        .await?;
+      } else {
+        UndoBlockUser::send(
+          &site,
+          &person.into(),
+          &local_user_view.person.into(),
+          data.reason.clone(),
+          context,
+        )
+        .await?;
+      }
+    }
+
     let res = BanPersonResponse {
       person_view,
       banned: data.ban,
index 20ceffe57b2455a274fee4dad55b36fa7076f9b9..92dbdd64df6d7cd7a5fafdb3237d2869a0af8ee4 100644 (file)
@@ -583,7 +583,7 @@ impl Perform for ListRegistrationApplications {
     is_admin(&local_user_view)?;
 
     let unread_only = data.unread_only;
-    let verified_email_only = blocking(context.pool(), Site::read_simple)
+    let verified_email_only = blocking(context.pool(), Site::read_local_site)
       .await??
       .require_email_verification;
 
@@ -689,7 +689,7 @@ impl Perform for GetUnreadRegistrationApplicationCount {
     // Only let admins do this
     is_admin(&local_user_view)?;
 
-    let verified_email_only = blocking(context.pool(), Site::read_simple)
+    let verified_email_only = blocking(context.pool(), Site::read_local_site)
       .await??
       .require_email_verification;
 
index 2d1f1bf5047ce904ec720596bb5cac70cd93e704..bc41b0c957278e155d6151a76d43cbf41fe4b739 100644 (file)
@@ -10,6 +10,7 @@ use itertools::Itertools;
 use lemmy_db_schema::{
   newtypes::{CommunityId, LocalUserId, PersonId, PostId},
   source::{
+    comment::Comment,
     community::Community,
     email_verification::{EmailVerification, EmailVerificationForm},
     password_reset_request::PasswordResetRequest,
@@ -22,8 +23,12 @@ use lemmy_db_schema::{
   traits::{ApubActor, Crud, Readable},
   DbPool,
 };
-use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
+use lemmy_db_views::{
+  comment_view::CommentQueryBuilder,
+  local_user_view::{LocalUserSettingsView, LocalUserView},
+};
 use lemmy_db_views_actor::{
+  community_moderator_view::CommunityModeratorView,
   community_person_ban_view::CommunityPersonBanView,
   community_view::CommunityView,
 };
@@ -35,7 +40,6 @@ use lemmy_utils::{
   LemmyError,
   Sensitive,
 };
-use url::Url;
 
 pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
 where
@@ -267,7 +271,7 @@ pub async fn check_person_block(
 #[tracing::instrument(skip_all)]
 pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
   if score == -1 {
-    let site = blocking(pool, Site::read_simple).await??;
+    let site = blocking(pool, Site::read_local_site).await??;
     if !site.enable_downvotes {
       return Err(LemmyError::from_message("downvotes_disabled"));
     }
@@ -281,7 +285,7 @@ pub async fn check_private_instance(
   pool: &DbPool,
 ) -> Result<(), LemmyError> {
   if local_user_view.is_none() {
-    let site = blocking(pool, Site::read_simple).await?;
+    let site = blocking(pool, Site::read_local_site).await?;
 
     // The site might not be set up yet
     if let Ok(site) = site {
@@ -311,7 +315,7 @@ pub async fn build_federated_instances(
 
     let mut linked = distinct_communities
       .iter()
-      .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
+      .map(|actor_id| Ok(actor_id.host_str().unwrap_or("").to_string()))
       .collect::<Result<Vec<String>, LemmyError>>()?;
 
     if let Some(allowed) = allowed.as_ref() {
@@ -511,7 +515,7 @@ pub async fn check_private_instance_and_federation_enabled(
   pool: &DbPool,
   settings: &Settings,
 ) -> Result<(), LemmyError> {
-  let site_opt = blocking(pool, Site::read_simple).await?;
+  let site_opt = blocking(pool, Site::read_local_site).await?;
 
   if let Ok(site) = site_opt {
     if site.private_instance && settings.federation.enabled {
@@ -555,3 +559,73 @@ where
     Ok(blocking(pool, move |conn| Actor::read_from_name(conn, &identifier)).await??)
   }
 }
+
+pub async fn remove_user_data(banned_person_id: PersonId, pool: &DbPool) -> Result<(), LemmyError> {
+  // Posts
+  blocking(pool, move |conn: &'_ _| {
+    Post::update_removed_for_creator(conn, banned_person_id, None, true)
+  })
+  .await??;
+
+  // Communities
+  // Remove all communities where they're the top mod
+  // for now, remove the communities manually
+  let first_mod_communities = blocking(pool, move |conn: &'_ _| {
+    CommunityModeratorView::get_community_first_mods(conn)
+  })
+  .await??;
+
+  // Filter to only this banned users top communities
+  let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
+    .into_iter()
+    .filter(|fmc| fmc.moderator.id == banned_person_id)
+    .collect();
+
+  for first_mod_community in banned_user_first_communities {
+    blocking(pool, move |conn: &'_ _| {
+      Community::update_removed(conn, first_mod_community.community.id, true)
+    })
+    .await??;
+  }
+
+  // Comments
+  blocking(pool, move |conn: &'_ _| {
+    Comment::update_removed_for_creator(conn, banned_person_id, true)
+  })
+  .await??;
+
+  Ok(())
+}
+
+pub async fn remove_user_data_in_community(
+  community_id: CommunityId,
+  banned_person_id: PersonId,
+  pool: &DbPool,
+) -> Result<(), LemmyError> {
+  // Posts
+  blocking(pool, move |conn| {
+    Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
+  })
+  .await??;
+
+  // Comments
+  // TODO Diesel doesn't allow updates with joins, so this has to be a loop
+  let comments = blocking(pool, move |conn| {
+    CommentQueryBuilder::create(conn)
+      .creator_id(banned_person_id)
+      .community_id(community_id)
+      .limit(std::i64::MAX)
+      .list()
+  })
+  .await??;
+
+  for comment_view in &comments {
+    let comment_id = comment_view.comment.id;
+    blocking(pool, move |conn| {
+      Comment::update_removed(conn, comment_id, true)
+    })
+    .await??;
+  }
+
+  Ok(())
+}
index fb090b8641aa91d25b14b2117b6c011478d6cbf5..d78c8ad7a6e4d8722b94a2852f366a3ec44bf806 100644 (file)
@@ -53,7 +53,7 @@ impl PerformCrud for CreateCommunity {
     let local_user_view =
       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
 
-    let site = blocking(context.pool(), move |conn| Site::read(conn, 0)).await??;
+    let site = blocking(context.pool(), Site::read_local_site).await??;
     if site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
       return Err(LemmyError::from_message(
         "only_admins_can_create_communities",
index 3de2c3ab84418e10ff88ec39f682ad25a5ad5859..d714afea7d859b7db457be8e91962876b305ece7 100644 (file)
@@ -7,19 +7,25 @@ use lemmy_api_common::{
   site::*,
   site_description_length_check,
 };
+use lemmy_apub::generate_site_inbox_url;
 use lemmy_db_schema::{
   diesel_option_overwrite,
   diesel_option_overwrite_to_url,
+  naive_now,
+  newtypes::DbUrl,
   source::site::{Site, SiteForm},
   traits::Crud,
 };
 use lemmy_db_views::site_view::SiteView;
 use lemmy_utils::{
+  apub::generate_actor_keypair,
+  settings::structs::Settings,
   utils::{check_slurs, check_slurs_opt},
   ConnectionId,
   LemmyError,
 };
 use lemmy_websocket::LemmyContext;
+use url::Url;
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for CreateSite {
@@ -33,7 +39,7 @@ impl PerformCrud for CreateSite {
   ) -> Result<SiteResponse, LemmyError> {
     let data: &CreateSite = self;
 
-    let read_site = Site::read_simple;
+    let read_site = Site::read_local_site;
     if blocking(context.pool(), read_site).await?.is_ok() {
       return Err(LemmyError::from_message("site_already_exists"));
     };
@@ -56,6 +62,9 @@ impl PerformCrud for CreateSite {
       site_description_length_check(desc)?;
     }
 
+    let actor_id: DbUrl = Url::parse(&Settings::get().get_protocol_and_hostname())?.into();
+    let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
+    let keypair = generate_actor_keypair()?;
     let site_form = SiteForm {
       name: data.name.to_owned(),
       sidebar,
@@ -66,6 +75,11 @@ impl PerformCrud for CreateSite {
       open_registration: data.open_registration,
       enable_nsfw: data.enable_nsfw,
       community_creation_admin_only: data.community_creation_admin_only,
+      actor_id: Some(actor_id),
+      last_refreshed_at: Some(naive_now()),
+      inbox_url,
+      private_key: Some(Some(keypair.private_key)),
+      public_key: Some(keypair.public_key),
       ..SiteForm::default()
     };
 
index 0a94062a7eb07314c7cd2c541f796ab1b02cb2bd..c6f595230966f4a38a1f783ce219df69d2b19ecc 100644 (file)
@@ -20,6 +20,7 @@ use lemmy_db_schema::{
 use lemmy_db_views::site_view::SiteView;
 use lemmy_utils::{utils::check_slurs_opt, ConnectionId, LemmyError};
 use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperationCrud};
+use std::default::Default;
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for EditSite {
@@ -41,7 +42,7 @@ impl PerformCrud for EditSite {
     // Make sure user is an admin
     is_admin(&local_user_view)?;
 
-    let found_site = blocking(context.pool(), Site::read_simple).await??;
+    let found_site = blocking(context.pool(), Site::read_local_site).await??;
 
     let sidebar = diesel_option_overwrite(&data.sidebar);
     let description = diesel_option_overwrite(&data.description);
@@ -68,6 +69,7 @@ impl PerformCrud for EditSite {
       require_application: data.require_application,
       application_question,
       private_instance: data.private_instance,
+      ..SiteForm::default()
     };
 
     let update_site = blocking(context.pool(), move |conn| {
index 6f8a5fa0e247c51956f3c3bb6887ee0e2c9566c0..06b576e51a46c81d0aec209a8fd56cb58c47bd86 100644 (file)
@@ -58,7 +58,7 @@ impl PerformCrud for Register {
     let (mut email_verification, mut require_application) = (false, false);
 
     // Make sure site has open registration
-    if let Ok(site) = blocking(context.pool(), Site::read_simple).await? {
+    if let Ok(site) = blocking(context.pool(), Site::read_local_site).await? {
       if !site.open_registration {
         return Err(LemmyError::from_message("registration_closed"));
       }
similarity index 89%
rename from crates/apub/assets/lemmy/activities/community/block_user.json
rename to crates/apub/assets/lemmy/activities/block/block_user.json
index 4d43e086e550872a60890cb842c0c7d1df3e473f..a12b68e7e54b00f0771bc07e4d035eb854ea6169 100644 (file)
@@ -9,6 +9,8 @@
   ],
   "target": "http://enterprise.lemmy.ml/c/main",
   "type": "Block",
+  "remove_data": "true",
+  "summary": "spam post",
   "expires": "2021-11-01T12:23:50.151874+00:00",
   "id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2"
 }
similarity index 86%
rename from crates/apub/assets/lemmy/activities/community/undo_block_user.json
rename to crates/apub/assets/lemmy/activities/block/undo_block_user.json
index 5810b4f48847065e20c3c78390feb3adbb8ebc1f..41c9aad7760818e9c9dc3b9621a4531686f4ce01 100644 (file)
@@ -14,6 +14,9 @@
     ],
     "target": "http://enterprise.lemmy.ml/c/main",
     "type": "Block",
+    "remove_data": "true",
+    "summary": "spam post",
+    "expires": "2021-11-01T12:23:50.151874+00:00",
     "id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c"
   },
   "cc": [
diff --git a/crates/apub/assets/lemmy/objects/instance.json b/crates/apub/assets/lemmy/objects/instance.json
new file mode 100644 (file)
index 0000000..73df877
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    {
+      "stickied": "as:stickied",
+      "pt": "https://join-lemmy.org#",
+      "sc": "http://schema.org#",
+      "matrixUserId": {
+        "type": "sc:Text",
+        "id": "as:alsoKnownAs"
+      },
+      "sensitive": "as:sensitive",
+      "comments_enabled": {
+        "type": "sc:Boolean",
+        "id": "pt:commentsEnabled"
+      },
+      "moderators": "as:moderators"
+    },
+    "https://w3id.org/security/v1"
+  ],
+  "type": "Service",
+  "id": "https://enterprise.lemmy.ml/",
+  "name": "Enterprise",
+  "summary": "A test instance",
+  "content": "<p>Enterprise sidebar</p>\\n",
+  "mediaType": "text/html",
+  "source": {
+    "content": "Enterprise sidebar",
+    "mediaType": "text/markdown"
+  },
+  "inbox": "https://enterprise.lemmy.ml/inbox",
+  "outbox": "https://enterprise.lemmy.ml/outbox",
+  "publicKey": {
+    "id": "https://enterprise.lemmy.ml/#main-key",
+    "owner": "https://enterprise.lemmy.ml/",
+    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAupcK0xTw5yQb/fnztAmb\n9LfPbhJJP1+1GwUaOXGYiDJD6uYJhl9CLmgztLl3RyV9ltOYoN8/NLNDfOMmgOjd\nrsNWEjDI9IcVPmiZnhU7hsi6KgQvJzzv8O5/xYjAGhDfrGmtdpL+lyG0B5fQod8J\n/V5VWvTQ0B0qFrLSBBuhOrp8/fTtDskdtElDPtnNfH2jn6FgtLOijidWwf9ekFo4\n0I1JeuEw6LuD/CzKVJTPoztzabUV1DQF/DnFJm+8y7SCJa9jEO56Uf9eVfa1jF6f\ndH6ZvNJMiafstVuLMAw7C/eNJy3ufXgtZ4403oOKA0aRSYf1cc9pHSZ9gDE/mevH\nLwIDAQAB\n-----END PUBLIC KEY-----\n"
+  },
+  "published": "2022-01-19T21:52:11.110741+00:00"
+}
\ No newline at end of file
diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs
new file mode 100644 (file)
index 0000000..01eb009
--- /dev/null
@@ -0,0 +1,241 @@
+use crate::{
+  activities::{
+    block::{generate_cc, generate_instance_inboxes, SiteOrCommunity},
+    community::{announce::GetCommunity, send_activity_in_community},
+    generate_activity_id,
+    send_lemmy_activity,
+    verify_activity,
+    verify_is_public,
+    verify_mod_action,
+    verify_person_in_community,
+  },
+  activity_lists::AnnouncableActivities,
+  objects::{community::ApubCommunity, person::ApubPerson},
+  protocol::activities::block::block_user::BlockUser,
+};
+use activitystreams_kinds::{activity::BlockType, public};
+use anyhow::anyhow;
+use chrono::NaiveDateTime;
+use lemmy_api_common::{blocking, remove_user_data, remove_user_data_in_community};
+use lemmy_apub_lib::{
+  data::Data,
+  object_id::ObjectId,
+  traits::{ActivityHandler, ActorType},
+  verify::verify_domains_match,
+};
+use lemmy_db_schema::{
+  source::{
+    community::{
+      CommunityFollower,
+      CommunityFollowerForm,
+      CommunityPersonBan,
+      CommunityPersonBanForm,
+    },
+    moderator::{ModBan, ModBanForm},
+    person::Person,
+  },
+  traits::{Bannable, Crud, Followable},
+};
+use lemmy_utils::{settings::structs::Settings, utils::convert_datetime, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+impl BlockUser {
+  pub(in crate::activities::block) async fn new(
+    target: &SiteOrCommunity,
+    user: &ApubPerson,
+    mod_: &ApubPerson,
+    remove_data: Option<bool>,
+    reason: Option<String>,
+    expires: Option<NaiveDateTime>,
+    context: &LemmyContext,
+  ) -> Result<BlockUser, LemmyError> {
+    Ok(BlockUser {
+      actor: ObjectId::new(mod_.actor_id()),
+      to: vec![public()],
+      object: ObjectId::new(user.actor_id()),
+      cc: generate_cc(target, context.pool()).await?,
+      target: target.id(),
+      kind: BlockType::Block,
+      remove_data,
+      summary: reason,
+      id: generate_activity_id(
+        BlockType::Block,
+        &context.settings().get_protocol_and_hostname(),
+      )?,
+      expires: expires.map(convert_datetime),
+      unparsed: Default::default(),
+    })
+  }
+
+  #[tracing::instrument(skip_all)]
+  pub async fn send(
+    target: &SiteOrCommunity,
+    user: &ApubPerson,
+    mod_: &ApubPerson,
+    remove_data: bool,
+    reason: Option<String>,
+    expires: Option<NaiveDateTime>,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    let block = BlockUser::new(
+      target,
+      user,
+      mod_,
+      Some(remove_data),
+      reason,
+      expires,
+      context,
+    )
+    .await?;
+    let block_id = block.id.clone();
+
+    match target {
+      SiteOrCommunity::Site(_) => {
+        let inboxes = generate_instance_inboxes(user, context.pool()).await?;
+        send_lemmy_activity(context, &block, &block_id, mod_, inboxes, false).await
+      }
+      SiteOrCommunity::Community(c) => {
+        let activity = AnnouncableActivities::BlockUser(block);
+        let inboxes = vec![user.shared_inbox_or_inbox_url()];
+        send_activity_in_community(activity, &block_id, mod_, c, inboxes, context).await
+      }
+    }
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for BlockUser {
+  type DataType = LemmyContext;
+
+  #[tracing::instrument(skip_all)]
+  async fn verify(
+    &self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_is_public(&self.to, &self.cc)?;
+    verify_activity(&self.id, self.actor.inner(), &context.settings())?;
+    match self
+      .target
+      .dereference(context, context.client(), request_counter)
+      .await?
+    {
+      SiteOrCommunity::Site(site) => {
+        let domain = self.object.inner().domain().expect("url needs domain");
+        if Settings::get().hostname == domain {
+          return Err(
+            anyhow!("Site bans from remote instance can't affect user's home instance").into(),
+          );
+        }
+        // site ban can only target a user who is on the same instance as the actor (admin)
+        verify_domains_match(&site.actor_id(), self.actor.inner())?;
+        verify_domains_match(&site.actor_id(), self.object.inner())?;
+      }
+      SiteOrCommunity::Community(community) => {
+        verify_person_in_community(&self.actor, &community, context, request_counter).await?;
+        verify_mod_action(&self.actor, &community, context, request_counter).await?;
+      }
+    }
+    Ok(())
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn receive(
+    self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let expires = self.expires.map(|u| u.naive_local());
+    let mod_person = self
+      .actor
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    let blocked_person = self
+      .object
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    let target = self
+      .target
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    match target {
+      SiteOrCommunity::Site(_site) => {
+        let blocked_person = blocking(context.pool(), move |conn| {
+          Person::ban_person(conn, blocked_person.id, true, expires)
+        })
+        .await??;
+        if self.remove_data.unwrap_or(false) {
+          remove_user_data(blocked_person.id, context.pool()).await?;
+        }
+
+        // write mod log
+        let form = ModBanForm {
+          mod_person_id: mod_person.id,
+          other_person_id: blocked_person.id,
+          reason: self.summary,
+          banned: Some(true),
+          expires,
+        };
+        blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
+      }
+      SiteOrCommunity::Community(community) => {
+        let community_user_ban_form = CommunityPersonBanForm {
+          community_id: community.id,
+          person_id: blocked_person.id,
+          expires: Some(expires),
+        };
+        blocking(context.pool(), move |conn| {
+          CommunityPersonBan::ban(conn, &community_user_ban_form)
+        })
+        .await??;
+
+        // Also unsubscribe them from the community, if they are subscribed
+        let community_follower_form = CommunityFollowerForm {
+          community_id: community.id,
+          person_id: blocked_person.id,
+          pending: false,
+        };
+        blocking(context.pool(), move |conn: &'_ _| {
+          CommunityFollower::unfollow(conn, &community_follower_form)
+        })
+        .await?
+        .ok();
+
+        if self.remove_data.unwrap_or(false) {
+          remove_user_data_in_community(community.id, blocked_person.id, context.pool()).await?;
+        }
+
+        // write to mod log
+        let form = ModBanForm {
+          mod_person_id: mod_person.id,
+          other_person_id: blocked_person.id,
+          reason: self.summary,
+          banned: Some(true),
+          expires,
+        };
+        blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl GetCommunity for BlockUser {
+  #[tracing::instrument(skip_all)]
+  async fn get_community(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<ApubCommunity, LemmyError> {
+    let target = self
+      .target
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    match target {
+      SiteOrCommunity::Community(c) => Ok(c),
+      SiteOrCommunity::Site(_) => Err(anyhow!("Calling get_community() on site activity").into()),
+    }
+  }
+}
diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs
new file mode 100644 (file)
index 0000000..7460ace
--- /dev/null
@@ -0,0 +1,144 @@
+use crate::{
+  objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson},
+  protocol::objects::{group::Group, instance::Instance},
+};
+use chrono::NaiveDateTime;
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  object_id::ObjectId,
+  traits::{ActorType, ApubObject},
+};
+use lemmy_db_schema::{source::site::Site, DbPool};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::Deserialize;
+use url::Url;
+
+pub mod block_user;
+pub mod undo_block_user;
+
+#[derive(Clone, Debug)]
+pub enum SiteOrCommunity {
+  Site(ApubSite),
+  Community(ApubCommunity),
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+pub enum InstanceOrGroup {
+  Instance(Instance),
+  Group(Group),
+}
+
+#[async_trait::async_trait(?Send)]
+impl ApubObject for SiteOrCommunity {
+  type DataType = LemmyContext;
+  type ApubType = InstanceOrGroup;
+  type TombstoneType = ();
+
+  #[tracing::instrument(skip_all)]
+  fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
+    Some(match self {
+      SiteOrCommunity::Site(i) => i.last_refreshed_at,
+      SiteOrCommunity::Community(c) => c.last_refreshed_at,
+    })
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn read_from_apub_id(
+    object_id: Url,
+    data: &Self::DataType,
+  ) -> Result<Option<Self>, LemmyError>
+  where
+    Self: Sized,
+  {
+    let site = ApubSite::read_from_apub_id(object_id.clone(), data).await?;
+    Ok(match site {
+      Some(o) => Some(SiteOrCommunity::Site(o)),
+      None => ApubCommunity::read_from_apub_id(object_id, data)
+        .await?
+        .map(SiteOrCommunity::Community),
+    })
+  }
+
+  async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
+    unimplemented!()
+  }
+
+  async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
+    unimplemented!()
+  }
+
+  fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
+    unimplemented!()
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn verify(
+    apub: &Self::ApubType,
+    expected_domain: &Url,
+    data: &Self::DataType,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    match apub {
+      InstanceOrGroup::Instance(i) => {
+        ApubSite::verify(i, expected_domain, data, request_counter).await
+      }
+      InstanceOrGroup::Group(g) => {
+        ApubCommunity::verify(g, expected_domain, data, request_counter).await
+      }
+    }
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn from_apub(
+    apub: Self::ApubType,
+    data: &Self::DataType,
+    request_counter: &mut i32,
+  ) -> Result<Self, LemmyError>
+  where
+    Self: Sized,
+  {
+    Ok(match apub {
+      InstanceOrGroup::Instance(p) => {
+        SiteOrCommunity::Site(ApubSite::from_apub(p, data, request_counter).await?)
+      }
+      InstanceOrGroup::Group(n) => {
+        SiteOrCommunity::Community(ApubCommunity::from_apub(n, data, request_counter).await?)
+      }
+    })
+  }
+}
+
+impl SiteOrCommunity {
+  fn id(&self) -> ObjectId<SiteOrCommunity> {
+    match self {
+      SiteOrCommunity::Site(s) => ObjectId::new(s.actor_id.clone()),
+      SiteOrCommunity::Community(c) => ObjectId::new(c.actor_id.clone()),
+    }
+  }
+}
+
+async fn generate_cc(target: &SiteOrCommunity, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
+  Ok(match target {
+    SiteOrCommunity::Site(_) => blocking(pool, Site::read_remote_sites)
+      .await??
+      .into_iter()
+      .map(|s| s.actor_id.into())
+      .collect(),
+    SiteOrCommunity::Community(c) => vec![c.actor_id()],
+  })
+}
+
+async fn generate_instance_inboxes(
+  blocked_user: &ApubPerson,
+  pool: &DbPool,
+) -> Result<Vec<Url>, LemmyError> {
+  let mut inboxes: Vec<Url> = blocking(pool, Site::read_remote_sites)
+    .await??
+    .into_iter()
+    .map(|s| s.inbox_url.into())
+    .collect();
+  inboxes.push(blocked_user.shared_inbox_or_inbox_url());
+  Ok(inboxes)
+}
diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs
new file mode 100644 (file)
index 0000000..014b01f
--- /dev/null
@@ -0,0 +1,164 @@
+use crate::{
+  activities::{
+    block::{generate_cc, generate_instance_inboxes, SiteOrCommunity},
+    community::{announce::GetCommunity, send_activity_in_community},
+    generate_activity_id,
+    send_lemmy_activity,
+    verify_activity,
+    verify_is_public,
+  },
+  activity_lists::AnnouncableActivities,
+  objects::{community::ApubCommunity, person::ApubPerson},
+  protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+};
+use activitystreams_kinds::{activity::UndoType, public};
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  data::Data,
+  object_id::ObjectId,
+  traits::{ActivityHandler, ActorType},
+  verify::verify_domains_match,
+};
+use lemmy_db_schema::{
+  source::{
+    community::{CommunityPersonBan, CommunityPersonBanForm},
+    moderator::{ModBan, ModBanForm},
+    person::Person,
+  },
+  traits::{Bannable, Crud},
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+
+impl UndoBlockUser {
+  #[tracing::instrument(skip_all)]
+  pub async fn send(
+    target: &SiteOrCommunity,
+    user: &ApubPerson,
+    mod_: &ApubPerson,
+    reason: Option<String>,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?;
+
+    let id = generate_activity_id(
+      UndoType::Undo,
+      &context.settings().get_protocol_and_hostname(),
+    )?;
+    let undo = UndoBlockUser {
+      actor: ObjectId::new(mod_.actor_id()),
+      to: vec![public()],
+      object: block,
+      cc: generate_cc(target, context.pool()).await?,
+      kind: UndoType::Undo,
+      id: id.clone(),
+      unparsed: Default::default(),
+    };
+
+    let inboxes = vec![user.shared_inbox_or_inbox_url()];
+    match target {
+      SiteOrCommunity::Site(_) => {
+        let inboxes = generate_instance_inboxes(user, context.pool()).await?;
+        send_lemmy_activity(context, &undo, &id, mod_, inboxes, false).await
+      }
+      SiteOrCommunity::Community(c) => {
+        let activity = AnnouncableActivities::UndoBlockUser(undo);
+        send_activity_in_community(activity, &id, mod_, c, inboxes, context).await
+      }
+    }
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoBlockUser {
+  type DataType = LemmyContext;
+
+  #[tracing::instrument(skip_all)]
+  async fn verify(
+    &self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_is_public(&self.to, &self.cc)?;
+    verify_activity(&self.id, self.actor.inner(), &context.settings())?;
+    verify_domains_match(self.actor.inner(), self.object.actor.inner())?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn receive(
+    self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let expires = self.object.expires.map(|u| u.naive_local());
+    let mod_person = self
+      .actor
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    let blocked_person = self
+      .object
+      .object
+      .dereference(context, context.client(), request_counter)
+      .await?;
+    match self
+      .object
+      .target
+      .dereference(context, context.client(), request_counter)
+      .await?
+    {
+      SiteOrCommunity::Site(_site) => {
+        let blocked_person = blocking(context.pool(), move |conn| {
+          Person::ban_person(conn, blocked_person.id, false, expires)
+        })
+        .await??;
+
+        // write mod log
+        let form = ModBanForm {
+          mod_person_id: mod_person.id,
+          other_person_id: blocked_person.id,
+          reason: self.object.summary,
+          banned: Some(false),
+          expires,
+        };
+        blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
+      }
+      SiteOrCommunity::Community(community) => {
+        let community_user_ban_form = CommunityPersonBanForm {
+          community_id: community.id,
+          person_id: blocked_person.id,
+          expires: None,
+        };
+        blocking(context.pool(), move |conn: &'_ _| {
+          CommunityPersonBan::unban(conn, &community_user_ban_form)
+        })
+        .await??;
+
+        // write to mod log
+        let form = ModBanForm {
+          mod_person_id: mod_person.id,
+          other_person_id: blocked_person.id,
+          reason: self.object.summary,
+          banned: Some(false),
+          expires,
+        };
+        blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl GetCommunity for UndoBlockUser {
+  #[tracing::instrument(skip_all)]
+  async fn get_community(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<ApubCommunity, LemmyError> {
+    self.object.get_community(context, request_counter).await
+  }
+}
diff --git a/crates/apub/src/activities/community/block_user.rs b/crates/apub/src/activities/community/block_user.rs
deleted file mode 100644 (file)
index f5a6f02..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-use crate::{
-  activities::{
-    community::{announce::GetCommunity, send_activity_in_community},
-    generate_activity_id,
-    verify_activity,
-    verify_is_public,
-    verify_mod_action,
-    verify_person_in_community,
-  },
-  activity_lists::AnnouncableActivities,
-  objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::activities::community::block_user::BlockUserFromCommunity,
-};
-use activitystreams_kinds::{activity::BlockType, public};
-use chrono::NaiveDateTime;
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::{
-  data::Data,
-  object_id::ObjectId,
-  traits::{ActivityHandler, ActorType},
-};
-use lemmy_db_schema::{
-  source::community::{
-    CommunityFollower,
-    CommunityFollowerForm,
-    CommunityPersonBan,
-    CommunityPersonBanForm,
-  },
-  traits::{Bannable, Followable},
-};
-use lemmy_utils::{utils::convert_datetime, LemmyError};
-use lemmy_websocket::LemmyContext;
-
-impl BlockUserFromCommunity {
-  pub(in crate::activities::community) fn new(
-    community: &ApubCommunity,
-    target: &ApubPerson,
-    actor: &ApubPerson,
-    expires: Option<NaiveDateTime>,
-    context: &LemmyContext,
-  ) -> Result<BlockUserFromCommunity, LemmyError> {
-    Ok(BlockUserFromCommunity {
-      actor: ObjectId::new(actor.actor_id()),
-      to: vec![public()],
-      object: ObjectId::new(target.actor_id()),
-      cc: vec![community.actor_id()],
-      target: ObjectId::new(community.actor_id()),
-      kind: BlockType::Block,
-      id: generate_activity_id(
-        BlockType::Block,
-        &context.settings().get_protocol_and_hostname(),
-      )?,
-      expires: expires.map(convert_datetime),
-      unparsed: Default::default(),
-    })
-  }
-
-  #[tracing::instrument(skip_all)]
-  pub async fn send(
-    community: &ApubCommunity,
-    target: &ApubPerson,
-    actor: &ApubPerson,
-    expires: Option<NaiveDateTime>,
-    context: &LemmyContext,
-  ) -> Result<(), LemmyError> {
-    let block = BlockUserFromCommunity::new(community, target, actor, expires, context)?;
-    let block_id = block.id.clone();
-
-    let activity = AnnouncableActivities::BlockUserFromCommunity(block);
-    let inboxes = vec![target.shared_inbox_or_inbox_url()];
-    send_activity_in_community(activity, &block_id, actor, community, inboxes, context).await
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ActivityHandler for BlockUserFromCommunity {
-  type DataType = LemmyContext;
-
-  #[tracing::instrument(skip_all)]
-  async fn verify(
-    &self,
-    context: &Data<LemmyContext>,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    verify_is_public(&self.to, &self.cc)?;
-    verify_activity(&self.id, self.actor.inner(), &context.settings())?;
-    let community = self.get_community(context, request_counter).await?;
-    verify_person_in_community(&self.actor, &community, context, request_counter).await?;
-    verify_mod_action(&self.actor, &community, context, request_counter).await?;
-    Ok(())
-  }
-
-  #[tracing::instrument(skip_all)]
-  async fn receive(
-    self,
-    context: &Data<LemmyContext>,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    let community = self.get_community(context, request_counter).await?;
-    let blocked_user = self
-      .object
-      .dereference(context, context.client(), request_counter)
-      .await?;
-
-    let community_user_ban_form = CommunityPersonBanForm {
-      community_id: community.id,
-      person_id: blocked_user.id,
-      expires: Some(self.expires.map(|u| u.naive_local())),
-    };
-
-    blocking(context.pool(), move |conn: &'_ _| {
-      CommunityPersonBan::ban(conn, &community_user_ban_form)
-    })
-    .await??;
-
-    // Also unsubscribe them from the community, if they are subscribed
-    let community_follower_form = CommunityFollowerForm {
-      community_id: community.id,
-      person_id: blocked_user.id,
-      pending: false,
-    };
-    blocking(context.pool(), move |conn: &'_ _| {
-      CommunityFollower::unfollow(conn, &community_follower_form)
-    })
-    .await?
-    .ok();
-
-    Ok(())
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl GetCommunity for BlockUserFromCommunity {
-  #[tracing::instrument(skip_all)]
-  async fn get_community(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<ApubCommunity, LemmyError> {
-    self
-      .target
-      .dereference(context, context.client(), request_counter)
-      .await
-  }
-}
index 670b9a7b027160cf596970f9824561480e8fc548..d8d8097ebc8c0280422fe6027ecd7bcb50aa743c 100644 (file)
@@ -11,10 +11,8 @@ use url::Url;
 
 pub mod add_mod;
 pub mod announce;
-pub mod block_user;
 pub mod remove_mod;
 pub mod report;
-pub mod undo_block_user;
 pub mod update;
 
 #[tracing::instrument(skip_all)]
diff --git a/crates/apub/src/activities/community/undo_block_user.rs b/crates/apub/src/activities/community/undo_block_user.rs
deleted file mode 100644 (file)
index a62315d..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-use crate::{
-  activities::{
-    community::{announce::GetCommunity, send_activity_in_community},
-    generate_activity_id,
-    verify_activity,
-    verify_is_public,
-    verify_mod_action,
-    verify_person_in_community,
-  },
-  activity_lists::AnnouncableActivities,
-  objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::activities::community::{
-    block_user::BlockUserFromCommunity,
-    undo_block_user::UndoBlockUserFromCommunity,
-  },
-};
-use activitystreams_kinds::{activity::UndoType, public};
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::{
-  data::Data,
-  object_id::ObjectId,
-  traits::{ActivityHandler, ActorType},
-};
-use lemmy_db_schema::{
-  source::community::{CommunityPersonBan, CommunityPersonBanForm},
-  traits::Bannable,
-};
-use lemmy_utils::LemmyError;
-use lemmy_websocket::LemmyContext;
-
-impl UndoBlockUserFromCommunity {
-  #[tracing::instrument(skip_all)]
-  pub async fn send(
-    community: &ApubCommunity,
-    target: &ApubPerson,
-    actor: &ApubPerson,
-    context: &LemmyContext,
-  ) -> Result<(), LemmyError> {
-    let block = BlockUserFromCommunity::new(community, target, actor, None, context)?;
-
-    let id = generate_activity_id(
-      UndoType::Undo,
-      &context.settings().get_protocol_and_hostname(),
-    )?;
-    let undo = UndoBlockUserFromCommunity {
-      actor: ObjectId::new(actor.actor_id()),
-      to: vec![public()],
-      object: block,
-      cc: vec![community.actor_id()],
-      kind: UndoType::Undo,
-      id: id.clone(),
-      unparsed: Default::default(),
-    };
-
-    let activity = AnnouncableActivities::UndoBlockUserFromCommunity(undo);
-    let inboxes = vec![target.shared_inbox_or_inbox_url()];
-    send_activity_in_community(activity, &id, actor, community, inboxes, context).await
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ActivityHandler for UndoBlockUserFromCommunity {
-  type DataType = LemmyContext;
-
-  #[tracing::instrument(skip_all)]
-  async fn verify(
-    &self,
-    context: &Data<LemmyContext>,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    verify_is_public(&self.to, &self.cc)?;
-    verify_activity(&self.id, self.actor.inner(), &context.settings())?;
-    let community = self.get_community(context, request_counter).await?;
-    verify_person_in_community(&self.actor, &community, context, request_counter).await?;
-    verify_mod_action(&self.actor, &community, context, request_counter).await?;
-    self.object.verify(context, request_counter).await?;
-    Ok(())
-  }
-
-  #[tracing::instrument(skip_all)]
-  async fn receive(
-    self,
-    context: &Data<LemmyContext>,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    let community = self.get_community(context, request_counter).await?;
-    let blocked_user = self
-      .object
-      .object
-      .dereference(context, context.client(), request_counter)
-      .await?;
-
-    let community_user_ban_form = CommunityPersonBanForm {
-      community_id: community.id,
-      person_id: blocked_user.id,
-      expires: None,
-    };
-
-    blocking(context.pool(), move |conn: &'_ _| {
-      CommunityPersonBan::unban(conn, &community_user_ban_form)
-    })
-    .await??;
-
-    Ok(())
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl GetCommunity for UndoBlockUserFromCommunity {
-  #[tracing::instrument(skip_all)]
-  async fn get_community(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<ApubCommunity, LemmyError> {
-    self.object.get_community(context, request_counter).await
-  }
-}
index e09d69255ab0d40eb7c1a63eb53f9f02eb10e23e..343633c8f104169f51b68740fa4436985df7f411 100644 (file)
@@ -25,6 +25,7 @@ use tracing::info;
 use url::{ParseError, Url};
 use uuid::Uuid;
 
+pub mod block;
 pub mod comment;
 pub mod community;
 pub mod deletion;
index 0512ed62c318ec86417963b84ceb758e26794327..c8666b93a68add90f8f096422e4bc30196e2f41a 100644 (file)
@@ -3,13 +3,12 @@ use crate::{
   objects::community::ApubCommunity,
   protocol::{
     activities::{
+      block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
       community::{
         add_mod::AddMod,
         announce::AnnounceActivity,
-        block_user::BlockUserFromCommunity,
         remove_mod::RemoveMod,
         report::Report,
-        undo_block_user::UndoBlockUserFromCommunity,
         update::UpdateCommunity,
       },
       create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
@@ -78,14 +77,22 @@ pub enum AnnouncableActivities {
   Delete(Delete),
   UndoDelete(UndoDelete),
   UpdateCommunity(UpdateCommunity),
-  BlockUserFromCommunity(BlockUserFromCommunity),
-  UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
+  BlockUser(BlockUser),
+  UndoBlockUser(UndoBlockUser),
   AddMod(AddMod),
   RemoveMod(RemoveMod),
   // For compatibility with Pleroma/Mastodon (send only)
   Page(Page),
 }
 
+#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
+#[serde(untagged)]
+#[activity_handler(LemmyContext)]
+pub enum SiteInboxActivities {
+  BlockUser(BlockUser),
+  UndoBlockUser(UndoBlockUser),
+}
+
 #[async_trait::async_trait(?Send)]
 impl GetCommunity for AnnouncableActivities {
   #[tracing::instrument(skip(self, context))]
@@ -103,8 +110,8 @@ impl GetCommunity for AnnouncableActivities {
       Delete(a) => a.get_community(context, request_counter).await?,
       UndoDelete(a) => a.get_community(context, request_counter).await?,
       UpdateCommunity(a) => a.get_community(context, request_counter).await?,
-      BlockUserFromCommunity(a) => a.get_community(context, request_counter).await?,
-      UndoBlockUserFromCommunity(a) => a.get_community(context, request_counter).await?,
+      BlockUser(a) => a.get_community(context, request_counter).await?,
+      UndoBlockUser(a) => a.get_community(context, request_counter).await?,
       AddMod(a) => a.get_community(context, request_counter).await?,
       RemoveMod(a) => a.get_community(context, request_counter).await?,
       Page(_) => unimplemented!(),
index 338ed0f5a282e56661d0e053a39e6b390888de88..72d23d4acd5287d23a0d7e65ec5e0fbb87512987 100644 (file)
@@ -148,6 +148,7 @@ mod tests {
     source::{
       community::Community,
       person::{Person, PersonForm},
+      site::Site,
     },
     traits::Crud,
   };
@@ -159,6 +160,7 @@ mod tests {
     let client = reqwest::Client::new().into();
     let manager = create_activity_queue(client);
     let context = init_context(manager.queue_handle().clone());
+    let (new_mod, site) = parse_lemmy_person(&context).await;
     let community = parse_lemmy_community(&context).await;
     let community_id = community.id;
 
@@ -174,7 +176,7 @@ mod tests {
 
     CommunityModerator::join(&context.pool().get().unwrap(), &community_moderator_form).unwrap();
 
-    let new_mod = parse_lemmy_person(&context).await;
+    assert_eq!(site.actor_id.to_string(), "https://enterprise.lemmy.ml/");
 
     let json: GroupModerators =
       file_to_json_object("assets/lemmy/collections/group_moderators.json").unwrap();
@@ -209,5 +211,6 @@ mod tests {
       community_context.0.id,
     )
     .unwrap();
+    Site::delete(&*community_context.1.pool().get().unwrap(), site.id).unwrap();
   }
 }
diff --git a/crates/apub/src/fetcher/deletable_apub_object.rs b/crates/apub/src/fetcher/deletable_apub_object.rs
new file mode 100644 (file)
index 0000000..ccb409e
--- /dev/null
@@ -0,0 +1,99 @@
+use crate::fetcher::post_or_comment::PostOrComment;
+use lemmy_api_common::blocking;
+use lemmy_db_queries::source::{
+  comment::Comment_,
+  community::Community_,
+  person::Person_,
+  post::Post_,
+};
+use lemmy_db_schema::source::{
+  comment::Comment,
+  community::Community,
+  person::Person,
+  post::Post,
+  site::Site,
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+
+// TODO: merge this trait with ApubObject (means that db_schema needs to depend on apub_lib)
+#[async_trait::async_trait(?Send)]
+pub trait DeletableApubObject {
+  // TODO: pass in tombstone with summary field, to decide between remove/delete
+  async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError>;
+}
+
+#[async_trait::async_trait(?Send)]
+impl DeletableApubObject for Community {
+  async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
+    let id = self.id;
+    blocking(context.pool(), move |conn| {
+      Community::update_deleted(conn, id, true)
+    })
+    .await??;
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl DeletableApubObject for Person {
+  async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
+    let id = self.id;
+    blocking(context.pool(), move |conn| Person::delete_account(conn, id)).await??;
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl DeletableApubObject for Post {
+  async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
+    let id = self.id;
+    blocking(context.pool(), move |conn| {
+      Post::update_deleted(conn, id, true)
+    })
+    .await??;
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl DeletableApubObject for Comment {
+  async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
+    let id = self.id;
+    blocking(context.pool(), move |conn| {
+      Comment::update_deleted(conn, id, true)
+    })
+    .await??;
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl DeletableApubObject for PostOrComment {
+  async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
+    match self {
+      PostOrComment::Comment(c) => {
+        blocking(context.pool(), move |conn| {
+          Comment::update_deleted(conn, c.id, true)
+        })
+        .await??;
+      }
+      PostOrComment::Post(p) => {
+        blocking(context.pool(), move |conn| {
+          Post::update_deleted(conn, p.id, true)
+        })
+        .await??;
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl DeletableApubObject for Site {
+  async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
+    // not implemented, ignore
+    Ok(())
+  }
+}
index 399793e14e34fe36cdd96f37a467a0fb6863a9ba..1cf705ae24e5122460883c1c7a3dad19343de299 100644 (file)
@@ -36,6 +36,7 @@ mod community;
 mod person;
 mod post;
 pub mod routes;
+pub mod site;
 
 #[tracing::instrument(skip_all)]
 pub async fn shared_inbox(
index bc0633d8d58e071832e4dcaa3f68eefb8d17ef64..9081d5a554dc7d27250442112379c8810acde490 100644 (file)
@@ -1,6 +1,7 @@
 use crate::{
   activity_lists::PersonInboxActivities,
   context::WithContext,
+  generate_outbox_url,
   http::{
     create_apub_response,
     create_apub_tombstone_response,
@@ -9,7 +10,7 @@ use crate::{
     ActivityCommonFields,
   },
   objects::person::ApubPerson,
-  protocol::collections::person_outbox::PersonOutbox,
+  protocol::collections::empty_outbox::EmptyOutbox,
 };
 use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
 use lemmy_api_common::blocking;
@@ -80,6 +81,7 @@ pub(crate) async fn get_apub_person_outbox(
     Person::read_from_name(conn, &info.user_name)
   })
   .await??;
-  let outbox = PersonOutbox::new(person).await?;
+  let outbox_id = generate_outbox_url(&person.actor_id)?.into();
+  let outbox = EmptyOutbox::new(outbox_id).await?;
   Ok(create_apub_response(&outbox))
 }
index a57290e4b9023bbfcf7aa51bd3daa492fa64ad9b..d90bf40029cdc7e18b5feff5101940d00a81c1ce 100644 (file)
@@ -11,6 +11,7 @@ use crate::http::{
   person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
   post::get_apub_post,
   shared_inbox,
+  site::{get_apub_site_http, get_apub_site_inbox, get_apub_site_outbox},
 };
 use actix_web::{
   guard::{Guard, GuardContext},
@@ -26,6 +27,8 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
     println!("federation enabled, host is {}", settings.hostname);
 
     cfg
+      .route("/", web::get().to(get_apub_site_http))
+      .route("/site_outbox", web::get().to(get_apub_site_outbox))
       .route(
         "/c/{community_name}",
         web::get().to(get_apub_community_http),
@@ -57,7 +60,8 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
         .guard(InboxRequestGuard)
         .route("/c/{community_name}/inbox", web::post().to(community_inbox))
         .route("/u/{user_name}/inbox", web::post().to(person_inbox))
-        .route("/inbox", web::post().to(shared_inbox)),
+        .route("/inbox", web::post().to(shared_inbox))
+        .route("/site_inbox", web::post().to(get_apub_site_inbox)),
     );
   }
 }
diff --git a/crates/apub/src/http/site.rs b/crates/apub/src/http/site.rs
new file mode 100644 (file)
index 0000000..894622a
--- /dev/null
@@ -0,0 +1,49 @@
+use crate::{
+  activity_lists::SiteInboxActivities,
+  context::WithContext,
+  http::{create_apub_response, payload_to_string, receive_activity, ActivityCommonFields},
+  objects::instance::ApubSite,
+  protocol::collections::empty_outbox::EmptyOutbox,
+};
+use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::traits::ApubObject;
+use lemmy_db_schema::source::site::Site;
+use lemmy_utils::{settings::structs::Settings, LemmyError};
+use lemmy_websocket::LemmyContext;
+use tracing::info;
+use url::Url;
+
+pub(crate) async fn get_apub_site_http(
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let site: ApubSite = blocking(context.pool(), Site::read_local_site)
+    .await??
+    .into();
+
+  let apub = site.into_apub(&context).await?;
+  Ok(create_apub_response(&apub))
+}
+
+#[tracing::instrument(skip_all)]
+pub(crate) async fn get_apub_site_outbox() -> Result<HttpResponse, LemmyError> {
+  let outbox_id = format!(
+    "{}/site_outbox",
+    Settings::get().get_protocol_and_hostname()
+  );
+  let outbox = EmptyOutbox::new(Url::parse(&outbox_id)?).await?;
+  Ok(create_apub_response(&outbox))
+}
+
+#[tracing::instrument(skip_all)]
+pub async fn get_apub_site_inbox(
+  request: HttpRequest,
+  payload: Payload,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let unparsed = payload_to_string(payload).await?;
+  info!("Received site inbox activity {}", unparsed);
+  let activity_data: ActivityCommonFields = serde_json::from_str(&unparsed)?;
+  let activity = serde_json::from_str::<WithContext<SiteInboxActivities>>(&unparsed)?;
+  receive_activity(request, activity.inner(), activity_data, &context).await
+}
index 94752a39787c0cde43bd11fadc94a46c44301446..0652048dc3d23f93ea6e3e4ebb42c3d056b2bb99 100644 (file)
@@ -164,6 +164,12 @@ pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
   Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
 }
 
+pub fn generate_site_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
+  let mut actor_id: Url = actor_id.clone().into();
+  actor_id.set_path("site_inbox");
+  Ok(actor_id.into())
+}
+
 pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
   let actor_id: Url = actor_id.clone().into();
   let url = format!(
index ee131adf7ed0903cea176b9373c3066a94386994..82d9e4e4458816edabfc5859dc5fa7bf4ef9cc8a 100644 (file)
@@ -18,7 +18,7 @@ use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   object_id::ObjectId,
   traits::ApubObject,
-  values::{MediaTypeHtml, MediaTypeMarkdown},
+  values::MediaTypeHtml,
   verify::verify_domains_match,
 };
 use lemmy_db_schema::{
@@ -120,10 +120,7 @@ impl ApubObject for ApubComment {
       cc: maa.ccs,
       content: markdown_to_html(&self.content),
       media_type: Some(MediaTypeHtml::Html),
-      source: SourceCompat::Lemmy(Source {
-        content: self.content.clone(),
-        media_type: MediaTypeMarkdown::Markdown,
-      }),
+      source: SourceCompat::Lemmy(Source::new(self.content.clone())),
       in_reply_to,
       published: Some(convert_datetime(self.published)),
       updated: self.updated.map(convert_datetime),
@@ -213,19 +210,21 @@ pub(crate) mod tests {
   use super::*;
   use crate::objects::{
     community::{tests::parse_lemmy_community, ApubCommunity},
+    instance::ApubSite,
     person::{tests::parse_lemmy_person, ApubPerson},
     post::ApubPost,
     tests::{file_to_json_object, init_context},
   };
   use assert_json_diff::assert_json_include;
   use lemmy_apub_lib::activity_queue::create_activity_queue;
+  use lemmy_db_schema::source::site::Site;
   use serial_test::serial;
 
   async fn prepare_comment_test(
     url: &Url,
     context: &LemmyContext,
-  ) -> (ApubPerson, ApubCommunity, ApubPost) {
-    let person = parse_lemmy_person(context).await;
+  ) -> (ApubPerson, ApubCommunity, ApubPost, ApubSite) {
+    let (person, site) = parse_lemmy_person(context).await;
     let community = parse_lemmy_community(context).await;
     let post_json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
     ApubPost::verify(&post_json, url, context, &mut 0)
@@ -234,13 +233,14 @@ pub(crate) mod tests {
     let post = ApubPost::from_apub(post_json, context, &mut 0)
       .await
       .unwrap();
-    (person, community, post)
+    (person, community, post, site)
   }
 
-  fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost), context: &LemmyContext) {
+  fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost, ApubSite), context: &LemmyContext) {
     Post::delete(&*context.pool().get().unwrap(), data.2.id).unwrap();
     Community::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
     Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
+    Site::delete(&*context.pool().get().unwrap(), data.3.id).unwrap();
   }
 
   #[actix_rt::test]
index cce61b57f25940606a178f56f9856122bfcb2b4c..4a618bf8867a0b02a524df7e063066d6fd486f7f 100644 (file)
@@ -3,6 +3,7 @@ use crate::{
   collections::{community_moderators::ApubCommunityModerators, CommunityContext},
   generate_moderators_url,
   generate_outbox_url,
+  objects::instance::fetch_instance_actor_for_object,
   protocol::{
     objects::{group::Group, tombstone::Tombstone, Endpoints},
     ImageObject,
@@ -16,7 +17,6 @@ use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   object_id::ObjectId,
   traits::{ActorType, ApubObject},
-  values::MediaTypeMarkdown,
 };
 use lemmy_db_schema::{source::community::Community, traits::ApubActor};
 use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
@@ -80,22 +80,15 @@ impl ApubObject for ApubCommunity {
 
   #[tracing::instrument(skip_all)]
   async fn into_apub(self, _context: &LemmyContext) -> Result<Group, LemmyError> {
-    let source = self.description.clone().map(|bio| Source {
-      content: bio,
-      media_type: MediaTypeMarkdown::Markdown,
-    });
-    let icon = self.icon.clone().map(ImageObject::new);
-    let image = self.banner.clone().map(ImageObject::new);
-
     let group = Group {
       kind: GroupType::Group,
       id: ObjectId::new(self.actor_id()),
       preferred_username: self.name.clone(),
       name: self.title.clone(),
       summary: self.description.as_ref().map(|b| markdown_to_html(b)),
-      source,
-      icon,
-      image,
+      source: self.description.clone().map(Source::new),
+      icon: self.icon.clone().map(ImageObject::new),
+      image: self.banner.clone().map(ImageObject::new),
       sensitive: Some(self.nsfw),
       moderators: Some(ObjectId::<ApubCommunityModerators>::new(
         generate_moderators_url(&self.actor_id)?,
@@ -160,6 +153,8 @@ impl ApubObject for ApubCommunity {
         .ok();
     }
 
+    fetch_instance_actor_for_object(community.actor_id(), context, request_counter).await;
+
     Ok(community)
   }
 }
@@ -219,9 +214,12 @@ impl ApubCommunity {
 #[cfg(test)]
 pub(crate) mod tests {
   use super::*;
-  use crate::objects::tests::{file_to_json_object, init_context};
+  use crate::objects::{
+    instance::tests::parse_lemmy_instance,
+    tests::{file_to_json_object, init_context},
+  };
   use lemmy_apub_lib::activity_queue::create_activity_queue;
-  use lemmy_db_schema::traits::Crud;
+  use lemmy_db_schema::{source::site::Site, traits::Crud};
   use serial_test::serial;
 
   pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
@@ -239,7 +237,7 @@ pub(crate) mod tests {
     let community = ApubCommunity::from_apub(json, context, &mut request_counter)
       .await
       .unwrap();
-    // this makes two requests to the (intentionally) broken outbox/moderators collections
+    // this makes one requests to the (intentionally broken) outbox collection
     assert_eq!(request_counter, 1);
     community
   }
@@ -250,6 +248,7 @@ pub(crate) mod tests {
     let client = reqwest::Client::new().into();
     let manager = create_activity_queue(client);
     let context = init_context(manager.queue_handle().clone());
+    let site = parse_lemmy_instance(&context).await;
     let community = parse_lemmy_community(&context).await;
 
     assert_eq!(community.title, "Ten Forward");
@@ -257,5 +256,6 @@ pub(crate) mod tests {
     assert_eq!(community.description.as_ref().unwrap().len(), 132);
 
     Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
+    Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
   }
 }
diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs
new file mode 100644 (file)
index 0000000..cdd76c5
--- /dev/null
@@ -0,0 +1,221 @@
+use crate::{
+  check_is_apub_id_valid,
+  objects::get_summary_from_string_or_source,
+  protocol::{objects::instance::Instance, ImageObject, Source, Unparsed},
+};
+use activitystreams_kinds::actor::ServiceType;
+use chrono::NaiveDateTime;
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  object_id::ObjectId,
+  traits::{ActorType, ApubObject},
+  values::MediaTypeHtml,
+  verify::verify_domains_match,
+};
+use lemmy_db_schema::{
+  naive_now,
+  source::site::{Site, SiteForm},
+};
+use lemmy_utils::{
+  utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
+  LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+use std::ops::Deref;
+use tracing::debug;
+use url::Url;
+
+#[derive(Clone, Debug)]
+pub struct ApubSite(Site);
+
+impl Deref for ApubSite {
+  type Target = Site;
+  fn deref(&self) -> &Self::Target {
+    &self.0
+  }
+}
+
+impl From<Site> for ApubSite {
+  fn from(s: Site) -> Self {
+    ApubSite { 0: s }
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl ApubObject for ApubSite {
+  type DataType = LemmyContext;
+  type ApubType = Instance;
+  type TombstoneType = ();
+
+  fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
+    Some(self.last_refreshed_at)
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn read_from_apub_id(
+    object_id: Url,
+    data: &Self::DataType,
+  ) -> Result<Option<Self>, LemmyError> {
+    Ok(
+      blocking(data.pool(), move |conn| {
+        Site::read_from_apub_id(conn, object_id)
+      })
+      .await??
+      .map(Into::into),
+    )
+  }
+
+  async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
+    unimplemented!()
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
+    let instance = Instance {
+      kind: ServiceType::Service,
+      id: ObjectId::new(self.actor_id()),
+      name: self.name.clone(),
+      content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
+      source: self.sidebar.clone().map(Source::new),
+      summary: self.description.clone(),
+      media_type: self.sidebar.as_ref().map(|_| MediaTypeHtml::Html),
+      icon: self.icon.clone().map(ImageObject::new),
+      image: self.banner.clone().map(ImageObject::new),
+      inbox: self.inbox_url.clone().into(),
+      outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
+      public_key: self.get_public_key()?,
+      published: convert_datetime(self.published),
+      updated: self.updated.map(convert_datetime),
+      unparsed: Unparsed::default(),
+    };
+    Ok(instance)
+  }
+
+  fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
+    unimplemented!()
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn verify(
+    apub: &Self::ApubType,
+    expected_domain: &Url,
+    data: &Self::DataType,
+    _request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    check_is_apub_id_valid(apub.id.inner(), true, &data.settings())?;
+    verify_domains_match(expected_domain, apub.id.inner())?;
+
+    let slur_regex = &data.settings().slur_regex();
+    check_slurs(&apub.name, slur_regex)?;
+    check_slurs_opt(&apub.summary, slur_regex)?;
+    Ok(())
+  }
+
+  #[tracing::instrument(skip_all)]
+  async fn from_apub(
+    apub: Self::ApubType,
+    data: &Self::DataType,
+    _request_counter: &mut i32,
+  ) -> Result<Self, LemmyError> {
+    let site_form = SiteForm {
+      name: apub.name.clone(),
+      sidebar: Some(get_summary_from_string_or_source(
+        &apub.content,
+        &apub.source,
+      )),
+      updated: apub.updated.map(|u| u.clone().naive_local()),
+      icon: Some(apub.icon.clone().map(|i| i.url.into())),
+      banner: Some(apub.image.clone().map(|i| i.url.into())),
+      description: Some(apub.summary.clone()),
+      actor_id: Some(apub.id.clone().into()),
+      last_refreshed_at: Some(naive_now()),
+      inbox_url: Some(apub.inbox.clone().into()),
+      public_key: Some(apub.public_key.public_key_pem.clone()),
+      ..SiteForm::default()
+    };
+    let site = blocking(data.pool(), move |conn| Site::upsert(conn, &site_form)).await??;
+    Ok(site.into())
+  }
+}
+
+impl ActorType for ApubSite {
+  fn actor_id(&self) -> Url {
+    self.actor_id.to_owned().into()
+  }
+  fn public_key(&self) -> String {
+    self.public_key.to_owned()
+  }
+  fn private_key(&self) -> Option<String> {
+    self.private_key.to_owned()
+  }
+
+  fn inbox_url(&self) -> Url {
+    self.inbox_url.clone().into()
+  }
+
+  fn shared_inbox_url(&self) -> Option<Url> {
+    None
+  }
+}
+
+/// Instance actor is at the root path, so we simply need to clear the path and other unnecessary
+/// parts of the url.
+pub fn instance_actor_id_from_url(mut url: Url) -> Url {
+  url.set_fragment(None);
+  url.set_path("");
+  url.set_query(None);
+  url
+}
+
+/// try to fetch the instance actor (to make things like instance rules available)
+pub(in crate::objects) async fn fetch_instance_actor_for_object(
+  object_id: Url,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) {
+  // try to fetch the instance actor (to make things like instance rules available)
+  let instance_id = instance_actor_id_from_url(object_id);
+  let site = ObjectId::<ApubSite>::new(instance_id.clone())
+    .dereference(context, context.client(), request_counter)
+    .await;
+  if let Err(e) = site {
+    debug!("Failed to dereference site for {}: {}", instance_id, e);
+  }
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+  use super::*;
+  use crate::objects::tests::{file_to_json_object, init_context};
+  use lemmy_apub_lib::activity_queue::create_activity_queue;
+  use lemmy_db_schema::traits::Crud;
+  use serial_test::serial;
+
+  pub(crate) async fn parse_lemmy_instance(context: &LemmyContext) -> ApubSite {
+    let json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
+    let id = Url::parse("https://enterprise.lemmy.ml/").unwrap();
+    let mut request_counter = 0;
+    ApubSite::verify(&json, &id, context, &mut request_counter)
+      .await
+      .unwrap();
+    let site = ApubSite::from_apub(json, context, &mut request_counter)
+      .await
+      .unwrap();
+    assert_eq!(request_counter, 0);
+    site
+  }
+
+  #[actix_rt::test]
+  #[serial]
+  async fn test_parse_lemmy_instance() {
+    let client = reqwest::Client::new().into();
+    let manager = create_activity_queue(client);
+    let context = init_context(manager.queue_handle().clone());
+    let site = parse_lemmy_instance(&context).await;
+
+    assert_eq!(site.name, "Enterprise");
+    assert_eq!(site.description.as_ref().unwrap().len(), 15);
+
+    Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
+  }
+}
index b5a4760d61186624bddd7cecc26525891885c940..d7e386b1063bdadb5b68f8c1797e2d4fa92991d9 100644 (file)
@@ -3,6 +3,7 @@ use html2md::parse_html;
 
 pub mod comment;
 pub mod community;
+pub mod instance;
 pub mod person;
 pub mod post;
 pub mod private_message;
index 21f0f248c52c15aca448cfbcd73e474a2b249e8c..80dd8bfd1c13640704e8e46245474f6224658a8b 100644 (file)
@@ -1,7 +1,7 @@
 use crate::{
   check_is_apub_id_valid,
   generate_outbox_url,
-  objects::get_summary_from_string_or_source,
+  objects::{get_summary_from_string_or_source, instance::fetch_instance_actor_for_object},
   protocol::{
     objects::{
       person::{Person, UserTypes},
@@ -16,7 +16,6 @@ use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   object_id::ObjectId,
   traits::{ActorType, ApubObject},
-  values::MediaTypeMarkdown,
   verify::verify_domains_match,
 };
 use lemmy_db_schema::{
@@ -88,12 +87,6 @@ impl ApubObject for ApubPerson {
     } else {
       UserTypes::Person
     };
-    let source = self.bio.clone().map(|bio| Source {
-      content: bio,
-      media_type: MediaTypeMarkdown::Markdown,
-    });
-    let icon = self.avatar.clone().map(ImageObject::new);
-    let image = self.banner.clone().map(ImageObject::new);
 
     let person = Person {
       kind,
@@ -101,9 +94,9 @@ impl ApubObject for ApubPerson {
       preferred_username: self.name.clone(),
       name: self.display_name.clone(),
       summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
-      source,
-      icon,
-      image,
+      source: self.bio.clone().map(Source::new),
+      icon: self.avatar.clone().map(ImageObject::new),
+      image: self.banner.clone().map(ImageObject::new),
       matrix_user_id: self.matrix_user_id.clone(),
       published: Some(convert_datetime(self.published)),
       outbox: generate_outbox_url(&self.actor_id)?.into(),
@@ -144,7 +137,7 @@ impl ApubObject for ApubPerson {
   async fn from_apub(
     person: Person,
     context: &LemmyContext,
-    _request_counter: &mut i32,
+    request_counter: &mut i32,
   ) -> Result<ApubPerson, LemmyError> {
     let person_form = PersonForm {
       name: person.preferred_username,
@@ -175,6 +168,10 @@ impl ApubObject for ApubPerson {
       DbPerson::upsert(conn, &person_form)
     })
     .await??;
+
+    let actor_id = person.actor_id.clone().into();
+    fetch_instance_actor_for_object(actor_id, context, request_counter).await;
+
     Ok(person.into())
   }
 }
@@ -204,12 +201,19 @@ impl ActorType for ApubPerson {
 #[cfg(test)]
 pub(crate) mod tests {
   use super::*;
-  use crate::objects::tests::{file_to_json_object, init_context};
+  use crate::{
+    objects::{
+      instance::{tests::parse_lemmy_instance, ApubSite},
+      tests::{file_to_json_object, init_context},
+    },
+    protocol::objects::instance::Instance,
+  };
   use lemmy_apub_lib::activity_queue::create_activity_queue;
-  use lemmy_db_schema::traits::Crud;
+  use lemmy_db_schema::{source::site::Site, traits::Crud};
   use serial_test::serial;
 
-  pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> ApubPerson {
+  pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> (ApubPerson, ApubSite) {
+    let site = parse_lemmy_instance(context).await;
     let json = file_to_json_object("assets/lemmy/objects/person.json").unwrap();
     let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
     let mut request_counter = 0;
@@ -220,7 +224,7 @@ pub(crate) mod tests {
       .await
       .unwrap();
     assert_eq!(request_counter, 0);
-    person
+    (person, site)
   }
 
   #[actix_rt::test]
@@ -229,13 +233,14 @@ pub(crate) mod tests {
     let client = reqwest::Client::new().into();
     let manager = create_activity_queue(client);
     let context = init_context(manager.queue_handle().clone());
-    let person = parse_lemmy_person(&context).await;
+    let (person, site) = parse_lemmy_person(&context).await;
 
     assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string()));
     assert!(!person.local);
     assert_eq!(person.bio.as_ref().unwrap().len(), 39);
 
     DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+    Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
   }
 
   #[actix_rt::test]
@@ -244,6 +249,16 @@ pub(crate) mod tests {
     let client = reqwest::Client::new().into();
     let manager = create_activity_queue(client);
     let context = init_context(manager.queue_handle().clone());
+
+    // create and parse a fake pleroma instance actor, to avoid network request during test
+    let mut json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
+    let id = Url::parse("https://queer.hacktivis.me/").unwrap();
+    json.id = ObjectId::new(id);
+    let mut request_counter = 0;
+    let site = ApubSite::from_apub(json, &context, &mut request_counter)
+      .await
+      .unwrap();
+
     let json = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
     let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
     let mut request_counter = 0;
@@ -261,5 +276,6 @@ pub(crate) mod tests {
     assert_eq!(person.bio.as_ref().unwrap().len(), 873);
 
     DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+    Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
   }
 }
index e4206080aeaaec84dee15e9d77c2c377a8c240cc..b15c9374b55d5bc38279c887de17734f67c9ad11 100644 (file)
@@ -214,6 +214,7 @@ mod tests {
     tests::{file_to_json_object, init_context},
   };
   use lemmy_apub_lib::activity_queue::create_activity_queue;
+  use lemmy_db_schema::source::site::Site;
   use serial_test::serial;
 
   #[actix_rt::test]
@@ -222,8 +223,8 @@ mod tests {
     let client = reqwest::Client::new().into();
     let manager = create_activity_queue(client);
     let context = init_context(manager.queue_handle().clone());
+    let (person, site) = parse_lemmy_person(&context).await;
     let community = parse_lemmy_community(&context).await;
-    let person = parse_lemmy_person(&context).await;
 
     let json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
     let url = Url::parse("https://enterprise.lemmy.ml/post/55143").unwrap();
@@ -246,5 +247,6 @@ mod tests {
     Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
     Person::delete(&*context.pool().get().unwrap(), person.id).unwrap();
     Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
+    Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
   }
 }
index 176ee009ee745fcb5e303546a76f503445f4e350..62af385533dbb3d459f367a4efc918c09faf2e8d 100644 (file)
@@ -8,7 +8,7 @@ use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   object_id::ObjectId,
   traits::ApubObject,
-  values::{MediaTypeHtml, MediaTypeMarkdown},
+  values::MediaTypeHtml,
   verify::verify_domains_match,
 };
 use lemmy_db_schema::{
@@ -87,10 +87,7 @@ impl ApubObject for ApubPrivateMessage {
       to: [ObjectId::new(recipient.actor_id)],
       content: markdown_to_html(&self.content),
       media_type: Some(MediaTypeHtml::Html),
-      source: Some(Source {
-        content: self.content.clone(),
-        media_type: MediaTypeMarkdown::Markdown,
-      }),
+      source: Some(Source::new(self.content.clone())),
       published: Some(convert_datetime(self.published)),
       updated: self.updated.map(convert_datetime),
       unparsed: Default::default(),
similarity index 65%
rename from crates/apub/src/protocol/activities/community/block_user.rs
rename to crates/apub/src/protocol/activities/block/block_user.rs
index 891fe1f553ffe23c8f26d52629c5d65411187bab..5d49fc602a00d1fcaee319da37ae7d202932ece6 100644 (file)
@@ -1,7 +1,4 @@
-use crate::{
-  objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::Unparsed,
-};
+use crate::{activities::block::SiteOrCommunity, objects::person::ApubPerson, protocol::Unparsed};
 use activitystreams_kinds::activity::BlockType;
 use chrono::{DateTime, FixedOffset};
 use lemmy_apub_lib::object_id::ObjectId;
@@ -10,16 +7,21 @@ use url::Url;
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct BlockUserFromCommunity {
+pub struct BlockUser {
   pub(crate) actor: ObjectId<ApubPerson>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: ObjectId<ApubPerson>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
-  pub(crate) target: ObjectId<ApubCommunity>,
+  pub(crate) target: ObjectId<SiteOrCommunity>,
   #[serde(rename = "type")]
   pub(crate) kind: BlockType,
+  /// Quick and dirty solution.
+  /// TODO: send a separate Delete activity instead
+  pub(crate) remove_data: Option<bool>,
+  /// block reason, written to mod log
+  pub(crate) summary: Option<String>,
   pub(crate) id: Url,
   #[serde(flatten)]
   pub(crate) unparsed: Unparsed,
diff --git a/crates/apub/src/protocol/activities/block/mod.rs b/crates/apub/src/protocol/activities/block/mod.rs
new file mode 100644 (file)
index 0000000..eb3736f
--- /dev/null
@@ -0,0 +1,17 @@
+pub mod block_user;
+pub mod undo_block_user;
+
+#[cfg(test)]
+mod tests {
+  use crate::protocol::{
+    activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+    tests::test_parse_lemmy_item,
+  };
+
+  #[actix_rt::test]
+  async fn test_parse_lemmy_block() {
+    test_parse_lemmy_item::<BlockUser>("assets/lemmy/activities/block/block_user.json").unwrap();
+    test_parse_lemmy_item::<UndoBlockUser>("assets/lemmy/activities/block/undo_block_user.json")
+      .unwrap();
+  }
+}
similarity index 78%
rename from crates/apub/src/protocol/activities/community/undo_block_user.rs
rename to crates/apub/src/protocol/activities/block/undo_block_user.rs
index 02218367bd3d74c7847d749ae790d47c0b987ad0..d3db580e401a5dec0eb2ec1fa2f17a876842bf88 100644 (file)
@@ -1,6 +1,6 @@
 use crate::{
   objects::person::ApubPerson,
-  protocol::{activities::community::block_user::BlockUserFromCommunity, Unparsed},
+  protocol::{activities::block::block_user::BlockUser, Unparsed},
 };
 use activitystreams_kinds::activity::UndoType;
 use lemmy_apub_lib::object_id::ObjectId;
@@ -9,11 +9,11 @@ use url::Url;
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct UndoBlockUserFromCommunity {
+pub struct UndoBlockUser {
   pub(crate) actor: ObjectId<ApubPerson>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
-  pub(crate) object: BlockUserFromCommunity,
+  pub(crate) object: BlockUser,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
index 56332a87771c3923a6fce28723f9748a44b84cbe..a25d5ca2d27c08ed67ab83c179778be30deea12e 100644 (file)
@@ -1,9 +1,7 @@
 pub mod add_mod;
 pub mod announce;
-pub mod block_user;
 pub mod remove_mod;
 pub mod report;
-pub mod undo_block_user;
 pub mod update;
 
 #[cfg(test)]
@@ -12,10 +10,8 @@ mod tests {
     activities::community::{
       add_mod::AddMod,
       announce::AnnounceActivity,
-      block_user::BlockUserFromCommunity,
       remove_mod::RemoveMod,
       report::Report,
-      undo_block_user::UndoBlockUserFromCommunity,
       update::UpdateCommunity,
     },
     tests::test_parse_lemmy_item,
@@ -32,15 +28,6 @@ mod tests {
     test_parse_lemmy_item::<RemoveMod>("assets/lemmy/activities/community/remove_mod.json")
       .unwrap();
 
-    test_parse_lemmy_item::<BlockUserFromCommunity>(
-      "assets/lemmy/activities/community/block_user.json",
-    )
-    .unwrap();
-    test_parse_lemmy_item::<UndoBlockUserFromCommunity>(
-      "assets/lemmy/activities/community/undo_block_user.json",
-    )
-    .unwrap();
-
     test_parse_lemmy_item::<UpdateCommunity>(
       "assets/lemmy/activities/community/update_community.json",
     )
index 2aa488f57e703d8f2fc6865a0957ebdef34805d5..47b01b241d99611d713e5118c276c207b8f2d4d8 100644 (file)
@@ -1,6 +1,7 @@
 use serde::{Deserialize, Serialize};
 use strum_macros::Display;
 
+pub mod block;
 pub mod community;
 pub mod create_or_update;
 pub mod deletion;
similarity index 60%
rename from crates/apub/src/protocol/collections/person_outbox.rs
rename to crates/apub/src/protocol/collections/empty_outbox.rs
index e616794c6c75a0b6601e2b0028ad4bdf294d5855..265575af4a722689b753fa12a5f21406ed7b9872 100644 (file)
@@ -1,24 +1,23 @@
-use crate::generate_outbox_url;
 use activitystreams_kinds::collection::OrderedCollectionType;
-use lemmy_db_schema::source::person::Person;
 use lemmy_utils::LemmyError;
 use serde::{Deserialize, Serialize};
 use url::Url;
 
+/// Empty placeholder outbox used for Person, Instance, which dont implement a proper outbox yet.
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub(crate) struct PersonOutbox {
+pub(crate) struct EmptyOutbox {
   r#type: OrderedCollectionType,
   id: Url,
   ordered_items: Vec<()>,
   total_items: i32,
 }
 
-impl PersonOutbox {
-  pub(crate) async fn new(user: Person) -> Result<PersonOutbox, LemmyError> {
-    Ok(PersonOutbox {
+impl EmptyOutbox {
+  pub(crate) async fn new(outbox_id: Url) -> Result<EmptyOutbox, LemmyError> {
+    Ok(EmptyOutbox {
       r#type: OrderedCollectionType::OrderedCollection,
-      id: generate_outbox_url(&user.actor_id)?.into(),
+      id: outbox_id,
       ordered_items: vec![],
       total_items: 0,
     })
index 183052be68d8b21524e135ae0c17927a3743b7d8..f34dd1d10800b763a8f6416e68b777837a23c611 100644 (file)
@@ -1,16 +1,16 @@
+pub(crate) mod empty_outbox;
 pub(crate) mod group_followers;
 pub(crate) mod group_moderators;
 pub(crate) mod group_outbox;
-pub(crate) mod person_outbox;
 
 #[cfg(test)]
 mod tests {
   use crate::protocol::{
     collections::{
+      empty_outbox::EmptyOutbox,
       group_followers::GroupFollowers,
       group_moderators::GroupModerators,
       group_outbox::GroupOutbox,
-      person_outbox::PersonOutbox,
     },
     tests::test_parse_lemmy_item,
   };
@@ -24,6 +24,6 @@ mod tests {
     assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items);
     test_parse_lemmy_item::<GroupModerators>("assets/lemmy/collections/group_moderators.json")
       .unwrap();
-    test_parse_lemmy_item::<PersonOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
+    test_parse_lemmy_item::<EmptyOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
   }
 }
index 4b3992fddcbe931801994a712bbe053f6034f66e..d1532a9523ae7f4d3b606cdcf6f0c79680b20c6c 100644 (file)
@@ -17,6 +17,15 @@ pub struct Source {
   pub(crate) media_type: MediaTypeMarkdown,
 }
 
+impl Source {
+  pub(crate) fn new(content: String) -> Self {
+    Source {
+      content,
+      media_type: MediaTypeMarkdown::Markdown,
+    }
+  }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct ImageObject {
diff --git a/crates/apub/src/protocol/objects/instance.rs b/crates/apub/src/protocol/objects/instance.rs
new file mode 100644 (file)
index 0000000..2a967ac
--- /dev/null
@@ -0,0 +1,39 @@
+use crate::{
+  objects::instance::ApubSite,
+  protocol::{ImageObject, Source, Unparsed},
+};
+use activitystreams_kinds::actor::ServiceType;
+use chrono::{DateTime, FixedOffset};
+use lemmy_apub_lib::{object_id::ObjectId, signatures::PublicKey, values::MediaTypeHtml};
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use url::Url;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Instance {
+  #[serde(rename = "type")]
+  pub(crate) kind: ServiceType,
+  pub(crate) id: ObjectId<ApubSite>,
+  // site name
+  pub(crate) name: String,
+  // sidebar
+  pub(crate) content: Option<String>,
+  pub(crate) source: Option<Source>,
+  // short instance description
+  pub(crate) summary: Option<String>,
+  pub(crate) media_type: Option<MediaTypeHtml>,
+  /// instance icon
+  pub(crate) icon: Option<ImageObject>,
+  /// instance banner
+  pub(crate) image: Option<ImageObject>,
+  pub(crate) inbox: Url,
+  /// mandatory field in activitypub, currently empty in lemmy
+  pub(crate) outbox: Url,
+  pub(crate) public_key: PublicKey,
+  pub(crate) published: DateTime<FixedOffset>,
+  pub(crate) updated: Option<DateTime<FixedOffset>>,
+  #[serde(flatten)]
+  pub(crate) unparsed: Unparsed,
+}
index 2367b686339e1dfb382d04715a6e83df6cd4722b..20aaca181cfef1f92560efb99ee4d041bd212445 100644 (file)
@@ -3,6 +3,7 @@ use url::Url;
 
 pub(crate) mod chat_message;
 pub(crate) mod group;
+pub(crate) mod instance;
 pub(crate) mod note;
 pub(crate) mod page;
 pub(crate) mod person;
@@ -23,6 +24,7 @@ mod tests {
       objects::{
         chat_message::ChatMessage,
         group::Group,
+        instance::Instance,
         note::Note,
         page::Page,
         person::Person,
@@ -33,9 +35,10 @@ mod tests {
   };
 
   #[actix_rt::test]
-  async fn test_parse_object_lemmy() {
-    test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
+  async fn test_parse_objects_lemmy() {
+    test_parse_lemmy_item::<Instance>("assets/lemmy/objects/instance.json").unwrap();
     test_parse_lemmy_item::<Group>("assets/lemmy/objects/group.json").unwrap();
+    test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
     test_parse_lemmy_item::<Page>("assets/lemmy/objects/page.json").unwrap();
     test_parse_lemmy_item::<Note>("assets/lemmy/objects/note.json").unwrap();
     test_parse_lemmy_item::<ChatMessage>("assets/lemmy/objects/chat_message.json").unwrap();
@@ -43,7 +46,7 @@ mod tests {
   }
 
   #[actix_rt::test]
-  async fn test_parse_object_pleroma() {
+  async fn test_parse_objects_pleroma() {
     file_to_json_object::<WithContext<Person>>("assets/pleroma/objects/person.json").unwrap();
     file_to_json_object::<WithContext<Note>>("assets/pleroma/objects/note.json").unwrap();
     file_to_json_object::<WithContext<ChatMessage>>("assets/pleroma/objects/chat_message.json")
@@ -51,19 +54,19 @@ mod tests {
   }
 
   #[actix_rt::test]
-  async fn test_parse_object_smithereen() {
+  async fn test_parse_objects_smithereen() {
     file_to_json_object::<WithContext<Person>>("assets/smithereen/objects/person.json").unwrap();
     file_to_json_object::<Note>("assets/smithereen/objects/note.json").unwrap();
   }
 
   #[actix_rt::test]
-  async fn test_parse_object_mastodon() {
+  async fn test_parse_objects_mastodon() {
     file_to_json_object::<WithContext<Person>>("assets/mastodon/objects/person.json").unwrap();
     file_to_json_object::<WithContext<Note>>("assets/mastodon/objects/note.json").unwrap();
   }
 
   #[actix_rt::test]
-  async fn test_parse_object_lotide() {
+  async fn test_parse_objects_lotide() {
     file_to_json_object::<WithContext<Group>>("assets/lotide/objects/group.json").unwrap();
     file_to_json_object::<WithContext<Person>>("assets/lotide/objects/person.json").unwrap();
     file_to_json_object::<WithContext<Note>>("assets/lotide/objects/note.json").unwrap();
index b948992470a380a2fb4d39e4d00d98fb24531d13..58dfe0b01b7c567ffacfe06f9a29d38f466c1071 100644 (file)
@@ -55,19 +55,7 @@ mod tests {
 
     let site_form = SiteForm {
       name: "test_site".into(),
-      sidebar: None,
-      description: None,
-      icon: None,
-      banner: None,
-      enable_downvotes: None,
-      open_registration: None,
-      enable_nsfw: None,
-      updated: None,
-      community_creation_admin_only: Some(false),
-      require_email_verification: None,
-      require_application: None,
-      application_question: None,
-      private_instance: None,
+      ..Default::default()
     };
 
     Site::create(&conn, &site_form).unwrap();
@@ -136,7 +124,8 @@ mod tests {
     let after_delete_creator = SiteAggregates::read(&conn);
     assert!(after_delete_creator.is_ok());
 
-    Site::delete(&conn, 1).unwrap();
+    let site_id = after_delete_creator.unwrap().id;
+    Site::delete(&conn, site_id).unwrap();
     let after_delete_site = SiteAggregates::read(&conn);
     assert!(after_delete_site.is_err());
   }
index d2b0d9cdebfee641f9863ccb73f88ba352a311e5..b2b3a6d8188d85c67d36d6ad8590c8d6c8f61245 100644 (file)
@@ -122,9 +122,9 @@ impl Community {
       .get_result::<Self>(conn)
   }
 
-  pub fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<String>, Error> {
+  pub fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<DbUrl>, Error> {
     use crate::schema::community::dsl::*;
-    community.select(actor_id).distinct().load::<String>(conn)
+    community.select(actor_id).distinct().load::<DbUrl>(conn)
   }
 
   pub fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
index 8a84bdfbbbf21a49c4dd2cbe2438840b72ceb6ab..60d2c013c2059fe8c3b4b21d2fadf0fbfaa386da 100644 (file)
@@ -1,5 +1,6 @@
-use crate::{source::site::*, traits::Crud};
+use crate::{source::site::*, traits::Crud, DbUrl};
 use diesel::{dsl::*, result::Error, *};
+use url::Url;
 
 impl Crud for Site {
   type Form = SiteForm;
@@ -27,8 +28,35 @@ impl Crud for Site {
 }
 
 impl Site {
-  pub fn read_simple(conn: &PgConnection) -> Result<Self, Error> {
+  pub fn read_local_site(conn: &PgConnection) -> Result<Self, Error> {
     use crate::schema::site::dsl::*;
-    site.first::<Self>(conn)
+    site.order_by(id).first::<Self>(conn)
+  }
+
+  pub fn upsert(conn: &PgConnection, site_form: &SiteForm) -> Result<Site, Error> {
+    use crate::schema::site::dsl::*;
+    insert_into(site)
+      .values(site_form)
+      .on_conflict(actor_id)
+      .do_update()
+      .set(site_form)
+      .get_result::<Self>(conn)
+  }
+
+  pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result<Option<Self>, Error> {
+    use crate::schema::site::dsl::*;
+    let object_id: DbUrl = object_id.into();
+    Ok(
+      site
+        .filter(actor_id.eq(object_id))
+        .first::<Site>(conn)
+        .ok()
+        .map(Into::into),
+    )
+  }
+
+  pub fn read_remote_sites(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+    use crate::schema::site::dsl::*;
+    site.order_by(id).offset(1).get_results::<Self>(conn)
   }
 }
index b863a250014df2d39acfc663612a16b40cbf6cc7..3e68768bcb9b02a0628c1b88007ebc48d280b7f3 100644 (file)
@@ -10,6 +10,7 @@ use std::{
   fmt,
   fmt::{Display, Formatter},
   io::Write,
+  ops::Deref,
 };
 use url::Url;
 
@@ -125,3 +126,11 @@ where
     DbUrl(id.into())
   }
 }
+
+impl Deref for DbUrl {
+  type Target = Url;
+
+  fn deref(&self) -> &Self::Target {
+    &self.0
+  }
+}
index 681452d9f290c8ddbf50ebd7a0bd2db65bfec968..b93cd4e728e91a045abec1065dbd0212f9998ec8 100644 (file)
@@ -454,6 +454,11 @@ table! {
         require_application -> Bool,
         application_question -> Nullable<Text>,
         private_instance -> Bool,
+        actor_id -> Text,
+        last_refreshed_at -> Timestamp,
+        inbox_url -> Text,
+        private_key -> Nullable<Text>,
+        public_key -> Text,
     }
 }
 
index 01c5bc16ec7195cc383280c3cca611215d08c467..25bed1c243b47c91acf686a3245723893110be2d 100644 (file)
@@ -20,6 +20,11 @@ pub struct Site {
   pub require_application: bool,
   pub application_question: Option<String>,
   pub private_instance: bool,
+  pub actor_id: DbUrl,
+  pub last_refreshed_at: chrono::NaiveDateTime,
+  pub inbox_url: DbUrl,
+  pub private_key: Option<String>,
+  pub public_key: String,
 }
 
 #[derive(Insertable, AsChangeset, Default)]
@@ -40,4 +45,9 @@ pub struct SiteForm {
   pub require_application: Option<bool>,
   pub application_question: Option<Option<String>>,
   pub private_instance: Option<bool>,
+  pub actor_id: Option<DbUrl>,
+  pub last_refreshed_at: Option<chrono::NaiveDateTime>,
+  pub inbox_url: Option<DbUrl>,
+  pub private_key: Option<Option<String>>,
+  pub public_key: Option<String>,
 }
index e77d7c6ce770298a51bb84b6b3026199d54499e4..1e8b8b52a0d39a2ce3fac7db04dc7b9be8c78658 100644 (file)
@@ -14,11 +14,12 @@ pub struct SiteView {
 
 impl SiteView {
   pub fn read(conn: &PgConnection) -> Result<Self, Error> {
-    let (site, counts) = site::table
+    let (mut site, counts) = site::table
       .inner_join(site_aggregates::table)
       .select((site::all_columns, site_aggregates::all_columns))
       .first::<(Site, SiteAggregates)>(conn)?;
 
+    site.private_key = None;
     Ok(SiteView { site, counts })
   }
 }
diff --git a/migrations/2022-01-28-104106_instance-actor/down.sql b/migrations/2022-01-28-104106_instance-actor/down.sql
new file mode 100644 (file)
index 0000000..a258c27
--- /dev/null
@@ -0,0 +1,6 @@
+alter table site
+    drop column actor_id,
+    drop column last_refreshed_at,
+    drop column inbox_url,
+    drop column private_key,
+    drop column public_key;
diff --git a/migrations/2022-01-28-104106_instance-actor/up.sql b/migrations/2022-01-28-104106_instance-actor/up.sql
new file mode 100644 (file)
index 0000000..914ab75
--- /dev/null
@@ -0,0 +1,6 @@
+alter table site
+    add column actor_id varchar(255) not null unique default generate_unique_changeme(),
+    add column last_refreshed_at Timestamp not null default now(),
+    add column inbox_url varchar(255) not null default generate_unique_changeme(),
+    add column private_key text,
+    add column public_key text not null default generate_unique_changeme();
index 4737066bbdbea75c6fc90fe7570c018a56e0b31f..161dd3b607f83e89108311fa63017e41a61eec85 100644 (file)
@@ -8,6 +8,7 @@ use lemmy_apub::{
   generate_inbox_url,
   generate_local_apub_endpoint,
   generate_shared_inbox_url,
+  generate_site_inbox_url,
   EndpointType,
 };
 use lemmy_db_schema::{
@@ -18,11 +19,13 @@ use lemmy_db_schema::{
     person::{Person, PersonForm},
     post::Post,
     private_message::PrivateMessage,
+    site::{Site, SiteForm},
   },
   traits::Crud,
 };
 use lemmy_utils::{apub::generate_actor_keypair, LemmyError};
 use tracing::info;
+use url::Url;
 
 pub fn run_advanced_migrations(
   conn: &PgConnection,
@@ -35,6 +38,7 @@ pub fn run_advanced_migrations(
   private_message_updates_2020_05_05(conn, protocol_and_hostname)?;
   post_thumbnail_url_updates_2020_07_27(conn, protocol_and_hostname)?;
   apub_columns_2021_02_02(conn)?;
+  instance_actor_2022_01_28(conn, protocol_and_hostname)?;
 
   Ok(())
 }
@@ -284,3 +288,29 @@ fn apub_columns_2021_02_02(conn: &PgConnection) -> Result<(), LemmyError> {
 
   Ok(())
 }
+
+/// Site object turns into an actor, so that things like instance description can be federated. This
+/// means we need to add actor columns to the site table, and initialize them with correct values.
+/// Before this point, there is only a single value in the site table which refers to the local
+/// Lemmy instance, so thats all we need to update.
+fn instance_actor_2022_01_28(
+  conn: &PgConnection,
+  protocol_and_hostname: &str,
+) -> Result<(), LemmyError> {
+  info!("Running instance_actor_2021_09_29");
+  if let Ok(site) = Site::read_local_site(conn) {
+    let key_pair = generate_actor_keypair()?;
+    let actor_id = Url::parse(protocol_and_hostname)?;
+    let site_form = SiteForm {
+      name: site.name,
+      actor_id: Some(actor_id.clone().into()),
+      last_refreshed_at: Some(naive_now()),
+      inbox_url: Some(generate_site_inbox_url(&actor_id.into())?),
+      private_key: Some(Some(key_pair.private_key)),
+      public_key: Some(key_pair.public_key),
+      ..Default::default()
+    };
+    Site::update(conn, site.id, &site_form)?;
+  }
+  Ok(())
+}