From 95ac570a24b891de67fbffe9a3cea9b1d2662faa Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 28 Sep 2021 18:06:33 +0200 Subject: [PATCH] Implement instance actor --- crates/api/src/community.rs | 1 + crates/api/src/local_user.rs | 4 +- crates/api/src/site.rs | 4 +- crates/api_common/src/community.rs | 11 +- crates/api_common/src/lib.rs | 6 +- crates/api_crud/src/community/create.rs | 2 +- crates/api_crud/src/community/read.rs | 14 +- crates/api_crud/src/site/create.rs | 14 +- crates/api_crud/src/site/update.rs | 4 +- crates/api_crud/src/user/create.rs | 2 +- .../apub/assets/lemmy/objects/instance.json | 39 ++++ .../src/collections/community_moderators.rs | 4 + .../apub/src/fetcher/deletable_apub_object.rs | 99 +++++++++ crates/apub/src/http/mod.rs | 1 + crates/apub/src/http/person.rs | 6 +- crates/apub/src/http/routes.rs | 3 + crates/apub/src/http/site.rs | 33 +++ crates/apub/src/objects/comment.rs | 17 +- crates/apub/src/objects/community.rs | 37 ++-- crates/apub/src/objects/instance.rs | 205 ++++++++++++++++++ crates/apub/src/objects/mod.rs | 1 + crates/apub/src/objects/person.rs | 13 +- crates/apub/src/objects/post.rs | 4 + crates/apub/src/objects/private_message.rs | 7 +- .../{person_outbox.rs => empty_outbox.rs} | 13 +- crates/apub/src/protocol/collections/mod.rs | 6 +- crates/apub/src/protocol/mod.rs | 9 + crates/apub/src/protocol/objects/instance.rs | 39 ++++ crates/apub/src/protocol/objects/mod.rs | 15 +- .../src/aggregates/site_aggregates.rs | 17 +- crates/db_schema/src/impls/site.rs | 29 ++- crates/db_schema/src/schema.rs | 5 + crates/db_schema/src/source/site.rs | 10 + .../2022-01-28-104106_instance-actor/down.sql | 6 + .../2022-01-28-104106_instance-actor/up.sql | 6 + src/code_migrations.rs | 29 +++ 36 files changed, 629 insertions(+), 86 deletions(-) create mode 100644 crates/apub/assets/lemmy/objects/instance.json create mode 100644 crates/apub/src/fetcher/deletable_apub_object.rs create mode 100644 crates/apub/src/http/site.rs create mode 100644 crates/apub/src/objects/instance.rs rename crates/apub/src/protocol/collections/{person_outbox.rs => empty_outbox.rs} (60%) create mode 100644 crates/apub/src/protocol/objects/instance.rs create mode 100644 migrations/2022-01-28-104106_instance-actor/down.sql create mode 100644 migrations/2022-01-28-104106_instance-actor/up.sql diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs index a1a44f094..5f4fe3c71 100644 --- a/crates/api/src/community.rs +++ b/crates/api/src/community.rs @@ -540,6 +540,7 @@ impl Perform for TransferCommunity { community_view, moderators, online: 0, + site: None, }) } } diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 974beb6ea..bd32280e7 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -91,7 +91,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 +200,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")); } diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 20ceffe57..92dbdd64d 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -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; diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index 9686172ac..03b224e00 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -1,4 +1,7 @@ -use lemmy_db_schema::newtypes::{CommunityId, PersonId}; +use lemmy_db_schema::{ + newtypes::{CommunityId, PersonId}, + source::site::Site, +}; use lemmy_db_views_actor::{ community_moderator_view::CommunityModeratorView, community_view::CommunityView, @@ -20,6 +23,12 @@ pub struct GetCommunityResponse { pub community_view: CommunityView, pub moderators: Vec, pub online: usize, + /// Metadata of the instance where the community is located. Only fields name, sidebar, + /// description, icon, banner, actor_id, last_refreshed_at get federated, everything else uses + /// default values. May be null if the community is hosted on an older Lemmy version, or on + /// another software. + /// TODO: this should probably be SiteView + pub site: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 2d1f1bf50..a49e8e4f3 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -267,7 +267,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 +281,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 { @@ -511,7 +511,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 { diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index fb090b864..d78c8ad7a 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -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", diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs index 7e61bfa71..caeffcadc 100644 --- a/crates/api_crud/src/community/read.rs +++ b/crates/api_crud/src/community/read.rs @@ -7,9 +7,10 @@ use lemmy_api_common::{ get_local_user_view_from_jwt_opt, resolve_actor_identifier, }; +use lemmy_apub::objects::instance::instance_actor_id_from_url; use lemmy_db_schema::{ from_opt_str_to_opt_enum, - source::community::Community, + source::{community::Community, site::Site}, traits::DeleteableOrRemoveable, ListingType, SortType, @@ -78,10 +79,21 @@ impl PerformCrud for GetCommunity { .await .unwrap_or(1); + let site_id = instance_actor_id_from_url(community_view.community.actor_id.clone().into()); + let site = blocking(context.pool(), move |conn| { + Site::read_from_apub_id(conn, site_id) + }) + .await + .map(|s| s.ok()) + .ok() + .flatten() + .flatten(); + let res = GetCommunityResponse { community_view, moderators, online, + site, }; // Return the jwt diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 3de2c3ab8..9b4266e2a 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -7,19 +7,25 @@ use lemmy_api_common::{ site::*, site_description_length_check, }; +use lemmy_apub::generate_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 { 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,8 @@ impl PerformCrud for CreateSite { site_description_length_check(desc)?; } + let actor_id: DbUrl = Url::parse(&Settings::get().get_protocol_and_hostname())?.into(); + let keypair = generate_actor_keypair()?; let site_form = SiteForm { name: data.name.to_owned(), sidebar, @@ -66,6 +74,10 @@ impl PerformCrud for CreateSite { open_registration: data.open_registration, enable_nsfw: data.enable_nsfw, community_creation_admin_only: data.community_creation_admin_only, + last_refreshed_at: Some(naive_now()), + inbox_url: Some(generate_inbox_url(&actor_id)?), + private_key: Some(Some(keypair.private_key)), + public_key: Some(keypair.public_key), ..SiteForm::default() }; diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 0a94062a7..c6f595230 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -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| { diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 6f8a5fa0e..06b576e51 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -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")); } diff --git a/crates/apub/assets/lemmy/objects/instance.json b/crates/apub/assets/lemmy/objects/instance.json new file mode 100644 index 000000000..73df87759 --- /dev/null +++ b/crates/apub/assets/lemmy/objects/instance.json @@ -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": "

Enterprise sidebar

\\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/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index 338ed0f5a..56b9bca97 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -140,6 +140,7 @@ mod tests { use super::*; use crate::objects::{ community::tests::parse_lemmy_community, + instance::tests::parse_lemmy_instance, person::tests::parse_lemmy_person, tests::{file_to_json_object, init_context}, }; @@ -148,6 +149,7 @@ mod tests { source::{ community::Community, person::{Person, PersonForm}, + site::Site, }, traits::Crud, }; @@ -159,6 +161,7 @@ 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; let community_id = community.id; @@ -209,5 +212,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 index 000000000..ccb409e8d --- /dev/null +++ b/crates/apub/src/fetcher/deletable_apub_object.rs @@ -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(()) + } +} diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index 399793e14..1cf705ae2 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -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( diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index bc0633d8d..9081d5a55 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -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)) } diff --git a/crates/apub/src/http/routes.rs b/crates/apub/src/http/routes.rs index a57290e4b..271c1b14e 100644 --- a/crates/apub/src/http/routes.rs +++ b/crates/apub/src/http/routes.rs @@ -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, }; 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_http)) .route( "/c/{community_name}", web::get().to(get_apub_community_http), diff --git a/crates/apub/src/http/site.rs b/crates/apub/src/http/site.rs new file mode 100644 index 000000000..3439ef1d9 --- /dev/null +++ b/crates/apub/src/http/site.rs @@ -0,0 +1,33 @@ +use crate::{ + http::create_apub_response, + objects::instance::ApubSite, + protocol::collections::empty_outbox::EmptyOutbox, +}; +use actix_web::{web, 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 url::Url; + +pub(crate) async fn get_apub_site_http( + context: web::Data, +) -> Result { + 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 { + 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)) +} diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index ee131adf7..f8e7045d2 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -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,22 @@ pub(crate) mod tests { use super::*; use crate::objects::{ community::{tests::parse_lemmy_community, ApubCommunity}, + instance::{tests::parse_lemmy_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) { + ) -> (ApubPerson, ApubCommunity, ApubPost, ApubSite) { let person = parse_lemmy_person(context).await; + let site = parse_lemmy_instance(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 +234,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] diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index cce61b57f..8070c131a 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -3,6 +3,7 @@ use crate::{ collections::{community_moderators::ApubCommunityModerators, CommunityContext}, generate_moderators_url, generate_outbox_url, + objects::instance::{instance_actor_id_from_url, ApubSite}, 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; @@ -26,7 +26,7 @@ use lemmy_utils::{ }; use lemmy_websocket::LemmyContext; use std::ops::Deref; -use tracing::debug; +use tracing::{debug, info}; use url::Url; #[derive(Clone, Debug)] @@ -80,22 +80,15 @@ impl ApubObject for ApubCommunity { #[tracing::instrument(skip_all)] async fn into_apub(self, _context: &LemmyContext) -> Result { - 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::::new( generate_moderators_url(&self.actor_id)?, @@ -160,6 +153,15 @@ impl ApubObject for ApubCommunity { .ok(); } + // try to fetch the instance actor (to make things like instance rules available) + let instance_id = instance_actor_id_from_url(community.actor_id.clone().into()); + let site = ObjectId::::new(instance_id.clone()) + .dereference(context, context.client(), request_counter) + .await; + if let Err(e) = site { + info!("Failed to dereference site for {}: {}", instance_id, e); + } + Ok(community) } } @@ -219,9 +221,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 +244,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 +255,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 +263,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 index 000000000..333092b1f --- /dev/null +++ b/crates/apub/src/objects/instance.rs @@ -0,0 +1,205 @@ +use crate::{ + check_is_apub_id_valid, + generate_outbox_url, + 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 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 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 { + Some(self.last_refreshed_at) + } + + #[tracing::instrument(skip_all)] + async fn read_from_apub_id( + object_id: Url, + data: &Self::DataType, + ) -> Result, 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 { + 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: generate_outbox_url(&self.actor_id)?.into(), + 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 { + 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 { + 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 { + self.private_key.to_owned() + } + + fn inbox_url(&self) -> Url { + self.inbox_url.clone().into() + } + + fn shared_inbox_url(&self) -> Option { + 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 +} + +#[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(); + } +} diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index b5a4760d6..d7e386b10 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -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; diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 21f0f248c..14245acf7 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -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(), diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index e4206080a..233d43951 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -209,11 +209,13 @@ mod tests { use super::*; use crate::objects::{ community::tests::parse_lemmy_community, + instance::tests::parse_lemmy_instance, person::tests::parse_lemmy_person, post::ApubPost, 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,6 +224,7 @@ 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; let person = parse_lemmy_person(&context).await; @@ -246,5 +249,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(); } } diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 176ee009e..62af38553 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -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(), diff --git a/crates/apub/src/protocol/collections/person_outbox.rs b/crates/apub/src/protocol/collections/empty_outbox.rs similarity index 60% rename from crates/apub/src/protocol/collections/person_outbox.rs rename to crates/apub/src/protocol/collections/empty_outbox.rs index e616794c6..265575af4 100644 --- a/crates/apub/src/protocol/collections/person_outbox.rs +++ b/crates/apub/src/protocol/collections/empty_outbox.rs @@ -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 { - Ok(PersonOutbox { +impl EmptyOutbox { + pub(crate) async fn new(outbox_id: Url) -> Result { + Ok(EmptyOutbox { r#type: OrderedCollectionType::OrderedCollection, - id: generate_outbox_url(&user.actor_id)?.into(), + id: outbox_id, ordered_items: vec![], total_items: 0, }) diff --git a/crates/apub/src/protocol/collections/mod.rs b/crates/apub/src/protocol/collections/mod.rs index 183052be6..f34dd1d10 100644 --- a/crates/apub/src/protocol/collections/mod.rs +++ b/crates/apub/src/protocol/collections/mod.rs @@ -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::("assets/lemmy/collections/group_moderators.json") .unwrap(); - test_parse_lemmy_item::("assets/lemmy/collections/person_outbox.json").unwrap(); + test_parse_lemmy_item::("assets/lemmy/collections/person_outbox.json").unwrap(); } } diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs index 4b3992fdd..d1532a952 100644 --- a/crates/apub/src/protocol/mod.rs +++ b/crates/apub/src/protocol/mod.rs @@ -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 index 000000000..2a967ac52 --- /dev/null +++ b/crates/apub/src/protocol/objects/instance.rs @@ -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, + // site name + pub(crate) name: String, + // sidebar + pub(crate) content: Option, + pub(crate) source: Option, + // short instance description + pub(crate) summary: Option, + pub(crate) media_type: Option, + /// instance icon + pub(crate) icon: Option, + /// instance banner + pub(crate) image: Option, + 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, + pub(crate) updated: Option>, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs index 2367b6863..20aaca181 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -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::("assets/lemmy/objects/person.json").unwrap(); + async fn test_parse_objects_lemmy() { + test_parse_lemmy_item::("assets/lemmy/objects/instance.json").unwrap(); test_parse_lemmy_item::("assets/lemmy/objects/group.json").unwrap(); + test_parse_lemmy_item::("assets/lemmy/objects/person.json").unwrap(); test_parse_lemmy_item::("assets/lemmy/objects/page.json").unwrap(); test_parse_lemmy_item::("assets/lemmy/objects/note.json").unwrap(); test_parse_lemmy_item::("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::>("assets/pleroma/objects/person.json").unwrap(); file_to_json_object::>("assets/pleroma/objects/note.json").unwrap(); file_to_json_object::>("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::>("assets/smithereen/objects/person.json").unwrap(); file_to_json_object::("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::>("assets/mastodon/objects/person.json").unwrap(); file_to_json_object::>("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::>("assets/lotide/objects/group.json").unwrap(); file_to_json_object::>("assets/lotide/objects/person.json").unwrap(); file_to_json_object::>("assets/lotide/objects/note.json").unwrap(); diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs index b94899247..58dfe0b01 100644 --- a/crates/db_schema/src/aggregates/site_aggregates.rs +++ b/crates/db_schema/src/aggregates/site_aggregates.rs @@ -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()); } diff --git a/crates/db_schema/src/impls/site.rs b/crates/db_schema/src/impls/site.rs index 8a84bdfbb..d38ef9715 100644 --- a/crates/db_schema/src/impls/site.rs +++ b/crates/db_schema/src/impls/site.rs @@ -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,30 @@ impl Crud for Site { } impl Site { - pub fn read_simple(conn: &PgConnection) -> Result { + pub fn read_local_site(conn: &PgConnection) -> Result { use crate::schema::site::dsl::*; - site.first::(conn) + site.order_by(id).first::(conn) + } + + pub fn upsert(conn: &PgConnection, site_form: &SiteForm) -> Result { + use crate::schema::site::dsl::*; + insert_into(site) + .values(site_form) + .on_conflict(actor_id) + .do_update() + .set(site_form) + .get_result::(conn) + } + + pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result, Error> { + use crate::schema::site::dsl::*; + let object_id: DbUrl = object_id.into(); + Ok( + site + .filter(actor_id.eq(object_id)) + .first::(conn) + .ok() + .map(Into::into), + ) } } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 681452d9f..b93cd4e72 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -454,6 +454,11 @@ table! { require_application -> Bool, application_question -> Nullable, private_instance -> Bool, + actor_id -> Text, + last_refreshed_at -> Timestamp, + inbox_url -> Text, + private_key -> Nullable, + public_key -> Text, } } diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index 01c5bc16e..25bed1c24 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -20,6 +20,11 @@ pub struct Site { pub require_application: bool, pub application_question: Option, pub private_instance: bool, + pub actor_id: DbUrl, + pub last_refreshed_at: chrono::NaiveDateTime, + pub inbox_url: DbUrl, + pub private_key: Option, + pub public_key: String, } #[derive(Insertable, AsChangeset, Default)] @@ -40,4 +45,9 @@ pub struct SiteForm { pub require_application: Option, pub application_question: Option>, pub private_instance: Option, + pub actor_id: Option, + pub last_refreshed_at: Option, + pub inbox_url: Option, + pub private_key: Option>, + pub public_key: Option, } 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 index 000000000..a258c27a4 --- /dev/null +++ b/migrations/2022-01-28-104106_instance-actor/down.sql @@ -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 index 000000000..914ab757e --- /dev/null +++ b/migrations/2022-01-28-104106_instance-actor/up.sql @@ -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(); diff --git a/src/code_migrations.rs b/src/code_migrations.rs index 4737066bb..07a969ec5 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -18,11 +18,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 +37,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 +287,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_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(()) +}