diff --git a/plume-common/src/activity_pub/inbox.rs b/plume-common/src/activity_pub/inbox.rs index 2537b1a4..9dba803c 100644 --- a/plume-common/src/activity_pub/inbox.rs +++ b/plume-common/src/activity_pub/inbox.rs @@ -318,72 +318,6 @@ fn get_id(json: serde_json::Value) -> Option { } } -/// A trait for ActivityPub objects that can be retrieved or constructed from ID. -/// -/// The two functions to implement are `from_activity` to create (and save) a new object -/// of this type from its AP representation, and `from_db` to try to find it in the database -/// using its ID. -/// -/// When dealing with the "object" field of incoming activities, `Inbox` will try to see if it is -/// a full object, and if so, save it with `from_activity`. If it is only an ID, it will try to find -/// it in the database with `from_db`, and otherwise dereference (fetch) the full object and parse it -/// with `from_activity`. -pub trait FromId: Sized { - /// The type representing a failure - type Error: From> + Debug; - - /// The ActivityPub object type representing Self - type Object: activitypub::Object; - - /// Tries to get an instance of `Self` from an ActivityPub ID. - /// - /// # Parameters - /// - /// - `ctx`: a context to get this instance (= a database in which to search) - /// - `id`: the ActivityPub ID of the object to find - /// - `object`: optional object that will be used if the object was not found in the database - /// If absent, the ID will be dereferenced. - fn from_id( - ctx: &C, - id: &str, - object: Option, - proxy: Option<&reqwest::Proxy>, - ) -> Result, Self::Error)> { - match Self::from_db(ctx, id) { - Ok(x) => Ok(x), - _ => match object { - Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)), - None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?) - .map_err(|e| (None, e)), - }, - } - } - - /// Dereferences an ID - fn deref( - id: &str, - proxy: Option, - ) -> Result, Self::Error)> { - request::get(id, Self::get_sender(), proxy) - .map_err(|_| (None, InboxError::DerefError)) - .and_then(|mut r| { - let json: serde_json::Value = r - .json() - .map_err(|_| (None, InboxError::InvalidObject(None)))?; - serde_json::from_value(json.clone()) - .map_err(|_| (Some(json), InboxError::InvalidObject(None))) - }) - .map_err(|(json, e)| (json, e.into())) - } - - /// Builds a `Self` from its ActivityPub representation - fn from_activity(ctx: &C, activity: Self::Object) -> Result; - - /// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database) - fn from_db(ctx: &C, id: &str) -> Result; - - fn get_sender() -> &'static dyn Signer; -} /// A trait for ActivityPub objects that can be retrieved or constructed from ID. /// /// The two functions to implement are `from_activity` to create (and save) a new object @@ -808,22 +742,6 @@ mod tests { } struct MyActor; - impl FromId<()> for MyActor { - type Error = (); - type Object = Person; - - fn from_db(_: &(), _id: &str) -> Result { - Ok(MyActor) - } - - fn from_activity(_: &(), _obj: Person) -> Result { - Ok(MyActor) - } - - fn get_sender() -> &'static dyn Signer { - &*MY_SIGNER - } - } impl FromId07<()> for MyActor { type Error = (); type Object = Person07; @@ -852,22 +770,6 @@ mod tests { } struct MyObject; - impl FromId<()> for MyObject { - type Error = (); - type Object = Note; - - fn from_db(_: &(), _id: &str) -> Result { - Ok(MyObject) - } - - fn from_activity(_: &(), _obj: Note) -> Result { - Ok(MyObject) - } - - fn get_sender() -> &'static dyn Signer { - &*MY_SIGNER - } - } impl AsObject for MyObject { type Error = (); type Output = (); @@ -1017,15 +919,6 @@ mod tests { act } - #[test] - fn test_inbox_basic() { - let act = serde_json::to_value(build_create()).unwrap(); - let res: Result<(), ()> = Inbox::handle(&(), act) - .with::(None) - .done(); - assert!(res.is_ok()); - } - #[test] fn test_inbox_basic07() { let act = serde_json::to_value(build_create07()).unwrap(); @@ -1035,18 +928,6 @@ mod tests { assert!(res.is_ok()); } - #[test] - fn test_inbox_multi_handlers() { - let act = serde_json::to_value(build_create()).unwrap(); - let res: Result<(), ()> = Inbox::handle(&(), act) - .with::(None) - .with::(None) - .with::(None) - .with::(None) - .done(); - assert!(res.is_ok()); - } - #[test] fn test_inbox_multi_handlers07() { let act = serde_json::to_value(build_create()).unwrap(); @@ -1059,17 +940,6 @@ mod tests { assert!(res.is_ok()); } - #[test] - fn test_inbox_failure() { - let act = serde_json::to_value(build_create()).unwrap(); - // Create is not handled by this inbox - let res: Result<(), ()> = Inbox::handle(&(), act) - .with::(None) - .with::(None) - .done(); - assert!(res.is_err()); - } - #[test] fn test_inbox_failure07() { let act = serde_json::to_value(build_create07()).unwrap(); @@ -1082,22 +952,6 @@ mod tests { } struct FailingActor; - impl FromId<()> for FailingActor { - type Error = (); - type Object = Person; - - fn from_db(_: &(), _id: &str) -> Result { - Err(()) - } - - fn from_activity(_: &(), _obj: Person) -> Result { - Err(()) - } - - fn get_sender() -> &'static dyn Signer { - &*MY_SIGNER - } - } impl AsActor<&()> for FailingActor { fn get_inbox_url(&self) -> String { String::from("https://test.ap/failing-actor/inbox") @@ -1155,22 +1009,6 @@ mod tests { } } - #[test] - fn test_inbox_actor_failure() { - let act = serde_json::to_value(build_create()).unwrap(); - - let res: Result<(), ()> = Inbox::handle(&(), act.clone()) - .with::(None) - .done(); - assert!(res.is_err()); - - let res: Result<(), ()> = Inbox::handle(&(), act.clone()) - .with::(None) - .with::(None) - .done(); - assert!(res.is_ok()); - } - #[test] fn test_inbox_actor_failure07() { let act = serde_json::to_value(build_create07()).unwrap(); diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs index 4428c6cc..2de2e6ed 100644 --- a/plume-models/src/blogs.rs +++ b/plume-models/src/blogs.rs @@ -27,11 +27,10 @@ use openssl::{ sign::{Signer, Verifier}, }; use plume_common::activity_pub::{ - inbox::{AsActor, FromId, FromId07}, + inbox::{AsActor, FromId07}, sign, ActivityStream, ApSignature, ApSignature07, CustomGroup as CustomGroup07, Id, IntoId, PublicKey, PublicKey07, Source, SourceProperty, ToAsString, ToAsUri, }; -use url::Url; use webfinger::*; pub type CustomGroup = CustomObject; @@ -161,7 +160,7 @@ impl Blog { .find(|l| l.mime_type == Some(String::from("application/activity+json"))) .ok_or(Error::Webfinger) .and_then(|l| { - Blog::from_id( + Blog::from_id07( conn, &l.href.ok_or(Error::MissingApProperty)?, None, @@ -471,110 +470,6 @@ impl IntoId for Blog { } } -impl FromId for Blog { - type Error = Error; - type Object = CustomGroup; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Self::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, acct: CustomGroup) -> Result { - let url = Url::parse(&acct.object.object_props.id_string()?)?; - let inst = url.host_str().ok_or(Error::Url)?; - let instance = Instance::find_by_domain(conn, inst).or_else(|_| { - Instance::insert( - conn, - NewInstance { - public_domain: inst.to_owned(), - name: inst.to_owned(), - local: false, - // We don't really care about all the following for remote instances - long_description: SafeString::new(""), - short_description: SafeString::new(""), - default_license: String::new(), - open_registrations: true, - short_description_html: String::new(), - long_description_html: String::new(), - }, - ) - })?; - let icon_id = acct - .object - .object_props - .icon_image() - .ok() - .and_then(|icon| { - let owner = icon.object_props.attributed_to_link::().ok()?; - Media::save_remote( - conn, - icon.object_props.url_string().ok()?, - &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, - ) - .ok() - }) - .map(|m| m.id); - - let banner_id = acct - .object - .object_props - .image_image() - .ok() - .and_then(|banner| { - let owner = banner.object_props.attributed_to_link::().ok()?; - Media::save_remote( - conn, - banner.object_props.url_string().ok()?, - &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, - ) - .ok() - }) - .map(|m| m.id); - - let name = acct.object.ap_actor_props.preferred_username_string()?; - if name.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { - return Err(Error::InvalidValue); - } - - Blog::insert( - conn, - NewBlog { - actor_id: name.clone(), - title: acct.object.object_props.name_string().unwrap_or(name), - outbox_url: acct.object.ap_actor_props.outbox_string()?, - inbox_url: acct.object.ap_actor_props.inbox_string()?, - summary: acct - .object - .ap_object_props - .source_object::() - .map(|s| s.content) - .unwrap_or_default(), - instance_id: instance.id, - ap_url: acct.object.object_props.id_string()?, - public_key: acct - .custom_props - .public_key_publickey()? - .public_key_pem_string()?, - private_key: None, - banner_id, - icon_id, - summary_html: SafeString::new( - &acct - .object - .object_props - .summary_string() - .unwrap_or_default(), - ), - theme: None, - }, - ) - } - - fn get_sender() -> &'static dyn sign::Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for Blog { type Error = Error; type Object = CustomGroup07; @@ -648,7 +543,7 @@ impl FromId07 for Blog { Media::save_remote( conn, banner.url()?.to_as_uri()?, - &User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?, + &User::from_id07(conn, &owner, None, CONFIG.proxy()).ok()?, ) .ok() }) @@ -1130,33 +1025,6 @@ pub(crate) mod tests { }) } - #[test] - fn self_federation() { - let conn = &db(); - conn.test_transaction::<_, (), _>(|| { - let (_users, blogs) = fill_database(&conn); - - let ap_repr = blogs[0].to_activity(&conn).unwrap(); - blogs[0].delete(&conn).unwrap(); - let blog = Blog::from_activity(&conn, ap_repr).unwrap(); - - assert_eq!(blog.actor_id, blogs[0].actor_id); - assert_eq!(blog.title, blogs[0].title); - assert_eq!(blog.summary, blogs[0].summary); - assert_eq!(blog.outbox_url, blogs[0].outbox_url); - assert_eq!(blog.inbox_url, blogs[0].inbox_url); - assert_eq!(blog.instance_id, blogs[0].instance_id); - assert_eq!(blog.ap_url, blogs[0].ap_url); - assert_eq!(blog.public_key, blogs[0].public_key); - assert_eq!(blog.fqn, blogs[0].fqn); - assert_eq!(blog.summary_html, blogs[0].summary_html); - assert_eq!(blog.icon_url(&conn), blogs[0].icon_url(&conn)); - assert_eq!(blog.banner_url(&conn), blogs[0].banner_url(&conn)); - - Ok(()) - }) - } - #[test] fn self_federation07() { let conn = &db(); diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index b6c47fde..31df688b 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -30,7 +30,7 @@ use chrono::{self, NaiveDateTime, TimeZone, Utc}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use plume_common::{ activity_pub::{ - inbox::{AsActor, AsObject, AsObject07, FromId, FromId07}, + inbox::{AsActor, AsObject, AsObject07, FromId07}, sign::Signer, Id, IntoId, ToAsString, ToAsUri, PUBLIC_VISIBILITY, }, @@ -294,141 +294,6 @@ impl Comment { } } -impl FromId for Comment { - type Error = Error; - type Object = Note; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Self::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, note: Note) -> Result { - let comm = { - let previous_url = note - .object_props - .in_reply_to - .as_ref() - .ok_or(Error::MissingApProperty)? - .as_str() - .ok_or(Error::MissingApProperty)?; - let previous_comment = Comment::find_by_ap_url(conn, previous_url); - - let is_public = |v: &Option| match v - .as_ref() - .unwrap_or(&serde_json::Value::Null) - { - serde_json::Value::Array(v) => v - .iter() - .filter_map(serde_json::Value::as_str) - .any(|s| s == PUBLIC_VISIBILITY), - serde_json::Value::String(s) => s == PUBLIC_VISIBILITY, - _ => false, - }; - - let public_visibility = is_public(¬e.object_props.to) - || is_public(¬e.object_props.bto) - || is_public(¬e.object_props.cc) - || is_public(¬e.object_props.bcc); - - let comm = Comment::insert( - conn, - NewComment { - content: SafeString::new(¬e.object_props.content_string()?), - spoiler_text: note.object_props.summary_string().unwrap_or_default(), - ap_url: note.object_props.id_string().ok(), - in_response_to_id: previous_comment.iter().map(|c| c.id).next(), - post_id: previous_comment.map(|c| c.post_id).or_else(|_| { - Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result - })?, - author_id: User::from_id( - conn, - ¬e.object_props.attributed_to_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)? - .id, - sensitive: note.object_props.summary_string().is_ok(), - public_visibility, - }, - )?; - - // save mentions - if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { - for tag in tags { - serde_json::from_value::(tag) - .map_err(Error::from) - .and_then(|m| { - let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0]; - let not_author = m.link_props.href_string()? != author.ap_url.clone(); - Mention::from_activity(conn, &m, comm.id, false, not_author) - }) - .ok(); - } - } - comm - }; - - if !comm.public_visibility { - let receivers_ap_url = |v: Option| { - let filter = |e: serde_json::Value| { - if let serde_json::Value::String(s) = e { - Some(s) - } else { - None - } - }; - match v.unwrap_or(serde_json::Value::Null) { - serde_json::Value::Array(v) => v, - v => vec![v], - } - .into_iter() - .filter_map(filter) - }; - - let mut note = note; - - let to = receivers_ap_url(note.object_props.to.take()); - let cc = receivers_ap_url(note.object_props.cc.take()); - let bto = receivers_ap_url(note.object_props.bto.take()); - let bcc = receivers_ap_url(note.object_props.bcc.take()); - - let receivers_ap_url = to - .chain(cc) - .chain(bto) - .chain(bcc) - .collect::>() // remove duplicates (don't do a query more than once) - .into_iter() - .flat_map(|v| { - if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) { - vec![user] - } else { - vec![] // TODO try to fetch collection - } - }) - .filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false)) - .collect::>(); //remove duplicates (prevent db error) - - for user in &receivers_ap_url { - CommentSeers::insert( - conn, - NewCommentSeers { - comment_id: comm.id, - user_id: user.id, - }, - )?; - } - } - - comm.notify(conn)?; - Ok(comm) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for Comment { type Error = Error; type Object = Note07; @@ -484,7 +349,7 @@ impl FromId07 for Comment { post_id: previous_comment.map(|c| c.post_id).or_else(|_| { Ok(Post::find_by_ap_url(conn, previous_url.as_str())?.id) as Result })?, - author_id: User::from_id( + author_id: User::from_id07( conn, ¬e .attributed_to() @@ -537,7 +402,7 @@ impl FromId07 for Comment { let receivers_ap_url = receiver_ids .into_iter() .flat_map(|v| { - if let Ok(user) = User::from_id(conn, v.as_ref(), None, CONFIG.proxy()) { + if let Ok(user) = User::from_id07(conn, v.as_ref(), None, CONFIG.proxy()) { vec![user] } else { vec![] // TODO try to fetch collection diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index 5d6beb43..6a2f3a9a 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -12,7 +12,7 @@ use activitystreams::{ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use plume_common::activity_pub::{ broadcast, broadcast07, - inbox::{AsActor, AsObject, AsObject07, FromId, FromId07}, + inbox::{AsActor, AsObject, AsObject07, FromId07}, sign::Signer, Id, IntoId, PUBLIC_VISIBILITY, }; @@ -272,38 +272,6 @@ impl AsObject07 for User { } } -impl FromId for Follow { - type Error = Error; - type Object = FollowAct; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Follow::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, follow: FollowAct) -> Result { - let actor = User::from_id( - conn, - &follow.follow_props.actor_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)?; - - let target = User::from_id( - conn, - &follow.follow_props.object_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)?; - Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for Follow { type Error = Error; type Object = FollowAct07; diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index 202354dc..8a7b0e23 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -12,7 +12,7 @@ use activitystreams::{ use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use plume_common::activity_pub::{ - inbox::{AsActor, AsObject, AsObject07, FromId, FromId07}, + inbox::{AsActor, AsObject, AsObject07, FromId07}, sign::Signer, Id, IntoId, PUBLIC_VISIBILITY, }; @@ -158,46 +158,6 @@ impl AsObject07 for Post { } } -impl FromId for Like { - type Error = Error; - type Object = activity::Like; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Like::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, act: activity::Like) -> Result { - let res = Like::insert( - conn, - NewLike { - post_id: Post::from_id( - conn, - &act.like_props.object_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)? - .id, - user_id: User::from_id( - conn, - &act.like_props.actor_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)? - .id, - ap_url: act.object_props.id_string()?, - }, - )?; - res.notify(conn)?; - Ok(res) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for Like { type Error = Error; type Object = Like07; @@ -210,7 +170,7 @@ impl FromId07 for Like { let res = Like::insert( conn, NewLike { - post_id: Post::from_id( + post_id: Post::from_id07( conn, act.object_field_ref() .as_single_id() diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index 7d73b3b4..13048292 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -7,7 +7,7 @@ use activitystreams::{object::Image as Image07, prelude::*}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use guid_create::GUID; use plume_common::{ - activity_pub::{inbox::FromId, request, Id, ToAsString, ToAsUri}, + activity_pub::{inbox::FromId07, request, Id, ToAsString, ToAsUri}, utils::{escape, MediaProcessor}, }; use std::{ @@ -222,7 +222,7 @@ impl Media { // TODO: conditional GET request::get( remote_url.as_str(), - User::get_sender(), + User::get_sender07(), CONFIG.proxy().cloned(), )? .copy_to(&mut dest)?; @@ -275,7 +275,7 @@ impl Media { remote_url: None, sensitive: image.object_props.summary_string().is_ok(), content_warning: image.object_props.summary_string().ok(), - owner_id: User::from_id( + owner_id: User::from_id07( conn, image .object_props @@ -311,7 +311,7 @@ impl Media { // TODO: conditional GET request::get( remote_url.as_str(), - User::get_sender(), + User::get_sender07(), CONFIG.proxy().cloned(), )? .copy_to(&mut dest)?; @@ -362,7 +362,7 @@ impl Media { remote_url: None, sensitive: summary.is_some(), content_warning: summary, - owner_id: User::from_id( + owner_id: User::from_id07( conn, &image .attributed_to() diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 7a1930eb..ad5bbebe 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -26,7 +26,7 @@ use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use once_cell::sync::Lazy; use plume_common::{ activity_pub::{ - inbox::{AsActor, AsObject, AsObject07, FromId, FromId07}, + inbox::{AsActor, AsObject, AsObject07, FromId07}, sign::Signer, Hashtag, Hashtag07, HashtagType07, Id, IntoId, Licensed, Licensed07, LicensedArticle as LicensedArticle07, Source, SourceProperty, ToAsString, ToAsUri, @@ -863,160 +863,6 @@ impl Post { } } -impl FromId for Post { - type Error = Error; - type Object = LicensedArticle; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Self::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result { - let conn = conn; - let license = article.custom_props.license_string().unwrap_or_default(); - let article = article.object; - - let (blog, authors) = article - .object_props - .attributed_to_link_vec::()? - .into_iter() - .fold((None, vec![]), |(blog, mut authors), link| { - let url = link; - match User::from_id(conn, &url, None, CONFIG.proxy()) { - Ok(u) => { - authors.push(u); - (blog, authors) - } - Err(_) => ( - blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()), - authors, - ), - } - }); - - let cover = article - .object_props - .icon_object::() - .ok() - .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); - - let title = article.object_props.name_string()?; - let ap_url = article - .object_props - .url_string() - .or_else(|_| article.object_props.id_string())?; - let post = Post::from_db(conn, &ap_url) - .and_then(|mut post| { - let mut updated = false; - - let slug = Self::slug(&title); - let content = SafeString::new(&article.object_props.content_string()?); - let subtitle = article.object_props.summary_string()?; - let source = article.ap_object_props.source_object::()?.content; - if post.slug != slug { - post.slug = slug.to_string(); - updated = true; - } - if post.title != title { - post.title = title.clone(); - updated = true; - } - if post.content != content { - post.content = content; - updated = true; - } - if post.license != license { - post.license = license.clone(); - updated = true; - } - if post.subtitle != subtitle { - post.subtitle = subtitle; - updated = true; - } - if post.source != source { - post.source = source; - updated = true; - } - if post.cover_id != cover { - post.cover_id = cover; - updated = true; - } - - if updated { - post.update(conn)?; - } - - Ok(post) - }) - .or_else(|_| { - Post::insert( - conn, - NewPost { - blog_id: blog.ok_or(Error::NotFound)?.id, - slug: Self::slug(&title).to_string(), - title, - content: SafeString::new(&article.object_props.content_string()?), - published: true, - license, - // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields - ap_url, - creation_date: Some(article.object_props.published_utctime()?.naive_utc()), - subtitle: article.object_props.summary_string()?, - source: article.ap_object_props.source_object::()?.content, - cover_id: cover, - }, - ) - .and_then(|post| { - for author in authors { - PostAuthor::insert( - conn, - NewPostAuthor { - post_id: post.id, - author_id: author.id, - }, - )?; - } - - Ok(post) - }) - })?; - - // save mentions and tags - let mut hashtags = md_to_html(&post.source, None, false, None) - .2 - .into_iter() - .collect::>(); - if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { - for tag in tags { - serde_json::from_value::(tag.clone()) - .map(|m| Mention::from_activity(conn, &m, post.id, true, true)) - .ok(); - - serde_json::from_value::(tag.clone()) - .map_err(Error::from) - .and_then(|t| { - let tag_name = t.name_string()?; - Ok(Tag::from_activity( - conn, - &t, - post.id, - hashtags.remove(&tag_name), - )) - }) - .ok(); - } - } - - Timeline::add_to_all_timelines(conn, &post, Kind::Original)?; - - Ok(post) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for Post { type Error = Error; type Object = LicensedArticle07; @@ -1273,43 +1119,6 @@ pub struct PostUpdate { pub tags: Option, } -impl FromId for PostUpdate { - type Error = Error; - type Object = LicensedArticle; - - fn from_db(_: &DbConn, _: &str) -> Result { - // Always fail because we always want to deserialize the AP object - Err(Error::NotFound) - } - - fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result { - Ok(PostUpdate { - ap_url: updated.object.object_props.id_string()?, - title: updated.object.object_props.name_string().ok(), - subtitle: updated.object.object_props.summary_string().ok(), - content: updated.object.object_props.content_string().ok(), - cover: updated - .object - .object_props - .icon_object::() - .ok() - .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)), - source: updated - .object - .ap_object_props - .source_object::() - .ok() - .map(|x| x.content), - license: updated.custom_props.license_string().ok(), - tags: updated.object.object_props.tag, - }) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for PostUpdate { type Error = Error; type Object = LicensedArticle07; @@ -1373,7 +1182,7 @@ impl AsObject for PostUpdate { fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { let mut post = - Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; + Post::from_id07(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; if !post.is_author(conn, actor.id)? { // TODO: maybe the author was added in the meantime @@ -1445,7 +1254,7 @@ impl AsObject07 for PostUpdate { fn activity07(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { let mut post = - Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; + Post::from_id07(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; if !post.is_author(conn, actor.id)? { // TODO: maybe the author was added in the meantime diff --git a/plume-models/src/remote_fetch_actor.rs b/plume-models/src/remote_fetch_actor.rs index 097fb1da..89eabe71 100644 --- a/plume-models/src/remote_fetch_actor.rs +++ b/plume-models/src/remote_fetch_actor.rs @@ -1,12 +1,16 @@ use crate::{ db_conn::{DbConn, DbPool}, follows, - posts::{LicensedArticle, Post}, + posts::Post, users::{User, UserEvent}, ACTOR_SYS, CONFIG, USER_CHAN, }; -use activitypub::activity::Create; -use plume_common::activity_pub::inbox::FromId; +use activitystreams::{ + activity::{ActorAndObjectRef, Create as Create07}, + base::AnyBase, + object::kind::ArticleType, +}; +use plume_common::activity_pub::{inbox::FromId07, LicensedArticle as LicensedArticle07}; use riker::actors::{Actor, ActorFactoryArgs, ActorRefFactory, Context, Sender, Subscribe, Tell}; use std::sync::Arc; use tracing::{error, info, warn}; @@ -64,17 +68,23 @@ impl ActorFactoryArgs for RemoteFetchActor { } fn fetch_and_cache_articles(user: &Arc, conn: &DbConn) { - let create_acts = user.fetch_outbox::(); + let create_acts = user.fetch_outbox07::(); match create_acts { Ok(create_acts) => { for create_act in create_acts { - match create_act.create_props.object_object::() { - Ok(article) => { - Post::from_activity(conn, article) + match create_act + .object_field_ref() + .as_single_base() + .and_then(|base| { + let any_base = AnyBase::from_base(base.clone()); // FIXME: Don't clone() + any_base.extend::().ok() + }) { + Some(Some(article)) => { + Post::from_activity07(conn, article) .expect("Article from remote user couldn't be saved"); info!("Fetched article from remote user"); } - Err(e) => warn!("Error while fetching articles in background: {:?}", e), + _ => warn!("Error while fetching articles in background"), } } } @@ -89,7 +99,7 @@ fn fetch_and_cache_followers(user: &Arc, conn: &DbConn) { match follower_ids { Ok(user_ids) => { for user_id in user_ids { - let follower = User::from_id(conn, &user_id, None, CONFIG.proxy()); + let follower = User::from_id07(conn, &user_id, None, CONFIG.proxy()); match follower { Ok(follower) => { let inserted = follows::Follow::insert( diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index 88269020..aa35aa27 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -12,7 +12,7 @@ use activitystreams::{ use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use plume_common::activity_pub::{ - inbox::{AsActor, AsObject, AsObject07, FromId, FromId07}, + inbox::{AsActor, AsObject, AsObject07, FromId07}, sign::Signer, Id, IntoId, PUBLIC_VISIBILITY, }; @@ -186,46 +186,6 @@ impl AsObject07 for Post { } } -impl FromId for Reshare { - type Error = Error; - type Object = Announce; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Reshare::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, act: Announce) -> Result { - let res = Reshare::insert( - conn, - NewReshare { - post_id: Post::from_id( - conn, - &act.announce_props.object_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)? - .id, - user_id: User::from_id( - conn, - &act.announce_props.actor_link::()?, - None, - CONFIG.proxy(), - ) - .map_err(|(_, e)| e)? - .id, - ap_url: act.object_props.id_string()?, - }, - )?; - res.notify(conn)?; - Ok(res) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for Reshare { type Error = Error; type Object = Announce07; @@ -238,7 +198,7 @@ impl FromId07 for Reshare { let res = Reshare::insert( conn, NewReshare { - post_id: Post::from_id( + post_id: Post::from_id07( conn, act.object_field_ref() .as_single_id() @@ -249,7 +209,7 @@ impl FromId07 for Reshare { ) .map_err(|(_, e)| e)? .id, - user_id: User::from_id( + user_id: User::from_id07( conn, act.actor_field_ref() .as_single_id() diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 638d9620..3497aa99 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -13,7 +13,7 @@ use activitypub::{ }; use activitystreams::{ activity::Delete as Delete07, - actor::AsApActor, + actor::{ApActor, AsApActor}, actor::{ApActor as ApActor07, Endpoints as Endpoints07, Person as Person07}, base::{AnyBase, Base}, collection::{ @@ -21,7 +21,7 @@ use activitystreams::{ }, iri_string::types::IriString, markers::Activity as Activity07, - object::{AsObject as _, Image as Image07, Tombstone as Tombstone07}, + object::{kind::ImageType, AsObject as _, Image as Image07, Tombstone as Tombstone07}, prelude::*, }; use chrono::{NaiveDateTime, Utc}; @@ -35,7 +35,7 @@ use openssl::{ }; use plume_common::{ activity_pub::{ - inbox::{AsActor, AsObject, AsObject07, FromId, FromId07}, + inbox::{AsActor, AsObject, AsObject07, FromId07}, request::get, sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}, ActivityStream, ApSignature, ApSignature07, CustomPerson as CustomPerson07, Id, IntoId, @@ -53,7 +53,6 @@ use std::{ hash::{Hash, Hasher}, sync::Arc, }; -use url::Url; use webfinger::*; pub type CustomPerson = CustomObject; @@ -238,7 +237,7 @@ impl User { .into_iter() .find(|l| l.mime_type == Some(String::from("application/activity+json"))) .ok_or(Error::Webfinger)?; - User::from_id( + User::from_id07( conn, link.href.as_ref().ok_or(Error::Webfinger)?, None, @@ -256,53 +255,84 @@ impl User { .ok_or(Error::Webfinger) } - fn fetch(url: &str) -> Result { - let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; + fn fetch(url: &str) -> Result { + let mut res = get(url, Self::get_sender07(), CONFIG.proxy().cloned())?; let text = &res.text()?; // without this workaround, publicKey is not correctly deserialized - let ap_sign = serde_json::from_str::(text)?; - let mut json = serde_json::from_str::(text)?; - json.custom_props = ap_sign; + let ap_sign = serde_json::from_str::(text)?; + let person = serde_json::from_str::(text)?; + let json = CustomPerson07::new( + ApActor::new( + person + .clone() + .id_unchecked() + .ok_or(Error::MissingApProperty)? + .to_owned(), + person, + ), + ap_sign, + ); // FIXME: Don't clone() Ok(json) } pub fn fetch_from_url(conn: &DbConn, url: &str) -> Result { - User::fetch(url).and_then(|json| User::from_activity(conn, json)) + User::fetch(url).and_then(|json| User::from_activity07(conn, json)) } pub fn refetch(&self, conn: &Connection) -> Result<()> { User::fetch(&self.ap_url.clone()).and_then(|json| { let avatar = Media::save_remote( conn, - json.object - .object_props - .icon_image()? // FIXME: Fails when icon is not set - .object_props - .url_string()?, + json.ap_actor_ref() + .icon() + .ok_or(Error::MissingApProperty)? // FIXME: Fails when icon is not set + .iter() + .next() + .and_then(|i| { + i.clone() + .extend::() // FIXME: Don't clone() + .ok()? + .and_then(|url| Some(url.id_unchecked()?.to_string())) + }) + .ok_or(Error::MissingApProperty)?, self, ) .ok(); + let pub_key = &json.ext_one.public_key.public_key_pem; diesel::update(self) .set(( - users::username.eq(json.object.ap_actor_props.preferred_username_string()?), - users::display_name.eq(json.object.object_props.name_string()?), - users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?), - users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?), + users::username.eq(json + .ap_actor_ref() + .preferred_username() + .ok_or(Error::MissingApProperty)?), + users::display_name.eq(json + .ap_actor_ref() + .name() + .ok_or(Error::MissingApProperty)? + .to_as_string() + .ok_or(Error::MissingApProperty)?), + users::outbox_url.eq(json + .ap_actor_ref() + .outbox()? + .ok_or(Error::MissingApProperty)? + .as_str()), + users::inbox_url.eq(json.ap_actor_ref().inbox()?.as_str()), users::summary.eq(SafeString::new( &json - .object - .object_props - .summary_string() + .ap_actor_ref() + .summary() + .and_then(|summary| summary.to_as_string()) .unwrap_or_default(), )), - users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?), + users::followers_endpoint.eq(json + .ap_actor_ref() + .followers()? + .ok_or(Error::MissingApProperty)? + .as_str()), users::avatar_id.eq(avatar.map(|a| a.id)), users::last_fetched_date.eq(Utc::now().naive_utc()), - users::public_key.eq(json - .custom_props - .public_key_publickey()? - .public_key_pem_string()?), + users::public_key.eq(pub_key), )) .execute(conn) .map(|_| ()) @@ -548,7 +578,7 @@ impl User { Ok(coll) } fn fetch_outbox_page(&self, url: &str) -> Result<(Vec, Option)> { - let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; + let mut res = get(url, Self::get_sender07(), CONFIG.proxy().cloned())?; let text = &res.text()?; let json: serde_json::Value = serde_json::from_str(text)?; let items = json["items"] @@ -565,7 +595,7 @@ impl User { &self, url: &str, ) -> Result<(Vec, Option)> { - let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; + let mut res = get(url, Self::get_sender07(), CONFIG.proxy().cloned())?; let text = &res.text()?; let json: serde_json::Value = serde_json::from_str(text)?; let items = json["items"] @@ -581,7 +611,7 @@ impl User { pub fn fetch_outbox(&self) -> Result> { let mut res = get( &self.outbox_url[..], - Self::get_sender(), + Self::get_sender07(), CONFIG.proxy().cloned(), )?; let text = &res.text()?; @@ -617,7 +647,7 @@ impl User { pub fn fetch_outbox07(&self) -> Result> { let mut res = get( &self.outbox_url[..], - Self::get_sender(), + Self::get_sender07(), CONFIG.proxy().cloned(), )?; let text = &res.text()?; @@ -653,7 +683,7 @@ impl User { pub fn fetch_followers_ids(&self) -> Result> { let mut res = get( &self.followers_endpoint[..], - Self::get_sender(), + Self::get_sender07(), CONFIG.proxy().cloned(), )?; let text = &res.text()?; @@ -1097,110 +1127,6 @@ impl IntoId for User { impl Eq for User {} -impl FromId for User { - type Error = Error; - type Object = CustomPerson; - - fn from_db(conn: &DbConn, id: &str) -> Result { - Self::find_by_ap_url(conn, id) - } - - fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result { - let url = Url::parse(&acct.object.object_props.id_string()?)?; - let inst = url.host_str().ok_or(Error::Url)?; - let instance = Instance::find_by_domain(conn, inst).or_else(|_| { - Instance::insert( - conn, - NewInstance { - name: inst.to_owned(), - public_domain: inst.to_owned(), - local: false, - // We don't really care about all the following for remote instances - long_description: SafeString::new(""), - short_description: SafeString::new(""), - default_license: String::new(), - open_registrations: true, - short_description_html: String::new(), - long_description_html: String::new(), - }, - ) - })?; - - let username = acct.object.ap_actor_props.preferred_username_string()?; - - if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) { - return Err(Error::InvalidValue); - } - - let fqn = if instance.local { - username.clone() - } else { - format!("{}@{}", username, instance.public_domain) - }; - - let user = User::insert( - conn, - NewUser { - display_name: acct - .object - .object_props - .name_string() - .unwrap_or_else(|_| username.clone()), - username, - outbox_url: acct.object.ap_actor_props.outbox_string()?, - inbox_url: acct.object.ap_actor_props.inbox_string()?, - role: 2, - summary: acct - .object - .object_props - .summary_string() - .unwrap_or_default(), - summary_html: SafeString::new( - &acct - .object - .object_props - .summary_string() - .unwrap_or_default(), - ), - email: None, - hashed_password: None, - instance_id: instance.id, - ap_url: acct.object.object_props.id_string()?, - public_key: acct - .custom_props - .public_key_publickey()? - .public_key_pem_string()?, - private_key: None, - shared_inbox_url: acct - .object - .ap_actor_props - .endpoints_endpoint() - .and_then(|e| e.shared_inbox_string()) - .ok(), - followers_endpoint: acct.object.ap_actor_props.followers_string()?, - fqn, - avatar_id: None, - }, - )?; - - if let Ok(icon) = acct.object.object_props.icon_image() { - if let Ok(url) = icon.object_props.url_string() { - let avatar = Media::save_remote(conn, url, &user); - - if let Ok(avatar) = avatar { - user.set_avatar(conn, avatar.id)?; - } - } - } - - Ok(user) - } - - fn get_sender() -> &'static dyn Signer { - Instance::get_local_instance_user().expect("Failed to local instance user") - } -} - impl FromId07 for User { type Error = Error; type Object = CustomPerson07; @@ -1690,32 +1616,6 @@ pub(crate) mod tests { }); } - #[test] - fn self_federation() { - let conn = db(); - conn.test_transaction::<_, (), _>(|| { - let users = fill_database(&conn); - - let ap_repr = users[0].to_activity(&conn).unwrap(); - users[0].delete(&conn).unwrap(); - let user = User::from_activity(&conn, ap_repr).unwrap(); - - assert_eq!(user.username, users[0].username); - assert_eq!(user.display_name, users[0].display_name); - assert_eq!(user.outbox_url, users[0].outbox_url); - assert_eq!(user.inbox_url, users[0].inbox_url); - assert_eq!(user.instance_id, users[0].instance_id); - assert_eq!(user.ap_url, users[0].ap_url); - assert_eq!(user.public_key, users[0].public_key); - assert_eq!(user.shared_inbox_url, users[0].shared_inbox_url); - assert_eq!(user.followers_endpoint, users[0].followers_endpoint); - assert_eq!(user.avatar_url(&conn), users[0].avatar_url(&conn)); - assert_eq!(user.fqn, users[0].fqn); - assert_eq!(user.summary_html, users[0].summary_html); - Ok(()) - }); - } - #[test] fn self_federation07() { let conn = db(); diff --git a/src/inbox.rs b/src/inbox.rs index 78069b69..f4fbef47 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -1,5 +1,5 @@ use plume_common::activity_pub::{ - inbox::FromId, + inbox::FromId07, request::Digest, sign::{verify_http_headers, Signable}, }; @@ -26,7 +26,7 @@ pub fn handle_incoming( .or_else(|| activity["actor"]["id"].as_str()) .ok_or(status::BadRequest(Some("Missing actor id for activity")))?; - let actor = User::from_id(&conn, actor_id, None, CONFIG.proxy()) + let actor = User::from_id07(&conn, actor_id, None, CONFIG.proxy()) .expect("instance::shared_inbox: user error"); if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) { // maybe we just know an old key? diff --git a/src/routes/instance.rs b/src/routes/instance.rs index cedaa900..d0130610 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -11,7 +11,7 @@ use validator::{Validate, ValidationErrors}; use crate::inbox; use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect}; use crate::template_utils::{IntoContext, Ructe}; -use plume_common::activity_pub::{broadcast, inbox::FromId}; +use plume_common::activity_pub::{broadcast, inbox::FromId07}; use plume_models::{ admin::*, blocklisted_emails::*, @@ -404,7 +404,7 @@ pub fn interact(conn: DbConn, user: Option, target: String) -> Option, target: String) -> Option