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:
parent
ab2998e214
commit
0ea1d57e48
|
@ -41,7 +41,7 @@ enum State {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns (HTML, mentions, hashtags)
|
/// 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 = Parser::new_ext(md, Options::all());
|
||||||
|
|
||||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
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 mention = text_acc;
|
||||||
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
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());
|
mentions.push(mention.clone());
|
||||||
events.push(Event::Start(link.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);
|
text_acc.push(c);
|
||||||
}
|
}
|
||||||
let hashtag = text_acc;
|
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());
|
hashtags.push(hashtag.clone());
|
||||||
events.push(Event::Start(link.clone()));
|
events.push(Event::Start(link.clone()));
|
||||||
|
@ -188,7 +188,7 @@ mod tests {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (md, mentions) in 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 {
|
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>>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,10 @@ impl Comment {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Note {
|
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 author = User::get(conn, self.author_id).expect("Comment::to_activity: author error");
|
||||||
let mut note = Note::default();
|
let mut note = Note::default();
|
||||||
|
|
|
@ -58,13 +58,13 @@ impl Follow {
|
||||||
.set_actor_link::<Id>(user.clone().into_id())
|
.set_actor_link::<Id>(user.clone().into_id())
|
||||||
.expect("Follow::to_activity: actor error");
|
.expect("Follow::to_activity: actor error");
|
||||||
act.follow_props
|
act.follow_props
|
||||||
.set_object_object(user.to_activity(&*conn))
|
.set_object_link::<Id>(target.clone().into_id())
|
||||||
.expect("Follow::to_activity: object error");
|
.expect("Follow::to_activity: object error");
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(self.ap_url.clone())
|
.set_id_string(self.ap_url.clone())
|
||||||
.expect("Follow::to_activity: id error");
|
.expect("Follow::to_activity: id error");
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link(target.clone().into_id())
|
.set_to_link(target.into_id())
|
||||||
.expect("Follow::to_activity: target error");
|
.expect("Follow::to_activity: target error");
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_cc_link_vec::<Id>(vec![])
|
.set_cc_link_vec::<Id>(vec![])
|
||||||
|
@ -82,14 +82,12 @@ impl Follow {
|
||||||
from_id: i32,
|
from_id: i32,
|
||||||
target_id: i32,
|
target_id: i32,
|
||||||
) -> Follow {
|
) -> Follow {
|
||||||
let from_url: String = from.clone().into_id().into();
|
|
||||||
let target_url: String = target.clone().into_id().into();
|
|
||||||
let res = Follow::insert(
|
let res = Follow::insert(
|
||||||
conn,
|
conn,
|
||||||
NewFollow {
|
NewFollow {
|
||||||
follower_id: from_id,
|
follower_id: from_id,
|
||||||
following_id: target_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
|
accept
|
||||||
.object_props
|
.object_props
|
||||||
.set_id_string(accept_id)
|
.set_id_string(accept_id)
|
||||||
.expect("Follow::accept_follow: id error");
|
.expect("Follow::accept_follow: set id error");
|
||||||
accept
|
accept
|
||||||
.object_props
|
.object_props
|
||||||
.set_to_link(from.clone().into_id())
|
.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))
|
.set_id_string(format!("{}/undo", self.ap_url))
|
||||||
.expect("Follow::delete: id error");
|
.expect("Follow::delete: id error");
|
||||||
undo.undo_props
|
undo.undo_props
|
||||||
.set_object_object(self.to_activity(conn))
|
.set_object_link::<Id>(self.clone().into_id())
|
||||||
.expect("Follow::delete: object error");
|
.expect("Follow::delete: object error");
|
||||||
undo
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -139,8 +139,8 @@ impl Instance {
|
||||||
short_description: SafeString,
|
short_description: SafeString,
|
||||||
long_description: SafeString,
|
long_description: SafeString,
|
||||||
) {
|
) {
|
||||||
let (sd, _, _) = md_to_html(short_description.as_ref());
|
let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain);
|
||||||
let (ld, _, _) = md_to_html(long_description.as_ref());
|
let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain);
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set((
|
.set((
|
||||||
instances::name.eq(name),
|
instances::name.eq(name),
|
||||||
|
|
|
@ -431,8 +431,8 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> LicensedArticle {
|
pub fn to_activity(&self, conn: &Connection) -> LicensedArticle {
|
||||||
let mut to = self.get_receivers_urls(conn);
|
let cc = self.get_receivers_urls(conn);
|
||||||
to.push(PUBLIC_VISIBILTY.to_string());
|
let to = vec![PUBLIC_VISIBILTY.to_string()];
|
||||||
|
|
||||||
let mut mentions_json = Mention::list_for_post(conn, self.id)
|
let mut mentions_json = Mention::list_for_post(conn, self.id)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -526,7 +526,7 @@ impl Post {
|
||||||
.expect("Post::to_activity: to error");
|
.expect("Post::to_activity: to error");
|
||||||
article
|
article
|
||||||
.object_props
|
.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");
|
.expect("Post::to_activity: cc error");
|
||||||
let mut license = Licensed::default();
|
let mut license = Licensed::default();
|
||||||
license.set_license_string(self.license.clone()).expect("Post::to_activity: license error");
|
license.set_license_string(self.license.clone()).expect("Post::to_activity: license error");
|
||||||
|
@ -627,7 +627,7 @@ impl Post {
|
||||||
post.license = license;
|
post.license = license;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut txt_hashtags = md_to_html(&post.source)
|
let mut txt_hashtags = md_to_html(&post.source, "")
|
||||||
.2
|
.2
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
|
@ -889,7 +889,7 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||||
}
|
}
|
||||||
|
|
||||||
// save mentions and tags
|
// save mentions and tags
|
||||||
let mut hashtags = md_to_html(&post.source)
|
let mut hashtags = md_to_html(&post.source, "")
|
||||||
.2
|
.2
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
|
|
|
@ -555,7 +555,7 @@ impl User {
|
||||||
serde_json::from_str(text).expect("User::fetch_outbox: parsing error");
|
serde_json::from_str(text).expect("User::fetch_outbox: parsing error");
|
||||||
json["items"]
|
json["items"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.expect("Outbox.items is not an array")
|
.unwrap_or(&vec![])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||||
.collect::<Vec<T>>()
|
.collect::<Vec<T>>()
|
||||||
|
@ -587,7 +587,7 @@ impl User {
|
||||||
serde_json::from_str(text).expect("User::fetch_followers_ids: parsing error");
|
serde_json::from_str(text).expect("User::fetch_followers_ids: parsing error");
|
||||||
json["items"]
|
json["items"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.expect("User::fetch_followers_ids: not an array error")
|
.unwrap_or(&vec![])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
|
|
85
src/inbox.rs
85
src/inbox.rs
|
@ -24,7 +24,7 @@ use serde_json;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{Deletable, FromActivity, InboxError},
|
inbox::{Deletable, FromActivity, InboxError, Notify},
|
||||||
Id,request::Digest,
|
Id,request::Digest,
|
||||||
};
|
};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
|
@ -68,7 +68,7 @@ pub trait Inbox {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Follow" => {
|
"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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Like" => {
|
"Like" => {
|
||||||
|
@ -81,44 +81,57 @@ pub trait Inbox {
|
||||||
}
|
}
|
||||||
"Undo" => {
|
"Undo" => {
|
||||||
let act: Undo = serde_json::from_value(act.clone())?;
|
let act: Undo = serde_json::from_value(act.clone())?;
|
||||||
match act.undo_props.object["type"]
|
if let Some(t) = act.undo_props.object["type"].as_str() {
|
||||||
.as_str()
|
match t {
|
||||||
.expect("Inbox::received: undo without original type error")
|
"Like" => {
|
||||||
{
|
likes::Like::delete_id(
|
||||||
"Like" => {
|
&act.undo_props
|
||||||
likes::Like::delete_id(
|
.object_object::<Like>()?
|
||||||
&act.undo_props
|
.object_props
|
||||||
.object_object::<Like>()?
|
.id_string()?,
|
||||||
.object_props
|
actor_id.as_ref(),
|
||||||
.id_string()?,
|
conn,
|
||||||
actor_id.as_ref(),
|
);
|
||||||
conn,
|
Ok(())
|
||||||
);
|
}
|
||||||
Ok(())
|
"Announce" => {
|
||||||
|
Reshare::delete_id(
|
||||||
|
&act.undo_props
|
||||||
|
.object_object::<Announce>()?
|
||||||
|
.object_props
|
||||||
|
.id_string()?,
|
||||||
|
actor_id.as_ref(),
|
||||||
|
conn,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"Follow" => {
|
||||||
|
Follow::delete_id(
|
||||||
|
&act.undo_props
|
||||||
|
.object_object::<FollowAct>()?
|
||||||
|
.object_props
|
||||||
|
.id_string()?,
|
||||||
|
actor_id.as_ref(),
|
||||||
|
conn,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(InboxError::CantUndo)?,
|
||||||
}
|
}
|
||||||
"Announce" => {
|
} else {
|
||||||
Reshare::delete_id(
|
let link = act.undo_props.object.as_str().expect("Inbox::received: undo don't contain type and isn't Link");
|
||||||
&act.undo_props
|
if let Some(like) = likes::Like::find_by_ap_url(conn, link) {
|
||||||
.object_object::<Announce>()?
|
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn);
|
||||||
.object_props
|
|
||||||
.id_string()?,
|
|
||||||
actor_id.as_ref(),
|
|
||||||
conn,
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
} else if let Some(reshare) = Reshare::find_by_ap_url(conn, link) {
|
||||||
"Follow" => {
|
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn);
|
||||||
Follow::delete_id(
|
|
||||||
&act.undo_props
|
|
||||||
.object_object::<FollowAct>()?
|
|
||||||
.object_props
|
|
||||||
.id_string()?,
|
|
||||||
actor_id.as_ref(),
|
|
||||||
conn,
|
|
||||||
);
|
|
||||||
Ok(())
|
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" => {
|
"Update" => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ use plume_models::{
|
||||||
blogs::Blog,
|
blogs::Blog,
|
||||||
comments::*,
|
comments::*,
|
||||||
db_conn::DbConn,
|
db_conn::DbConn,
|
||||||
|
instance::Instance,
|
||||||
mentions::Mention,
|
mentions::Mention,
|
||||||
posts::Post,
|
posts::Post,
|
||||||
safe_string::SafeString,
|
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)?;
|
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
|
||||||
form.validate()
|
form.validate()
|
||||||
.map(|_| {
|
.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 {
|
let comm = Comment::insert(&*conn, NewComment {
|
||||||
content: SafeString::new(html.as_ref()),
|
content: SafeString::new(html.as_ref()),
|
||||||
in_response_to_id: form.responding_to,
|
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 comments = Comment::list_by_post(&*conn, post.id);
|
||||||
|
|
||||||
let previous = form.responding_to.map(|r| Comment::get(&*conn, r)
|
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(
|
Some(render!(posts::details(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||||
|
|
|
@ -201,7 +201,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
|
||||||
// actually it's not "Ok"…
|
// actually it's not "Ok"…
|
||||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _)))
|
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _)))
|
||||||
} else {
|
} 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
|
// update publication date if when this article is no longer a draft
|
||||||
let newly_published = if !post.published && !form.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"…
|
// actually it's not "Ok"…
|
||||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
|
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
|
||||||
} else {
|
} 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 {
|
let post = Post::insert(&*conn, NewPost {
|
||||||
blog_id: blog.id,
|
blog_id: blog.id,
|
||||||
|
|
|
@ -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> {
|
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 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()))))?;
|
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(
|
pub fn ap_followers(
|
||||||
name: String,
|
name: String,
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
|
|
Loading…
Reference in New Issue