diff --git a/Cargo.lock b/Cargo.lock index fd404a8ef..83c8c9abf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1696,7 +1696,6 @@ name = "lemmy_apub" version = "0.1.0" dependencies = [ "activitystreams", - "activitystreams-ext", "actix", "actix-rt", "actix-web", diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 12d0afc9b..f18c2f869 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -19,7 +19,6 @@ lemmy_api_common = { path = "../api_common" } lemmy_websocket = { path = "../websocket" } diesel = "1.4.7" activitystreams = "0.7.0-alpha.11" -activitystreams-ext = "0.1.0-alpha.2" bcrypt = "0.10.0" chrono = { version = "0.4.19", features = ["serde"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs index c42bd53aa..bfc1b7fe6 100644 --- a/crates/apub/src/activities/comment/create_or_update.rs +++ b/crates/apub/src/activities/comment/create_or_update.rs @@ -96,7 +96,7 @@ impl ActivityHandler for CreateOrUpdateComment { request_counter, ) .await?; - verify_domains_match(&self.common.actor, &self.object.id)?; + verify_domains_match(&self.common.actor, self.object.id_unchecked())?; // TODO: should add a check that the correct community is in cc (probably needs changes to // comment deserialization) self.object.verify(context, request_counter).await?; @@ -108,14 +108,8 @@ impl ActivityHandler for CreateOrUpdateComment { context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let comment = Comment::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - false, - ) - .await?; + let comment = + Comment::from_apub(&self.object, context, &self.common.actor, request_counter).await?; let recipients = get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; let notif_type = match self.kind { diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index 8b6a32d73..964a42388 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -5,8 +5,7 @@ use crate::{ verify_mod_action, verify_person_in_community, }, - objects::FromApubToForm, - GroupExt, + objects::community::Group, }; use activitystreams::activity::kind::UpdateType; use lemmy_api_common::blocking; @@ -23,7 +22,7 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct UpdateCommunity { to: PublicUrl, - object: GroupExt, + object: Group, cc: [Url; 1], #[serde(rename = "type")] kind: UpdateType, @@ -47,7 +46,7 @@ impl ActivityHandler for UpdateCommunity { async fn receive( self, context: &LemmyContext, - request_counter: &mut i32, + _request_counter: &mut i32, ) -> Result<(), LemmyError> { let cc = self.cc[0].clone().into(); let community = blocking(context.pool(), move |conn| { @@ -55,14 +54,8 @@ impl ActivityHandler for UpdateCommunity { }) .await??; - let updated_community = CommunityForm::from_apub( - &self.object, - context, - community.actor_id.clone().into(), - request_counter, - false, - ) - .await?; + let updated_community = + Group::from_apub_to_form(&self.object, &community.actor_id.clone().into()).await?; let cf = CommunityForm { name: updated_community.name, title: updated_community.title, diff --git a/crates/apub/src/activities/post/create_or_update.rs b/crates/apub/src/activities/post/create_or_update.rs index 2096dcda3..94838e16f 100644 --- a/crates/apub/src/activities/post/create_or_update.rs +++ b/crates/apub/src/activities/post/create_or_update.rs @@ -87,7 +87,7 @@ impl ActivityHandler for CreateOrUpdatePost { verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?; match self.kind { CreateOrUpdateType::Create => { - verify_domains_match(&self.common.actor, &self.object.id)?; + verify_domains_match(&self.common.actor, self.object.id_unchecked())?; verify_urls_match(&self.common.actor, &self.object.attributed_to)?; // Check that the post isnt locked or stickied, as that isnt possible for newly created posts. // However, when fetching a remote post we generate a new create activity with the current @@ -104,7 +104,7 @@ impl ActivityHandler for CreateOrUpdatePost { if is_mod_action { verify_mod_action(&self.common.actor, community_id, context).await?; } else { - verify_domains_match(&self.common.actor, &self.object.id)?; + verify_domains_match(&self.common.actor, self.object.id_unchecked())?; verify_urls_match(&self.common.actor, &self.object.attributed_to)?; } } @@ -120,14 +120,7 @@ impl ActivityHandler for CreateOrUpdatePost { ) -> Result<(), LemmyError> { let actor = get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?; - let post = Post::from_apub( - &self.object, - context, - actor.actor_id(), - request_counter, - false, - ) - .await?; + let post = Post::from_apub(&self.object, context, &actor.actor_id(), request_counter).await?; let notif_type = match self.kind { CreateOrUpdateType::Create => UserOperationCrud::CreatePost, diff --git a/crates/apub/src/activities/private_message/create_or_update.rs b/crates/apub/src/activities/private_message/create_or_update.rs index 5ac90700d..d51e2d033 100644 --- a/crates/apub/src/activities/private_message/create_or_update.rs +++ b/crates/apub/src/activities/private_message/create_or_update.rs @@ -73,7 +73,7 @@ impl ActivityHandler for CreateOrUpdatePrivateMessage { ) -> Result<(), LemmyError> { verify_activity(self.common())?; verify_person(&self.common.actor, context, request_counter).await?; - verify_domains_match(&self.common.actor, &self.object.id)?; + verify_domains_match(&self.common.actor, self.object.id_unchecked())?; self.object.verify(context, request_counter).await?; Ok(()) } @@ -83,14 +83,8 @@ impl ActivityHandler for CreateOrUpdatePrivateMessage { context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let private_message = PrivateMessage::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - false, - ) - .await?; + let private_message = + PrivateMessage::from_apub(&self.object, context, &self.common.actor, request_counter).await?; let notif_type = match self.kind { CreateOrUpdateType::Create => UserOperationCrud::CreatePrivateMessage, diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index 3d6303421..6f88dd36f 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -85,7 +85,7 @@ impl CommunityType for Community { } else { let mut update = Update::new( mod_.actor_id(), - self.to_apub(context.pool()).await?.into_any_base()?, + AnyBase::from_arbitrary_json(self.to_apub(context.pool()).await?)?, ); update .set_many_contexts(lemmy_context()) diff --git a/crates/apub/src/extensions/group_extension.rs b/crates/apub/src/extensions/group_extension.rs deleted file mode 100644 index c83becf21..000000000 --- a/crates/apub/src/extensions/group_extension.rs +++ /dev/null @@ -1,43 +0,0 @@ -use activitystreams::unparsed::UnparsedMutExt; -use activitystreams_ext::UnparsedExtension; -use lemmy_utils::LemmyError; -use serde::{Deserialize, Serialize}; -use url::Url; - -/// Activitystreams extension to allow (de)serializing additional Community field -/// `sensitive` (called 'nsfw' in Lemmy). -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GroupExtension { - pub sensitive: Option, - pub moderators: Option, -} - -impl GroupExtension { - pub fn new(sensitive: bool, moderators_url: Url) -> Result { - Ok(GroupExtension { - sensitive: Some(sensitive), - moderators: Some(moderators_url), - }) - } -} - -impl UnparsedExtension for GroupExtension -where - U: UnparsedMutExt, -{ - type Error = serde_json::Error; - - fn try_from_unparsed(unparsed_mut: &mut U) -> Result { - Ok(GroupExtension { - sensitive: unparsed_mut.remove("sensitive")?, - moderators: unparsed_mut.remove("moderators")?, - }) - } - - fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { - unparsed_mut.insert("sensitive", self.sensitive)?; - unparsed_mut.insert("moderators", self.moderators)?; - Ok(()) - } -} diff --git a/crates/apub/src/extensions/mod.rs b/crates/apub/src/extensions/mod.rs index 6324182b7..7c58789a9 100644 --- a/crates/apub/src/extensions/mod.rs +++ b/crates/apub/src/extensions/mod.rs @@ -1,3 +1,2 @@ pub mod context; -pub(crate) mod group_extension; pub mod signatures; diff --git a/crates/apub/src/extensions/signatures.rs b/crates/apub/src/extensions/signatures.rs index be323d51b..8fded5152 100644 --- a/crates/apub/src/extensions/signatures.rs +++ b/crates/apub/src/extensions/signatures.rs @@ -1,5 +1,3 @@ -use activitystreams::unparsed::UnparsedMutExt; -use activitystreams_ext::UnparsedExtension; use actix_web::HttpRequest; use anyhow::anyhow; use http::{header::HeaderName, HeaderMap, HeaderValue}; @@ -90,15 +88,6 @@ pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), L } } -/// Extension for actor public key, which is needed on person and community for HTTP signatures. -/// -/// Taken from: https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PublicKeyExtension { - pub public_key: PublicKey, -} - #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PublicKey { @@ -106,29 +95,3 @@ pub struct PublicKey { pub owner: Url, pub public_key_pem: String, } - -impl PublicKey { - pub fn to_ext(&self) -> PublicKeyExtension { - PublicKeyExtension { - public_key: self.to_owned(), - } - } -} - -impl UnparsedExtension for PublicKeyExtension -where - U: UnparsedMutExt, -{ - type Error = serde_json::Error; - - fn try_from_unparsed(unparsed_mut: &mut U) -> Result { - Ok(PublicKeyExtension { - public_key: unparsed_mut.remove("publicKey")?, - }) - } - - fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { - unparsed_mut.insert("publicKey", self.public_key)?; - Ok(()) - } -} diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index 670493a98..ff94129be 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -6,13 +6,9 @@ use crate::{ person::get_or_fetch_and_upsert_person, should_refetch_actor, }, - objects::FromApub, - GroupExt, -}; -use activitystreams::{ - actor::ApActorExt, - collection::{CollectionExt, OrderedCollection}, + objects::{community::Group, FromApub}, }; +use activitystreams::collection::{CollectionExt, OrderedCollection}; use anyhow::Context; use diesel::result::Error::NotFound; use lemmy_api_common::blocking; @@ -63,7 +59,7 @@ async fn fetch_remote_community( old_community: Option, request_counter: &mut i32, ) -> Result { - let group = fetch_remote_object::(context.client(), apub_id, request_counter).await; + let group = fetch_remote_object::(context.client(), apub_id, request_counter).await; if let Some(c) = old_community.to_owned() { if is_deleted(&group) { @@ -78,22 +74,20 @@ async fn fetch_remote_community( } let group = group?; - let community = - Community::from_apub(&group, context, apub_id.to_owned(), request_counter, false).await?; + let community = Community::from_apub(&group, context, apub_id, request_counter).await?; update_community_mods(&group, &community, context, request_counter).await?; // only fetch outbox for new communities, otherwise this can create an infinite loop if old_community.is_none() { - let outbox = group.inner.outbox()?.context(location_info!())?; - fetch_community_outbox(context, outbox, request_counter).await? + fetch_community_outbox(context, &group.outbox, request_counter).await? } Ok(community) } async fn update_community_mods( - group: &GroupExt, + group: &Group, community: &Community, context: &LemmyContext, request_counter: &mut i32, @@ -168,10 +162,10 @@ async fn fetch_community_outbox( pub(crate) async fn fetch_community_mods( context: &LemmyContext, - group: &GroupExt, + group: &Group, recursion_counter: &mut i32, ) -> Result, LemmyError> { - if let Some(mods_url) = &group.ext_one.moderators { + if let Some(mods_url) = &group.moderators { let mods = fetch_remote_object::(context.client(), mods_url, recursion_counter) .await?; diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index a06b99d63..835bf63a3 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -34,14 +34,7 @@ pub async fn get_or_fetch_and_insert_post( debug!("Fetching and creating remote post: {}", post_ap_id); let page = fetch_remote_object::(context.client(), post_ap_id, recursion_counter).await?; - let post = Post::from_apub( - &page, - context, - post_ap_id.to_owned(), - recursion_counter, - false, - ) - .await?; + let post = Post::from_apub(&page, context, post_ap_id, recursion_counter).await?; Ok(post) } @@ -73,14 +66,7 @@ pub async fn get_or_fetch_and_insert_comment( ); let comment = fetch_remote_object::(context.client(), comment_ap_id, recursion_counter).await?; - let comment = Comment::from_apub( - &comment, - context, - comment_ap_id.to_owned(), - recursion_counter, - false, - ) - .await?; + let comment = Comment::from_apub(&comment, context, comment_ap_id, recursion_counter).await?; let post_id = comment.post_id; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; diff --git a/crates/apub/src/fetcher/person.rs b/crates/apub/src/fetcher/person.rs index 237230905..3acedb086 100644 --- a/crates/apub/src/fetcher/person.rs +++ b/crates/apub/src/fetcher/person.rs @@ -45,14 +45,7 @@ pub async fn get_or_fetch_and_upsert_person( return Ok(u); } - let person = Person::from_apub( - &person?, - context, - apub_id.to_owned(), - recursion_counter, - false, - ) - .await?; + let person = Person::from_apub(&person?, context, apub_id, recursion_counter).await?; let person_id = person.id; blocking(context.pool(), move |conn| { @@ -68,14 +61,7 @@ pub async fn get_or_fetch_and_upsert_person( let person = fetch_remote_object::(context.client(), apub_id, recursion_counter).await?; - let person = Person::from_apub( - &person, - context, - apub_id.to_owned(), - recursion_counter, - false, - ) - .await?; + let person = Person::from_apub(&person, context, apub_id, recursion_counter).await?; Ok(person) } diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index a82e026fc..2c9bebb02 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -6,12 +6,10 @@ use crate::{ is_deleted, }, find_object_by_id, - objects::{comment::Note, person::Person as ApubPerson, post::Page, FromApub}, - GroupExt, + objects::{comment::Note, community::Group, person::Person as ApubPerson, post::Page, FromApub}, Object, }; -use activitystreams::base::BaseExt; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use lemmy_api_common::{blocking, site::SearchResponse}; use lemmy_db_queries::{ source::{ @@ -42,7 +40,7 @@ use url::Url; #[serde(untagged)] enum SearchAcceptedObjects { Person(Box), - Group(Box), + Group(Box), Page(Box), Comment(Box), } @@ -108,7 +106,6 @@ async fn build_response( recursion_counter: &mut i32, context: &LemmyContext, ) -> Result { - let domain = query_url.domain().context("url has no domain")?; let mut response = SearchResponse { type_: SearchType::All.to_string(), comments: vec![], @@ -130,8 +127,7 @@ async fn build_response( ]; } SearchAcceptedObjects::Group(g) => { - let community_uri = g.inner.id(domain)?.context("group has no id")?; - + let community_uri = g.id(&query_url)?; let community = get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?; @@ -143,13 +139,13 @@ async fn build_response( ]; } SearchAcceptedObjects::Page(p) => { - let p = Post::from_apub(&p, context, query_url, recursion_counter, false).await?; + let p = Post::from_apub(&p, context, &query_url, recursion_counter).await?; response.posts = vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??]; } SearchAcceptedObjects::Comment(c) => { - let c = Comment::from_apub(&c, context, query_url, recursion_counter, false).await?; + let c = Comment::from_apub(&c, context, &query_url, recursion_counter).await?; response.comments = vec![ blocking(context.pool(), move |conn| { diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index ea62670cd..950f11dc4 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -9,19 +9,8 @@ pub mod http; pub mod migrations; pub mod objects; -use crate::{ - extensions::{ - group_extension::GroupExtension, - signatures::{PublicKey, PublicKeyExtension}, - }, - fetcher::community::get_or_fetch_and_upsert_community, -}; -use activitystreams::{ - actor, - base::AnyBase, - object::{ApObject, AsObject, ObjectExt}, -}; -use activitystreams_ext::Ext2; +use crate::extensions::signatures::PublicKey; +use activitystreams::base::AnyBase; use anyhow::{anyhow, Context}; use diesel::NotFound; use lemmy_api_common::blocking; @@ -45,11 +34,6 @@ use serde::Serialize; use std::net::IpAddr; use url::{ParseError, Url}; -/// Activitystreams type for community -pub type GroupExt = - Ext2>, GroupExtension, PublicKeyExtension>; -pub type SiteExt = actor::ApActor>; - pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; /// Checks if the ID is allowed for sending or receiving. @@ -60,7 +44,10 @@ pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; /// - URL being in the allowlist (if it is active) /// - URL not being in the blocklist (if it is active) /// -pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Result<(), LemmyError> { +pub(crate) fn check_is_apub_id_valid( + apub_id: &Url, + use_strict_allowlist: bool, +) -> Result<(), LemmyError> { let settings = Settings::get(); let domain = apub_id.domain().context(location_info!())?.to_string(); let local_instance = settings.get_hostname_without_port()?; @@ -165,11 +152,6 @@ pub trait ActorType { public_key_pem: self.public_key().context(location_info!())?, }) } - - // TODO: can delete this - fn get_public_key_ext(&self) -> Result { - Ok(self.get_public_key()?.to_ext()) - } } #[async_trait::async_trait(?Send)] @@ -227,7 +209,7 @@ pub enum EndpointType { } /// Generates an apub endpoint for a given domain, IE xyz.tld -pub fn generate_apub_endpoint_for_domain( +pub(crate) fn generate_apub_endpoint_for_domain( endpoint_type: EndpointType, name: &str, domain: &str, @@ -304,7 +286,7 @@ pub fn build_actor_id_from_shortname( /// Store a sent or received activity in the database, for logging purposes. These records are not /// persistent. -pub async fn insert_activity( +pub(crate) async fn insert_activity( ap_id: &Url, activity: T, local: bool, @@ -340,7 +322,7 @@ impl PostOrComment { /// Tries to find a post or comment in the local database, without any network requests. /// This is used to handle deletions and removals, because in case we dont have the object, we can /// simply ignore the activity. -pub async fn find_post_or_comment_by_id( +pub(crate) async fn find_post_or_comment_by_id( context: &LemmyContext, apub_id: Url, ) -> Result { @@ -374,7 +356,10 @@ pub enum Object { PrivateMessage(Box), } -pub async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result { +pub(crate) async fn find_object_by_id( + context: &LemmyContext, + apub_id: Url, +) -> Result { let ap_id = apub_id.clone(); if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await { return Ok(match pc { @@ -412,7 +397,7 @@ pub async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result(activity: &T) -> Vec -where - T: AsObject, -{ - let mut to_and_cc = vec![]; - if let Some(to) = activity.to() { - let to = to.to_owned().unwrap_to_vec(); - let mut to = to - .iter() - .map(|t| t.as_xsd_any_uri()) - .flatten() - .map(|t| t.to_owned()) - .collect(); - to_and_cc.append(&mut to); - } - if let Some(cc) = activity.cc() { - let cc = cc.to_owned().unwrap_to_vec(); - let mut cc = cc - .iter() - .map(|c| c.as_xsd_any_uri()) - .flatten() - .map(|c| c.to_owned()) - .collect(); - to_and_cc.append(&mut cc); - } - to_and_cc -} - -pub async fn get_community_from_to_or_cc( - activity: &T, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result -where - T: AsObject, -{ - for cid in get_activity_to_and_cc(activity) { - let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await; - if community.is_ok() { - return community; - } - } - Err(NotFound.into()) -} diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index e9041b196..1bebb39e9 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -52,7 +52,7 @@ pub struct Note { #[serde(rename = "@context")] context: OneOrMany, r#type: NoteType, - pub(crate) id: Url, + id: Url, pub(crate) attributed_to: Url, /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain /// the community ID, as it would be incompatible with Pleroma (and we can get the community from @@ -69,6 +69,14 @@ pub struct Note { } impl Note { + pub(crate) fn id_unchecked(&self) -> &Url { + &self.id + } + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + async fn get_parents( &self, context: &LemmyContext, @@ -214,10 +222,10 @@ impl FromApub for Comment { async fn from_apub( note: &Note, context: &LemmyContext, - _expected_domain: Url, + expected_domain: &Url, request_counter: &mut i32, - _mod_action_allowed: bool, ) -> Result { + let ap_id = Some(note.id(expected_domain)?.clone().into()); let creator = get_or_fetch_and_upsert_person(¬e.attributed_to, context, request_counter).await?; let (post, parent_comment_id) = note.get_parents(context, request_counter).await?; @@ -235,7 +243,7 @@ impl FromApub for Comment { published: Some(note.published.naive_local()), updated: note.updated.map(|u| u.to_owned().naive_local()), deleted: None, - ap_id: Some(note.id.clone().into()), + ap_id, local: Some(false), }; Ok(blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??) diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index f9948588d..5269d7663 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,101 +1,157 @@ use crate::{ - extensions::{context::lemmy_context, group_extension::GroupExtension}, + extensions::{context::lemmy_context, signatures::PublicKey}, fetcher::community::fetch_community_mods, generate_moderators_url, - objects::{ - check_object_domain, - create_tombstone, - get_object_from_apub, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, + objects::{create_tombstone, FromApub, ImageObject, Source, ToApub}, ActorType, - GroupExt, }; use activitystreams::{ - actor::{kind::GroupType, ApActor, Endpoints, Group}, - base::BaseExt, - object::{ApObject, Image, Tombstone}, - prelude::*, + actor::{kind::GroupType, Endpoints}, + base::AnyBase, + object::{kind::ImageType, Tombstone}, + primitives::OneOrMany, + unparsed::Unparsed, }; -use activitystreams_ext::Ext2; -use anyhow::Context; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::DbPool; +use lemmy_apub_lib::{ + values::{MediaTypeHtml, MediaTypeMarkdown}, + verify_domains_match, +}; +use lemmy_db_queries::{ApubObject, DbPool}; use lemmy_db_schema::{ naive_now, source::community::{Community, CommunityForm}, }; -use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::{ - location_info, - utils::{check_slurs, check_slurs_opt, convert_datetime}, + utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html}, LemmyError, }; use lemmy_websocket::LemmyContext; +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 Group { + #[serde(rename = "@context")] + context: OneOrMany, + #[serde(rename = "type")] + kind: GroupType, + id: Url, + /// username, set at account creation and can never be changed + preferred_username: String, + /// title (can be changed at any time) + name: String, + content: Option, + media_type: Option, + source: Option, + icon: Option, + /// banner + image: Option, + // lemmy extension + sensitive: Option, + // lemmy extension + pub(crate) moderators: Option, + inbox: Url, + pub(crate) outbox: Url, + followers: Url, + endpoints: Endpoints, + public_key: PublicKey, + published: DateTime, + updated: Option>, + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Group { + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + pub(crate) async fn from_apub_to_form( + group: &Group, + expected_domain: &Url, + ) -> Result { + let actor_id = Some(group.id(expected_domain)?.clone().into()); + let name = group.preferred_username.clone(); + let title = group.name.clone(); + let description = group.source.clone().map(|s| s.content); + let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into()); + + check_slurs(&name)?; + check_slurs(&title)?; + check_slurs_opt(&description)?; + + Ok(CommunityForm { + name, + title, + description, + removed: None, + published: Some(group.published.naive_local()), + updated: group.updated.map(|u| u.naive_local()), + deleted: None, + nsfw: Some(group.sensitive.unwrap_or(false)), + actor_id, + local: Some(false), + private_key: None, + public_key: Some(group.public_key.public_key_pem.clone()), + last_refreshed_at: Some(naive_now()), + icon: Some(group.icon.clone().map(|i| i.url.into())), + banner: Some(group.image.clone().map(|i| i.url.into())), + followers_url: Some(group.followers.clone().into()), + inbox_url: Some(group.inbox.clone().into()), + shared_inbox_url: Some(shared_inbox), + }) + } +} + #[async_trait::async_trait(?Send)] impl ToApub for Community { - type ApubType = GroupExt; + type ApubType = Group; - async fn to_apub(&self, pool: &DbPool) -> Result { - let id = self.id; - let moderators = blocking(pool, move |conn| { - CommunityModeratorView::for_community(conn, id) - }) - .await??; - let moderators: Vec = moderators - .into_iter() - .map(|m| m.moderator.actor_id.into_inner()) - .collect(); + async fn to_apub(&self, _pool: &DbPool) -> Result { + let source = self.description.clone().map(|bio| Source { + content: bio, + media_type: MediaTypeMarkdown::Markdown, + }); + let icon = self.icon.clone().map(|url| ImageObject { + kind: ImageType::Image, + url: url.into(), + }); + let image = self.banner.clone().map(|url| ImageObject { + kind: ImageType::Image, + url: url.into(), + }); - let mut group = ApObject::new(Group::new()); - group - .set_many_contexts(lemmy_context()) - .set_id(self.actor_id.to_owned().into()) - .set_name(self.title.to_owned()) - .set_published(convert_datetime(self.published)) - // NOTE: included attritubed_to field for compatibility with lemmy v0.9.9 - .set_many_attributed_tos(moderators); - - if let Some(u) = self.updated.to_owned() { - group.set_updated(convert_datetime(u)); - } - if let Some(d) = self.description.to_owned() { - set_content_and_source(&mut group, &d)?; - } - - if let Some(icon_url) = &self.icon { - let mut image = Image::new(); - image.set_url::(icon_url.to_owned().into()); - group.set_icon(image.into_any_base()?); - } - - if let Some(banner_url) = &self.banner { - let mut image = Image::new(); - image.set_url::(banner_url.to_owned().into()); - group.set_image(image.into_any_base()?); - } - - let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group); - ap_actor - .set_preferred_username(self.name.to_owned()) - .set_outbox(self.get_outbox_url()?) - .set_followers(self.followers_url.clone().into()) - .set_endpoints(Endpoints { - shared_inbox: Some(self.get_shared_inbox_or_inbox_url()), + let group = Group { + context: lemmy_context(), + kind: GroupType::Group, + id: self.actor_id(), + preferred_username: self.name.clone(), + name: self.title.clone(), + content: self.description.as_ref().map(|b| markdown_to_html(b)), + media_type: self.description.as_ref().map(|_| MediaTypeHtml::Html), + source, + icon, + image, + sensitive: Some(self.nsfw), + moderators: Some(generate_moderators_url(&self.actor_id)?.into()), + inbox: self.inbox_url.clone().into(), + outbox: self.get_outbox_url()?, + followers: self.followers_url.clone().into(), + endpoints: Endpoints { + shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()), ..Default::default() - }); - - Ok(Ext2::new( - ap_actor, - GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?, - self.get_public_key_ext()?, - )) + }, + public_key: self.get_public_key()?, + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), + }; + Ok(group) } fn to_tombstone(&self) -> Result { @@ -110,116 +166,19 @@ impl ToApub for Community { #[async_trait::async_trait(?Send)] impl FromApub for Community { - type ApubType = GroupExt; + type ApubType = Group; /// Converts a `Group` to `Community`, inserts it into the database and updates moderators. async fn from_apub( - group: &GroupExt, + group: &Group, context: &LemmyContext, - expected_domain: Url, + expected_domain: &Url, request_counter: &mut i32, - mod_action_allowed: bool, ) -> Result { - get_object_from_apub( - group, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) - .await - } -} - -#[async_trait::async_trait(?Send)] -impl FromApubToForm for CommunityForm { - async fn from_apub( - group: &GroupExt, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - _mod_action_allowed: bool, - ) -> Result { fetch_community_mods(context, group, request_counter).await?; + let form = Group::from_apub_to_form(group, expected_domain).await?; - let name = group - .inner - .preferred_username() - .context(location_info!())? - .to_string(); - let title = group - .inner - .name() - .context(location_info!())? - .as_one() - .context(location_info!())? - .as_xsd_string() - .context(location_info!())? - .to_string(); - - let description = get_source_markdown_value(group)?; - - check_slurs(&name)?; - check_slurs(&title)?; - check_slurs_opt(&description)?; - - let icon = match group.icon() { - Some(any_image) => Some( - Image::from_any_base(any_image.as_one().context(location_info!())?.clone()) - .context(location_info!())? - .context(location_info!())? - .url() - .context(location_info!())? - .as_single_xsd_any_uri() - .map(|u| u.to_owned().into()), - ), - None => None, - }; - let banner = match group.image() { - Some(any_image) => Some( - Image::from_any_base(any_image.as_one().context(location_info!())?.clone()) - .context(location_info!())? - .context(location_info!())? - .url() - .context(location_info!())? - .as_single_xsd_any_uri() - .map(|u| u.to_owned().into()), - ), - None => None, - }; - let shared_inbox = group - .inner - .endpoints()? - .map(|e| e.shared_inbox) - .flatten() - .map(|s| s.to_owned().into()); - - Ok(CommunityForm { - name, - title, - description, - removed: None, - published: group.inner.published().map(|u| u.to_owned().naive_local()), - updated: group.inner.updated().map(|u| u.to_owned().naive_local()), - deleted: None, - nsfw: Some(group.ext_one.sensitive.unwrap_or(false)), - actor_id: Some(check_object_domain(group, expected_domain, true)?), - local: Some(false), - private_key: None, - public_key: Some(group.ext_two.to_owned().public_key.public_key_pem), - last_refreshed_at: Some(naive_now()), - icon, - banner, - followers_url: Some( - group - .inner - .followers()? - .context(location_info!())? - .to_owned() - .into(), - ), - inbox_url: Some(group.inner.inbox()?.to_owned().into()), - shared_inbox_url: Some(shared_inbox), - }) + let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??; + Ok(community) } } diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 2a31aac84..2114a1d89 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,22 +1,13 @@ -use crate::{check_is_apub_id_valid, fetcher::person::get_or_fetch_and_upsert_person}; +use crate::fetcher::person::get_or_fetch_and_upsert_person; use activitystreams::{ - base::{AsBase, BaseExt, ExtendsExt}, - markers::Base, - mime::{FromStrError, Mime}, - object::{kind::ImageType, ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt}, + base::BaseExt, + object::{kind::ImageType, Tombstone, TombstoneExt}, }; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use chrono::NaiveDateTime; -use lemmy_api_common::blocking; use lemmy_apub_lib::values::MediaTypeMarkdown; -use lemmy_db_queries::{ApubObject, Crud, DbPool}; -use lemmy_db_schema::DbUrl; -use lemmy_utils::{ - location_info, - settings::structs::Settings, - utils::{convert_datetime, markdown_to_html}, - LemmyError, -}; +use lemmy_db_queries::DbPool; +use lemmy_utils::{utils::convert_datetime, LemmyError}; use lemmy_websocket::LemmyContext; use url::Url; @@ -46,22 +37,8 @@ pub trait FromApub { async fn from_apub( apub: &Self::ApubType, context: &LemmyContext, - expected_domain: Url, + expected_domain: &Url, request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result - where - Self: Sized; -} - -#[async_trait::async_trait(?Send)] -pub trait FromApubToForm { - async fn from_apub( - apub: &ApubType, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, ) -> Result where Self: Sized; @@ -77,7 +54,8 @@ pub struct Source { #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct ImageObject { - content: ImageType, + #[serde(rename = "type")] + kind: ImageType, url: Url, } @@ -105,120 +83,3 @@ where Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into()) } } - -pub(in crate::objects) fn check_object_domain( - apub: &T, - expected_domain: Url, - use_strict_allowlist: bool, -) -> Result -where - T: Base + AsBase, -{ - let domain = expected_domain.domain().context(location_info!())?; - let object_id = apub.id(domain)?.context(location_info!())?; - check_is_apub_id_valid(object_id, use_strict_allowlist)?; - Ok(object_id.to_owned().into()) -} - -pub(in crate::objects) fn set_content_and_source( - object: &mut T, - markdown_text: &str, -) -> Result<(), LemmyError> -where - T: ApObjectExt + ObjectExt + AsBase, -{ - let mut source = Object::<()>::new_none_type(); - source - .set_content(markdown_text) - .set_media_type(mime_markdown()?); - object.set_source(source.into_any_base()?); - - object.set_content(markdown_to_html(markdown_text)); - object.set_media_type(mime_html()?); - Ok(()) -} - -pub(in crate::objects) fn get_source_markdown_value( - object: &T, -) -> Result, LemmyError> -where - T: ApObjectExt + ObjectExt + AsBase, -{ - let content = object - .content() - .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string())) - .flatten(); - if content.is_some() { - let source = object.source().context(location_info!())?; - let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?; - check_is_markdown(source.media_type())?; - let source_content = source - .content() - .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string())) - .flatten() - .context(location_info!())?; - return Ok(Some(source_content)); - } - Ok(None) -} - -fn mime_markdown() -> Result { - "text/markdown".parse() -} - -fn mime_html() -> Result { - "text/html".parse() -} - -pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), LemmyError> { - let mime = mime.context(location_info!())?; - if !mime.eq(&mime_markdown()?) { - Err(LemmyError::from(anyhow!( - "Lemmy only supports markdown content" - ))) - } else { - Ok(()) - } -} - -/// Converts an ActivityPub object (eg `Note`) to a database object (eg `Comment`). If an object -/// with the same ActivityPub ID already exists in the database, it is returned directly. Otherwise -/// the apub object is parsed, inserted and returned. -pub async fn get_object_from_apub( - from: &From, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - is_mod_action: bool, -) -> Result -where - From: BaseExt, - To: ApubObject + Crud + Send + 'static, - ToForm: FromApubToForm + Send + 'static, -{ - let object_id = from.id_unchecked().context(location_info!())?.to_owned(); - let domain = object_id.domain().context(location_info!())?; - - // if its a local object, return it directly from the database - if Settings::get().hostname == domain { - let object = blocking(context.pool(), move |conn| { - To::read_from_apub_id(conn, &object_id.into()) - }) - .await??; - Ok(object) - } - // otherwise parse and insert, assuring that it comes from the right domain - else { - let to_form = ToForm::from_apub( - from, - context, - expected_domain, - request_counter, - is_mod_action, - ) - .await?; - - let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??; - Ok(to) - } -} diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 07b578a8d..ec14a943e 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -43,6 +43,7 @@ pub enum UserTypes { pub struct Person { #[serde(rename = "@context")] context: OneOrMany, + #[serde(rename = "type")] kind: UserTypes, id: Url, /// username, set at account creation and can never be changed @@ -50,7 +51,7 @@ pub struct Person { /// displayname (can be changed at any time) name: Option, content: Option, - media_type: MediaTypeHtml, + media_type: Option, source: Option, /// user avatar icon: Option, @@ -91,11 +92,11 @@ impl ToApub for DbPerson { media_type: MediaTypeMarkdown::Markdown, }); let icon = self.avatar.clone().map(|url| ImageObject { - content: ImageType::Image, + kind: ImageType::Image, url: url.into(), }); let image = self.banner.clone().map(|url| ImageObject { - content: ImageType::Image, + kind: ImageType::Image, url: url.into(), }); @@ -106,7 +107,7 @@ impl ToApub for DbPerson { preferred_username: self.name.clone(), name: self.display_name.clone(), content: self.bio.as_ref().map(|b| markdown_to_html(b)), - media_type: MediaTypeHtml::Html, + media_type: self.bio.as_ref().map(|_| MediaTypeHtml::Html), source, icon, image, @@ -114,7 +115,7 @@ impl ToApub for DbPerson { published: convert_datetime(self.published), outbox: self.get_outbox_url()?, endpoints: Endpoints { - shared_inbox: Some(self.get_shared_inbox_or_inbox_url()), + shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()), ..Default::default() }, public_key: self.get_public_key()?, @@ -136,10 +137,10 @@ impl FromApub for DbPerson { async fn from_apub( person: &Person, context: &LemmyContext, - _expected_domain: Url, + expected_domain: &Url, _request_counter: &mut i32, - _mod_action_allowed: bool, ) -> Result { + let actor_id = Some(person.id(expected_domain)?.clone().into()); let name = person.preferred_username.clone(); let display_name: Option = person.name.clone(); let bio = person.source.clone().map(|s| s.content); @@ -163,7 +164,7 @@ impl FromApub for DbPerson { banner: Some(person.image.clone().map(|i| i.url.into())), published: Some(person.published.naive_local()), updated: person.updated.map(|u| u.clone().naive_local()), - actor_id: Some(person.id.clone().into()), + actor_id, bio: Some(bio), local: Some(false), admin: Some(false), diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 78cdfb8a1..43a019d16 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -47,7 +47,7 @@ pub struct Page { #[serde(rename = "@context")] context: OneOrMany, r#type: PageType, - pub(crate) id: Url, + id: Url, pub(crate) attributed_to: Url, to: [Url; 2], name: String, @@ -66,6 +66,14 @@ pub struct Page { } impl Page { + pub(crate) fn id_unchecked(&self) -> &Url { + &self.id + } + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + /// Only mods can change the post's stickied/locked status. So if either of these is changed from /// the current value, it is a mod action and needs to be verified as such. /// @@ -121,7 +129,7 @@ impl ToApub for Post { media_type: MediaTypeMarkdown::Markdown, }); let image = self.thumbnail_url.clone().map(|thumb| ImageObject { - content: ImageType::Image, + kind: ImageType::Image, url: thumb.into(), }); @@ -164,10 +172,17 @@ impl FromApub for Post { async fn from_apub( page: &Page, context: &LemmyContext, - _expected_domain: Url, + expected_domain: &Url, request_counter: &mut i32, - _mod_action_allowed: bool, ) -> Result { + // We can't verify the domain in case of mod action, because the mod may be on a different + // instance from the post author. + let ap_id = if page.is_mod_action(context.pool()).await? { + page.id_unchecked() + } else { + page.id(expected_domain)? + }; + let ap_id = Some(ap_id.clone().into()); let creator = get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?; let community = extract_community(&page.to, context, request_counter).await?; @@ -200,7 +215,7 @@ impl FromApub for Post { embed_description, embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: Some(page.id.clone().into()), + ap_id, local: Some(false), }; Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??) diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 2cbb6b9f4..02cf12eba 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -34,7 +34,7 @@ pub struct Note { #[serde(rename = "@context")] context: OneOrMany, r#type: NoteType, - pub(crate) id: Url, + id: Url, pub(crate) attributed_to: Url, to: Url, content: String, @@ -47,6 +47,14 @@ pub struct Note { } impl Note { + pub(crate) fn id_unchecked(&self) -> &Url { + &self.id + } + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + pub(crate) async fn verify( &self, context: &LemmyContext, @@ -109,10 +117,10 @@ impl FromApub for PrivateMessage { async fn from_apub( note: &Note, context: &LemmyContext, - _expected_domain: Url, + expected_domain: &Url, request_counter: &mut i32, - _mod_action_allowed: bool, ) -> Result { + let ap_id = Some(note.id(expected_domain)?.clone().into()); let creator = get_or_fetch_and_upsert_person(¬e.attributed_to, context, request_counter).await?; let recipient = get_or_fetch_and_upsert_person(¬e.to, context, request_counter).await?; @@ -125,7 +133,7 @@ impl FromApub for PrivateMessage { updated: note.updated.map(|u| u.to_owned().naive_local()), deleted: None, read: None, - ap_id: Some(note.id.clone().into()), + ap_id, local: Some(false), }; Ok(