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
This commit is contained in:
fdb-hiroshima 2018-12-23 11:12:15 +01:00 committed by GitHub
parent ab2998e214
commit 0ea1d57e48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 85 additions and 64 deletions

View File

@ -41,7 +41,7 @@ enum State {
}
/// Returns (HTML, mentions, hashtags)
pub fn md_to_html(md: &str) -> (String, HashSet<String>, HashSet<String>) {
pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet<String>, HashSet<String>) {
let parser = Parser::new_ext(md, Options::all());
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
@ -80,7 +80,7 @@ pub fn md_to_html(md: &str) -> (String, HashSet<String>, HashSet<String>) {
}
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<String>, HashSet<String>) {
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::<HashSet<String>>());
assert_eq!(md_to_html(md, "").1, mentions.into_iter().map(|s| s.to_string()).collect::<HashSet<String>>());
}
}
@ -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::<HashSet<String>>());
assert_eq!(md_to_html(md, "").2, mentions.into_iter().map(|s| s.to_string()).collect::<HashSet<String>>());
}
}
}

View File

@ -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();

View File

@ -58,13 +58,13 @@ impl Follow {
.set_actor_link::<Id>(user.clone().into_id())
.expect("Follow::to_activity: actor error");
act.follow_props
.set_object_object(user.to_activity(&*conn))
.set_object_link::<Id>(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::<Id>(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<Connection, Undo> 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::<Id>(self.clone().into_id())
.expect("Follow::delete: object error");
undo
}
@ -214,3 +212,9 @@ impl Deletable<Connection, Undo> for Follow {
}
}
}
impl IntoId for Follow {
fn into_id(self) -> Id {
Id::new(self.ap_url)
}
}

View File

@ -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),

View File

@ -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::<Id>(vec![])
.set_cc_link_vec::<Id>(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<LicensedArticle, (&'a Connection, &'a Searcher)> 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())

View File

@ -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::<Vec<T>>()
@ -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::<Vec<String>>()

View File

@ -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,10 +81,8 @@ 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")
{
if let Some(t) = act.undo_props.object["type"].as_str() {
match t {
"Like" => {
likes::Like::delete_id(
&act.undo_props
@ -120,6 +118,21 @@ pub trait Inbox {
}
_ => Err(InboxError::CantUndo)?,
}
} 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(())
} 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)?
}
}
}
"Update" => {
let act: Update = serde_json::from_value(act.clone())?;

View File

@ -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<NewCommentForm>
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<NewCommentForm>
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())),

View File

@ -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<NewPostForm>, 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,

View File

@ -164,7 +164,7 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
)
}
#[get("/@/<name>/followers?<page>")]
#[get("/@/<name>/followers?<page>", rank = 2)]
pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, Ructe> {
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("/@/<name>/followers")]
#[get("/@/<name>/followers", rank = 1)]
pub fn ap_followers(
name: String,
conn: DbConn,