lemmy/crates/apub/src/objects/mod.rs
Andrew Yoon e78ba38e94
Use URL type in most outstanding struct fields (#1468)
* Use URL type in most outstanding struct fields

This fixes all known remaining cases where url fields are stored as
plain strings, with the exception of form fields where empty strings
are used as sentinels (see `diesel_option_overwrite_to_url`).

Tested for regressions in the federated docker setup attempting to
exercise all changed fields, including through apub federation.

Fixes #1385

* Add migration to fix blank-string post.url values to be null

This also then fixes #602

* Address review feedback

- Fixed some unwraps and err message formatting
- Bumped the `url` library to 2.2.1 to fix a bug with serde error
  messages
- Add unit tests for the two diesel option override functions
- Fix migration teardown by adding a no-op

* Rename lemmy_db_queries::Url to lemmy_db_queries::DbUrl

* fix compile error

* box PostOrComment variants
2021-03-02 12:41:48 +00:00

247 lines
7 KiB
Rust

use crate::{
check_is_apub_id_valid,
fetcher::{community::get_or_fetch_and_upsert_community, user::get_or_fetch_and_upsert_user},
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 url::Url;
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod post;
pub(crate) mod private_message;
pub(crate) mod user;
/// 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<Self::ApubType, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
}
#[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<Self, LemmyError>
where
Self: Sized;
}
#[async_trait::async_trait(?Send)]
pub(in crate::objects) trait FromApubToForm<ApubType> {
async fn from_apub(
apub: &ApubType,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError>
where
Self: Sized;
}
/// Updated is actually the deletion time
fn create_tombstone<T>(
deleted: bool,
object_id: Url,
updated: Option<NaiveDateTime>,
former_type: T,
) -> Result<Tombstone, LemmyError>
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())
}
} else {
Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
}
}
pub(in crate::objects) fn check_object_domain<T, Kind>(
apub: &T,
expected_domain: Url,
) -> Result<DbUrl, LemmyError>
where
T: Base + AsBase<Kind>,
{
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<T, Kind1, Kind2>(
object: &mut T,
markdown_text: &str,
) -> Result<(), LemmyError>
where
T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
{
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<T, Kind1, Kind2>(
object: &T,
) -> Result<Option<String>, LemmyError>
where
T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
{
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));
}
Ok(None)
}
fn mime_markdown() -> Result<Mime, FromStrError> {
"text/markdown".parse()
}
fn mime_html() -> Result<Mime, FromStrError> {
"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(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm>(
from: &From,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<To, LemmyError>
where
From: BaseExt<Kind>,
To: ApubObject<ToForm> + Crud<ToForm> + Send + 'static,
ToForm: FromApubToForm<From> + 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)
}
}
pub(in crate::objects) async fn check_object_for_community_or_site_ban<T, Kind>(
object: &T,
community_id: i32,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
where
T: ObjectExt<Kind>,
{
let user_id = object
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let user = get_or_fetch_and_upsert_user(user_id, context, request_counter).await?;
check_community_or_site_ban(&user, community_id, context.pool()).await
}
pub(in crate::objects) async fn get_to_community<T, Kind>(
object: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Community, LemmyError>
where
T: ObjectExt<Kind>,
{
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::<Result<Vec<&Url>, 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;
}
}
Err(NotFound.into())
}