From 0ea1d57e481071146f85ca0d86e57721f24fe16f Mon Sep 17 00:00:00 2001 From: fdb-hiroshima <35889323+fdb-hiroshima@users.noreply.github.com> Date: Sun, 23 Dec 2018 11:12:15 +0100 Subject: [PATCH] Fix some federation issues (#357) * Fix some follow issues Fix not receiving notifications when followed by remote users Fix imposibility to be unfollowed by Mastodon/Pleroma users (tested only against Pleroma) * Fix notification on every post * Fix issues with federation Send Link instead of Object when emiting Follow request Receive both Link and Object for Follow request Don't panic when fetching user with no followers or posts from Pleroma Reorder follower routes so Activity Pub one is reachable * Generate absolute urls for mentions and tags * Verify author when undoing activity by Link --- plume-common/src/utils.rs | 10 ++--- plume-models/src/comments.rs | 5 ++- plume-models/src/follows.rs | 18 +++++--- plume-models/src/instance.rs | 4 +- plume-models/src/posts.rs | 10 ++--- plume-models/src/users.rs | 4 +- src/inbox.rs | 85 +++++++++++++++++++++--------------- src/routes/comments.rs | 5 ++- src/routes/posts.rs | 4 +- src/routes/user.rs | 4 +- 10 files changed, 85 insertions(+), 64 deletions(-) diff --git a/plume-common/src/utils.rs b/plume-common/src/utils.rs index 73559a77..98514f9f 100644 --- a/plume-common/src/utils.rs +++ b/plume-common/src/utils.rs @@ -41,7 +41,7 @@ enum State { } /// Returns (HTML, mentions, hashtags) -pub fn md_to_html(md: &str) -> (String, HashSet, HashSet) { +pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet, HashSet) { let parser = Parser::new_ext(md, Options::all()); let (parser, mentions, hashtags): (Vec, Vec, Vec) = parser @@ -80,7 +80,7 @@ pub fn md_to_html(md: &str) -> (String, HashSet, HashSet) { } let mention = text_acc; let short_mention = mention.splitn(1, '@').nth(0).unwrap_or(""); - let link = Tag::Link(format!("/@/{}/", &mention).into(), short_mention.to_owned().into()); + let link = Tag::Link(format!("//{}/@/{}/", base_url, &mention).into(), short_mention.to_owned().into()); mentions.push(mention.clone()); events.push(Event::Start(link.clone())); @@ -100,7 +100,7 @@ pub fn md_to_html(md: &str) -> (String, HashSet, HashSet) { text_acc.push(c); } let hashtag = text_acc; - let link = Tag::Link(format!("/tag/{}", &hashtag.to_camel_case()).into(), hashtag.to_owned().into()); + let link = Tag::Link(format!("//{}/tag/{}", base_url, &hashtag.to_camel_case()).into(), hashtag.to_owned().into()); hashtags.push(hashtag.clone()); events.push(Event::Start(link.clone())); @@ -188,7 +188,7 @@ mod tests { ]; for (md, mentions) in tests { - assert_eq!(md_to_html(md).1, mentions.into_iter().map(|s| s.to_string()).collect::>()); + assert_eq!(md_to_html(md, "").1, mentions.into_iter().map(|s| s.to_string()).collect::>()); } } @@ -207,7 +207,7 @@ mod tests { ]; for (md, mentions) in tests { - assert_eq!(md_to_html(md).2, mentions.into_iter().map(|s| s.to_string()).collect::>()); + assert_eq!(md_to_html(md, "").2, mentions.into_iter().map(|s| s.to_string()).collect::>()); } } } diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index b51dd393..901a907b 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -91,7 +91,10 @@ impl Comment { } pub fn to_activity(&self, conn: &Connection) -> Note { - let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref()); + let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(), + &Instance::get_local(conn) + .expect("Comment::to_activity: instance error") + .public_domain); let author = User::get(conn, self.author_id).expect("Comment::to_activity: author error"); let mut note = Note::default(); diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index b7faad95..c7b14a5d 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -58,13 +58,13 @@ impl Follow { .set_actor_link::(user.clone().into_id()) .expect("Follow::to_activity: actor error"); act.follow_props - .set_object_object(user.to_activity(&*conn)) + .set_object_link::(target.clone().into_id()) .expect("Follow::to_activity: object error"); act.object_props .set_id_string(self.ap_url.clone()) .expect("Follow::to_activity: id error"); act.object_props - .set_to_link(target.clone().into_id()) + .set_to_link(target.into_id()) .expect("Follow::to_activity: target error"); act.object_props .set_cc_link_vec::(vec![]) @@ -82,14 +82,12 @@ impl Follow { from_id: i32, target_id: i32, ) -> Follow { - let from_url: String = from.clone().into_id().into(); - let target_url: String = target.clone().into_id().into(); let res = Follow::insert( conn, NewFollow { follower_id: from_id, following_id: target_id, - ap_url: format!("{}/follow/{}", from_url, target_url), + ap_url: follow.object_props.id_string().expect("Follow::accept_follow: get id error"), }, ); @@ -98,7 +96,7 @@ impl Follow { accept .object_props .set_id_string(accept_id) - .expect("Follow::accept_follow: id error"); + .expect("Follow::accept_follow: set id error"); accept .object_props .set_to_link(from.clone().into_id()) @@ -199,7 +197,7 @@ impl Deletable for Follow { .set_id_string(format!("{}/undo", self.ap_url)) .expect("Follow::delete: id error"); undo.undo_props - .set_object_object(self.to_activity(conn)) + .set_object_link::(self.clone().into_id()) .expect("Follow::delete: object error"); undo } @@ -214,3 +212,9 @@ impl Deletable for Follow { } } } + +impl IntoId for Follow { + fn into_id(self) -> Id { + Id::new(self.ap_url) + } +} diff --git a/plume-models/src/instance.rs b/plume-models/src/instance.rs index 86d01817..6d01ab19 100644 --- a/plume-models/src/instance.rs +++ b/plume-models/src/instance.rs @@ -139,8 +139,8 @@ impl Instance { short_description: SafeString, long_description: SafeString, ) { - let (sd, _, _) = md_to_html(short_description.as_ref()); - let (ld, _, _) = md_to_html(long_description.as_ref()); + let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain); + let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain); diesel::update(self) .set(( instances::name.eq(name), diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 333ff728..94f968ee 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -431,8 +431,8 @@ impl Post { } pub fn to_activity(&self, conn: &Connection) -> LicensedArticle { - let mut to = self.get_receivers_urls(conn); - to.push(PUBLIC_VISIBILTY.to_string()); + let cc = self.get_receivers_urls(conn); + let to = vec![PUBLIC_VISIBILTY.to_string()]; let mut mentions_json = Mention::list_for_post(conn, self.id) .into_iter() @@ -526,7 +526,7 @@ impl Post { .expect("Post::to_activity: to error"); article .object_props - .set_cc_link_vec::(vec![]) + .set_cc_link_vec::(cc.into_iter().map(Id::new).collect()) .expect("Post::to_activity: cc error"); let mut license = Licensed::default(); license.set_license_string(self.license.clone()).expect("Post::to_activity: license error"); @@ -627,7 +627,7 @@ impl Post { post.license = license; } - let mut txt_hashtags = md_to_html(&post.source) + let mut txt_hashtags = md_to_html(&post.source, "") .2 .into_iter() .map(|s| s.to_camel_case()) @@ -889,7 +889,7 @@ impl<'a> FromActivity for Post } // save mentions and tags - let mut hashtags = md_to_html(&post.source) + let mut hashtags = md_to_html(&post.source, "") .2 .into_iter() .map(|s| s.to_camel_case()) diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 49e80098..242af35d 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -555,7 +555,7 @@ impl User { serde_json::from_str(text).expect("User::fetch_outbox: parsing error"); json["items"] .as_array() - .expect("Outbox.items is not an array") + .unwrap_or(&vec![]) .into_iter() .filter_map(|j| serde_json::from_value(j.clone()).ok()) .collect::>() @@ -587,7 +587,7 @@ impl User { serde_json::from_str(text).expect("User::fetch_followers_ids: parsing error"); json["items"] .as_array() - .expect("User::fetch_followers_ids: not an array error") + .unwrap_or(&vec![]) .into_iter() .filter_map(|j| serde_json::from_value(j.clone()).ok()) .collect::>() diff --git a/src/inbox.rs b/src/inbox.rs index 9a2425fa..e5db005e 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -24,7 +24,7 @@ use serde_json; use std::io::Read; use plume_common::activity_pub::{ - inbox::{Deletable, FromActivity, InboxError}, + inbox::{Deletable, FromActivity, InboxError, Notify}, Id,request::Digest, }; use plume_models::{ @@ -68,7 +68,7 @@ pub trait Inbox { Ok(()) } "Follow" => { - Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id).notify(conn); Ok(()) } "Like" => { @@ -81,44 +81,57 @@ pub trait Inbox { } "Undo" => { let act: Undo = serde_json::from_value(act.clone())?; - match act.undo_props.object["type"] - .as_str() - .expect("Inbox::received: undo without original type error") - { - "Like" => { - likes::Like::delete_id( - &act.undo_props - .object_object::()? - .object_props - .id_string()?, - actor_id.as_ref(), - conn, - ); - Ok(()) + if let Some(t) = act.undo_props.object["type"].as_str() { + match t { + "Like" => { + likes::Like::delete_id( + &act.undo_props + .object_object::()? + .object_props + .id_string()?, + actor_id.as_ref(), + conn, + ); + Ok(()) + } + "Announce" => { + Reshare::delete_id( + &act.undo_props + .object_object::()? + .object_props + .id_string()?, + actor_id.as_ref(), + conn, + ); + Ok(()) + } + "Follow" => { + Follow::delete_id( + &act.undo_props + .object_object::()? + .object_props + .id_string()?, + actor_id.as_ref(), + conn, + ); + Ok(()) + } + _ => Err(InboxError::CantUndo)?, } - "Announce" => { - Reshare::delete_id( - &act.undo_props - .object_object::()? - .object_props - .id_string()?, - actor_id.as_ref(), - conn, - ); + } else { + let link = act.undo_props.object.as_str().expect("Inbox::received: undo don't contain type and isn't Link"); + if let Some(like) = likes::Like::find_by_ap_url(conn, link) { + likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn); Ok(()) - } - "Follow" => { - Follow::delete_id( - &act.undo_props - .object_object::()? - .object_props - .id_string()?, - actor_id.as_ref(), - conn, - ); + } else if let Some(reshare) = Reshare::find_by_ap_url(conn, link) { + Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn); Ok(()) + } else if let Some(follow) = Follow::find_by_ap_url(conn, link) { + Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn); + Ok(()) + } else { + Err(InboxError::NoType)? } - _ => Err(InboxError::CantUndo)?, } } "Update" => { diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 144e6512..6eeb5382 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -12,6 +12,7 @@ use plume_models::{ blogs::Blog, comments::*, db_conn::DbConn, + instance::Instance, mentions::Mention, posts::Post, safe_string::SafeString, @@ -35,7 +36,7 @@ pub fn create(blog_name: String, slug: String, form: LenientForm let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; form.validate() .map(|_| { - let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref()); + let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref(), &Instance::get_local(&conn).expect("comments::create: Error getting local instance").public_domain); let comm = Comment::insert(&*conn, NewComment { content: SafeString::new(html.as_ref()), in_response_to_id: form.responding_to, @@ -64,7 +65,7 @@ pub fn create(blog_name: String, slug: String, form: LenientForm let comments = Comment::list_by_post(&*conn, post.id); let previous = form.responding_to.map(|r| Comment::get(&*conn, r) - .expect("posts::details_reponse: Error retrieving previous comment")); + .expect("comments::create: Error retrieving previous comment")); Some(render!(posts::details( &(&*conn, &intl.catalog, Some(user.clone())), diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 70181872..00451d59 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -201,7 +201,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien // actually it's not "Ok"… Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _))) } else { - let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref()); + let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::update: Error getting local instance").public_domain); // update publication date if when this article is no longer a draft let newly_published = if !post.published && !form.draft { @@ -309,7 +309,7 @@ pub fn create(blog_name: String, form: LenientForm, user: User, con // actually it's not "Ok"… Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) } else { - let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref()); + let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::create: Error getting l ocal instance").public_domain); let post = Post::insert(&*conn, NewPost { blog_id: blog.id, diff --git a/src/routes/user.rs b/src/routes/user.rs index ae96513e..89d99e56 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -164,7 +164,7 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash { ) } -#[get("/@//followers?")] +#[get("/@//followers?", rank = 2)] pub fn followers(name: String, conn: DbConn, account: Option, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?; @@ -387,7 +387,7 @@ pub fn inbox( }) } -#[get("/@//followers")] +#[get("/@//followers", rank = 1)] pub fn ap_followers( name: String, conn: DbConn,