X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Fapub%2Fsrc%2Fobjects%2Fmod.rs;h=b3653172ac5242a7cb255408a33baf0abce194e1;hb=92568956353f21649ed9aff68b42699c9d036f30;hp=235d5223c39cc58676c57ea4a8d2338dc15f49c7;hpb=ddf4a667b1ba15222287b30455a202bd3a336547;p=lemmy.git diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 235d5223..b3653172 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,247 +1,116 @@ -use crate::{ - check_is_apub_id_valid, - fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person}, - inbox::community_inbox::check_community_or_site_ban, -}; -use activitystreams::{ - base::{AsBase, BaseExt, ExtendsExt}, - markers::Base, - mime::{FromStrError, Mime}, - object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt}, -}; -use anyhow::{anyhow, Context}; -use chrono::NaiveDateTime; -use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; -use lemmy_db_queries::{ApubObject, Crud, DbPool}; -use lemmy_db_schema::{source::community::Community, DbUrl}; -use lemmy_utils::{ - location_info, - settings::structs::Settings, - utils::{convert_datetime, markdown_to_html}, - LemmyError, -}; -use lemmy_websocket::LemmyContext; +use crate::protocol::Source; +use activitypub_federation::protocol::values::MediaTypeMarkdownOrHtml; +use anyhow::anyhow; +use html2md::parse_html; +use lemmy_utils::{error::LemmyError, settings::structs::Settings}; use url::Url; -pub(crate) mod comment; -pub(crate) mod community; -pub(crate) mod post; -pub(crate) mod private_message; -pub(crate) mod person; - -/// Trait for converting an object or actor into the respective ActivityPub type. -#[async_trait::async_trait(?Send)] -pub(crate) trait ToApub { - type ApubType; - async fn to_apub(&self, pool: &DbPool) -> Result; - fn to_tombstone(&self) -> Result; -} - -#[async_trait::async_trait(?Send)] -pub(crate) trait FromApub { - type ApubType; - /// Converts an object from ActivityPub type to Lemmy internal type. - /// - /// * `apub` The object to read from - /// * `context` LemmyContext which holds DB pool, HTTP client etc - /// * `expected_domain` Domain where the object was received from - async fn from_apub( - apub: &Self::ApubType, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - ) -> Result - where - Self: Sized; -} - -#[async_trait::async_trait(?Send)] -pub(in crate::objects) trait FromApubToForm { - async fn from_apub( - apub: &ApubType, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - ) -> Result - where - Self: Sized; -} - -/// Updated is actually the deletion time -fn create_tombstone( - deleted: bool, - object_id: Url, - updated: Option, - former_type: T, -) -> Result -where - T: ToString, -{ - if deleted { - if let Some(updated) = updated { - let mut tombstone = Tombstone::new(); - tombstone.set_id(object_id); - tombstone.set_former_type(former_type.to_string()); - tombstone.set_deleted(convert_datetime(updated)); - Ok(tombstone) - } else { - Err(anyhow!("Cant convert to tombstone because updated time was None.").into()) - } +pub mod comment; +pub mod community; +pub mod instance; +pub mod person; +pub mod post; +pub mod private_message; + +pub(crate) fn read_from_string_or_source( + content: &str, + media_type: &Option, + source: &Option, +) -> String { + if let Some(s) = source { + // markdown sent by lemmy in source field + s.content.clone() + } else if media_type == &Some(MediaTypeMarkdownOrHtml::Markdown) { + // markdown sent by peertube in content field + content.to_string() } else { - 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, -) -> 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)?; - 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()) - .flatten() - .map(|s| s.to_string()); - 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()) - .flatten() - .context(location_info!())? - .to_string(); - return Ok(Some(source_content)); + // otherwise, convert content html to markdown + parse_html(content) } - Ok(None) } -fn mime_markdown() -> Result { - "text/markdown".parse() +pub(crate) fn read_from_string_or_source_opt( + content: &Option, + media_type: &Option, + source: &Option, +) -> Option { + content + .as_ref() + .map(|content| read_from_string_or_source(content, media_type, source)) } -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" - ))) +/// When for example a Post is made in a remote community, the community will send it back, +/// wrapped in Announce. If we simply receive this like any other federated object, overwrite the +/// existing, local Post. In particular, it will set the field local = false, so that the object +/// can't be fetched from the Activitypub HTTP endpoint anymore (which only serves local objects). +pub(crate) fn verify_is_remote_object(id: &Url, settings: &Settings) -> Result<(), LemmyError> { + let local_domain = settings.get_hostname_without_port()?; + if id.domain() == Some(&local_domain) { + Err(anyhow!("cant accept local object from remote instance").into()) } 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(in crate::objects) async fn get_object_from_apub( - from: &From, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, -) -> 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).await?; - - let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??; - Ok(to) +#[cfg(test)] +pub(crate) mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use activitypub_federation::config::{Data, FederationConfig}; + use anyhow::anyhow; + use lemmy_api_common::{context::LemmyContext, request::build_user_agent}; + use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool_for_tests}; + use lemmy_utils::{ + rate_limit::{RateLimitCell, RateLimitConfig}, + settings::SETTINGS, + }; + use reqwest::{Client, Request, Response}; + use reqwest_middleware::{ClientBuilder, Middleware, Next}; + use task_local_extensions::Extensions; + + struct BlockedMiddleware; + + /// A reqwest middleware which blocks all requests + #[async_trait::async_trait] + impl Middleware for BlockedMiddleware { + async fn handle( + &self, + _req: Request, + _extensions: &mut Extensions, + _next: Next<'_>, + ) -> reqwest_middleware::Result { + Err(anyhow!("Network requests not allowed").into()) + } } -} -pub(in crate::objects) async fn check_object_for_community_or_site_ban( - object: &T, - community_id: i32, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> -where - T: ObjectExt, -{ - let person_id = object - .attributed_to() - .context(location_info!())? - .as_single_xsd_any_uri() - .context(location_info!())?; - let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; - check_community_or_site_ban(&person, community_id, context.pool()).await -} - -pub(in crate::objects) async fn get_to_community( - object: &T, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result -where - T: ObjectExt, -{ - let community_ids = object - .to() - .context(location_info!())? - .as_many() - .context(location_info!())? - .iter() - .map(|a| a.as_xsd_any_uri().context(location_info!())) - .collect::, anyhow::Error>>()?; - for cid in community_ids { - let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await; - if community.is_ok() { - return community; - } + // TODO: would be nice if we didnt have to use a full context for tests. + pub(crate) async fn init_context() -> Data { + // call this to run migrations + let pool = build_db_pool_for_tests().await; + + let settings = SETTINGS.clone(); + let client = Client::builder() + .user_agent(build_user_agent(&settings)) + .build() + .unwrap(); + + let client = ClientBuilder::new(client).with(BlockedMiddleware).build(); + let secret = Secret { + id: 0, + jwt_secret: String::new(), + }; + + let rate_limit_config = RateLimitConfig::builder().build(); + let rate_limit_cell = RateLimitCell::new(rate_limit_config).await; + + let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone()); + let config = FederationConfig::builder() + .domain("example.com") + .app_data(context) + .build() + .await + .unwrap(); + config.to_request_data() } - Err(NotFound.into()) }