mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-26 14:21:19 +00:00
Rewrite community apub
This commit is contained in:
parent
bec54d07f5
commit
83e1bf311e
21 changed files with 246 additions and 601 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1696,7 +1696,6 @@ name = "lemmy_apub"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"activitystreams",
|
||||
"activitystreams-ext",
|
||||
"actix",
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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<bool>,
|
||||
pub moderators: Option<Url>,
|
||||
}
|
||||
|
||||
impl GroupExtension {
|
||||
pub fn new(sensitive: bool, moderators_url: Url) -> Result<GroupExtension, LemmyError> {
|
||||
Ok(GroupExtension {
|
||||
sensitive: Some(sensitive),
|
||||
moderators: Some(moderators_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<U> UnparsedExtension<U> for GroupExtension
|
||||
where
|
||||
U: UnparsedMutExt,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -1,3 +1,2 @@
|
|||
pub mod context;
|
||||
pub(crate) mod group_extension;
|
||||
pub mod signatures;
|
||||
|
|
|
@ -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<U> UnparsedExtension<U> for PublicKeyExtension
|
||||
where
|
||||
U: UnparsedMutExt,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Community>,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<Community, LemmyError> {
|
||||
let group = fetch_remote_object::<GroupExt>(context.client(), apub_id, request_counter).await;
|
||||
let group = fetch_remote_object::<Group>(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<Vec<Url>, LemmyError> {
|
||||
if let Some(mods_url) = &group.ext_one.moderators {
|
||||
if let Some(mods_url) = &group.moderators {
|
||||
let mods =
|
||||
fetch_remote_object::<OrderedCollection>(context.client(), mods_url, recursion_counter)
|
||||
.await?;
|
||||
|
|
|
@ -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::<Page>(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::<Note>(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??;
|
||||
|
|
|
@ -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::<ApubPerson>(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)
|
||||
}
|
||||
|
|
|
@ -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<ApubPerson>),
|
||||
Group(Box<GroupExt>),
|
||||
Group(Box<Group>),
|
||||
Page(Box<Page>),
|
||||
Comment(Box<Note>),
|
||||
}
|
||||
|
@ -108,7 +106,6 @@ async fn build_response(
|
|||
recursion_counter: &mut i32,
|
||||
context: &LemmyContext,
|
||||
) -> Result<SearchResponse, LemmyError> {
|
||||
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| {
|
||||
|
|
|
@ -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<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
|
||||
pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
|
||||
|
||||
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<PublicKeyExtension, LemmyError> {
|
||||
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<T>(
|
||||
pub(crate) async fn insert_activity<T>(
|
||||
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<PostOrComment, LemmyError> {
|
||||
|
@ -374,7 +356,10 @@ pub enum Object {
|
|||
PrivateMessage(Box<PrivateMessage>),
|
||||
}
|
||||
|
||||
pub async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result<Object, LemmyError> {
|
||||
pub(crate) async fn find_object_by_id(
|
||||
context: &LemmyContext,
|
||||
apub_id: Url,
|
||||
) -> Result<Object, LemmyError> {
|
||||
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<O
|
|||
Err(NotFound.into())
|
||||
}
|
||||
|
||||
pub async fn check_community_or_site_ban(
|
||||
pub(crate) async fn check_community_or_site_ban(
|
||||
person: &Person,
|
||||
community_id: CommunityId,
|
||||
pool: &DbPool,
|
||||
|
@ -429,48 +414,3 @@ pub async fn check_community_or_site_ban(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Vec<Url>
|
||||
where
|
||||
T: AsObject<Kind>,
|
||||
{
|
||||
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<T, Kind>(
|
||||
activity: &T,
|
||||
context: &LemmyContext,
|
||||
request_counter: &mut i32,
|
||||
) -> Result<Community, LemmyError>
|
||||
where
|
||||
T: AsObject<Kind>,
|
||||
{
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ pub struct Note {
|
|||
#[serde(rename = "@context")]
|
||||
context: OneOrMany<AnyBase>,
|
||||
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<Comment, LemmyError> {
|
||||
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??)
|
||||
|
|
|
@ -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<AnyBase>,
|
||||
#[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<String>,
|
||||
media_type: Option<MediaTypeHtml>,
|
||||
source: Option<Source>,
|
||||
icon: Option<ImageObject>,
|
||||
/// banner
|
||||
image: Option<ImageObject>,
|
||||
// lemmy extension
|
||||
sensitive: Option<bool>,
|
||||
// lemmy extension
|
||||
pub(crate) moderators: Option<Url>,
|
||||
inbox: Url,
|
||||
pub(crate) outbox: Url,
|
||||
followers: Url,
|
||||
endpoints: Endpoints<Url>,
|
||||
public_key: PublicKey,
|
||||
published: DateTime<FixedOffset>,
|
||||
updated: Option<DateTime<FixedOffset>>,
|
||||
#[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<CommunityForm, LemmyError> {
|
||||
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<GroupExt, LemmyError> {
|
||||
let id = self.id;
|
||||
let moderators = blocking(pool, move |conn| {
|
||||
CommunityModeratorView::for_community(conn, id)
|
||||
})
|
||||
.await??;
|
||||
let moderators: Vec<Url> = moderators
|
||||
.into_iter()
|
||||
.map(|m| m.moderator.actor_id.into_inner())
|
||||
.collect();
|
||||
|
||||
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::<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::<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()),
|
||||
..Default::default()
|
||||
async fn to_apub(&self, _pool: &DbPool) -> Result<Group, LemmyError> {
|
||||
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(),
|
||||
});
|
||||
|
||||
Ok(Ext2::new(
|
||||
ap_actor,
|
||||
GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
|
||||
self.get_public_key_ext()?,
|
||||
))
|
||||
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()
|
||||
},
|
||||
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<Tombstone, LemmyError> {
|
||||
|
@ -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<Community, LemmyError> {
|
||||
get_object_from_apub(
|
||||
group,
|
||||
context,
|
||||
expected_domain,
|
||||
request_counter,
|
||||
mod_action_allowed,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl FromApubToForm<GroupExt> for CommunityForm {
|
||||
async fn from_apub(
|
||||
group: &GroupExt,
|
||||
context: &LemmyContext,
|
||||
expected_domain: Url,
|
||||
request_counter: &mut i32,
|
||||
_mod_action_allowed: bool,
|
||||
) -> Result<Self, LemmyError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Self, LemmyError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait FromApubToForm<ApubType> {
|
||||
async fn from_apub(
|
||||
apub: &ApubType,
|
||||
context: &LemmyContext,
|
||||
expected_domain: Url,
|
||||
request_counter: &mut i32,
|
||||
mod_action_allowed: bool,
|
||||
) -> Result<Self, LemmyError>
|
||||
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<T, Kind>(
|
||||
apub: &T,
|
||||
expected_domain: Url,
|
||||
use_strict_allowlist: bool,
|
||||
) -> 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, use_strict_allowlist)?;
|
||||
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().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<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 async fn get_object_from_apub<From, Kind, To, ToForm, IdType>(
|
||||
from: &From,
|
||||
context: &LemmyContext,
|
||||
expected_domain: Url,
|
||||
request_counter: &mut i32,
|
||||
is_mod_action: bool,
|
||||
) -> Result<To, LemmyError>
|
||||
where
|
||||
From: BaseExt<Kind>,
|
||||
To: ApubObject<ToForm> + Crud<ToForm, IdType> + 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,
|
||||
is_mod_action,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??;
|
||||
Ok(to)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ pub enum UserTypes {
|
|||
pub struct Person {
|
||||
#[serde(rename = "@context")]
|
||||
context: OneOrMany<AnyBase>,
|
||||
#[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<String>,
|
||||
content: Option<String>,
|
||||
media_type: MediaTypeHtml,
|
||||
media_type: Option<MediaTypeHtml>,
|
||||
source: Option<Source>,
|
||||
/// user avatar
|
||||
icon: Option<ImageObject>,
|
||||
|
@ -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<DbPerson, LemmyError> {
|
||||
let actor_id = Some(person.id(expected_domain)?.clone().into());
|
||||
let name = person.preferred_username.clone();
|
||||
let display_name: Option<String> = 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),
|
||||
|
|
|
@ -47,7 +47,7 @@ pub struct Page {
|
|||
#[serde(rename = "@context")]
|
||||
context: OneOrMany<AnyBase>,
|
||||
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<Post, LemmyError> {
|
||||
// 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??)
|
||||
|
|
|
@ -34,7 +34,7 @@ pub struct Note {
|
|||
#[serde(rename = "@context")]
|
||||
context: OneOrMany<AnyBase>,
|
||||
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<PrivateMessage, LemmyError> {
|
||||
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(
|
||||
|
|
Loading…
Reference in a new issue