From e6b8943085df30ab0339bfaf464df0e429c47c08 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 11:28:42 +0100 Subject: [PATCH 01/27] New pattern for comment creation in code Use the builder design pattern to build a NewComment Add a function to transform a NewComment into a Create activity Virtually send this activity to the shared inbox of the instance --- src/main.rs | 1 + src/models/comments.rs | 99 ++++++++++++++++++++++++++++-------------- src/models/mod.rs | 12 +++++ src/routes/comments.rs | 37 +++++++++------- src/routes/posts.rs | 2 +- src/safe_string.rs | 2 +- 6 files changed, 103 insertions(+), 50 deletions(-) diff --git a/src/main.rs b/src/main.rs index 57ecff6..e041e03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,7 @@ fn main() { routes::comments::new, routes::comments::new_auth, routes::comments::create, + routes::comments::create_response, routes::instance::index, routes::instance::shared_inbox, diff --git a/src/models/comments.rs b/src/models/comments.rs index 5926170..1305709 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -1,6 +1,6 @@ use activitypub::{ activity::Create, - object::{Note, properties::ObjectProperties} + object::Note }; use chrono; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::any}; @@ -12,6 +12,7 @@ use activity_pub::{ inbox::{FromActivity, Notify} }; use models::{ + get_next_id, instance::Instance, notifications::*, posts::Post, @@ -33,7 +34,7 @@ pub struct Comment { pub spoiler_text: String } -#[derive(Insertable)] +#[derive(Insertable, Default)] #[table_name = "comments"] pub struct NewComment { pub content: SafeString, @@ -59,37 +60,6 @@ impl Comment { Post::get(conn, self.post_id).unwrap() } - pub fn into_activity(&self, conn: &PgConnection) -> Note { - let mut to = self.get_author(conn).get_followers(conn).into_iter().map(|f| f.ap_url).collect::>(); - to.append(&mut self.get_post(conn).get_receivers_urls(conn)); - to.push(PUBLIC_VISIBILTY.to_string()); - - let mut comment = Note::default(); - comment.object_props = ObjectProperties { - id: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), - summary: Some(serde_json::to_value(self.spoiler_text.clone()).unwrap()), - content: Some(serde_json::to_value(self.content.clone()).unwrap()), - in_reply_to: Some(serde_json::to_value(self.in_response_to_id.map_or_else(|| self.get_post(conn).ap_url, |id| { - let comm = Comment::get(conn, id).unwrap(); - comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) - })).unwrap()), - published: Some(serde_json::to_value(self.creation_date).unwrap()), - attributed_to: Some(serde_json::to_value(self.get_author(conn).compute_id(conn)).unwrap()), - to: Some(serde_json::to_value(to).unwrap()), - cc: Some(serde_json::to_value(Vec::::new()).unwrap()), - ..ObjectProperties::default() - }; - comment - } - - pub fn create_activity(&self, conn: &PgConnection) -> Create { - let mut act = Create::default(); - act.create_props.set_actor_link(self.get_author(conn).into_id()).unwrap(); - act.create_props.set_object_object(self.into_activity(conn)).unwrap(); - act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).unwrap(); - act - } - pub fn count_local(conn: &PgConnection) -> usize { use schema::users; let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); @@ -149,3 +119,66 @@ impl Notify for Comment { }; } } + +impl NewComment { + pub fn build() -> Self { + NewComment::default() + } + + pub fn content>(mut self, val: T) -> Self { + self.content = SafeString::new(val.as_ref()); + self + } + + pub fn in_response_to_id(mut self, val: Option) -> Self { + self.in_response_to_id = val; + self + } + + pub fn post(mut self, post: Post) -> Self { + self.post_id = post.id; + self + } + + pub fn author(mut self, author: User) -> Self { + self.author_id = author.id; + self + } + + pub fn create(mut self, conn: &PgConnection) -> (Create, i32) { + let post = Post::get(conn, self.post_id).unwrap(); + // We have to manually compute it since the new comment haven't been inserted yet, and it needs the activity we are building to be created + let next_id = get_next_id(conn, "comments_id_seq"); + self.ap_url = Some(format!("{}#comment-{}", post.ap_url, next_id)); + self.sensitive = false; + self.spoiler_text = String::new(); + + let author = User::get(conn, self.author_id).unwrap(); + let mut note = Note::default(); + let mut to = author.get_followers(conn).into_iter().map(User::into_id).collect::>(); + to.append(&mut post + .get_authors(conn) + .into_iter() + .flat_map(|a| a.get_followers(conn)) + .map(User::into_id) + .collect::>()); + to.push(Id::new(PUBLIC_VISIBILTY.to_string())); + + note.object_props.set_id_string(self.ap_url.clone().unwrap_or(String::new())).expect("NewComment::create: note.id error"); + note.object_props.set_summary_string(self.spoiler_text.clone()).expect("NewComment::create: note.summary error"); + note.object_props.set_content_string(self.content.get().clone()).expect("NewComment::create: note.content error"); + note.object_props.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(|| Post::get(conn, self.post_id).unwrap().ap_url, |id| { + let comm = Comment::get(conn, id).unwrap(); + comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) + }))).expect("NewComment::create: note.in_reply_to error"); + note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("NewComment::create: note.published error"); + note.object_props.set_attributed_to_link(author.clone().into_id()).expect("NewComment::create: note.attributed_to error"); + note.object_props.set_to_link_vec(to).expect("NewComment::create: note.to error"); + + let mut act = Create::default(); + act.create_props.set_actor_link(author.into_id()).expect("NewComment::create: actor error"); + act.create_props.set_object_object(note).expect("NewComment::create: object error"); + act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).expect("NewComment::create: id error"); + (act, next_id) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index d0f01f3..fa120b1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,5 @@ +use diesel::{PgConnection, RunQueryDsl, select}; + macro_rules! find_by { ($table:ident, $fn:ident, $($col:ident as $type:ident),+) => { /// Try to find a $table with a given $col @@ -35,6 +37,16 @@ macro_rules! insert { }; } +sql_function!(nextval, nextval_t, (seq: ::diesel::sql_types::Text) -> ::diesel::sql_types::BigInt); +sql_function!(setval, setval_t, (seq: ::diesel::sql_types::Text, val: ::diesel::sql_types::BigInt) -> ::diesel::sql_types::BigInt); + +fn get_next_id(conn: &PgConnection, seq: &str) -> i32 { + // We cant' use currval because it may fail if nextval have never been called before + let next = select(nextval(seq)).get_result::(conn).expect("Next ID fail"); + select(setval(seq, next - 1)).get_result::(conn).expect("Reset ID fail"); + next as i32 +} + pub mod blog_authors; pub mod blogs; pub mod comments; diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 49e678b..a2eddaf 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -3,18 +3,19 @@ use rocket::{ response::{Redirect, Flash} }; use rocket_contrib::Template; +use serde_json; -use activity_pub::{broadcast, IntoId, inbox::Notify}; +use activity_pub::{broadcast, inbox::Inbox}; use db_conn::DbConn; use models::{ blogs::Blog, comments::*, + instance::Instance, posts::Post, users::User }; use utils; -use safe_string::SafeString; #[get("/~///comment")] fn new(blog: String, slug: String, user: User, conn: DbConn) -> Template { @@ -43,23 +44,29 @@ struct NewCommentForm { pub content: String } +// See: https://github.com/SergioBenitez/Rocket/pull/454 +#[post("/~///comment", data = "")] +fn create(blog_name: String, slug: String, data: Form, user: User, conn: DbConn) -> Redirect { + create_response(blog_name, slug, None, data, user, conn) +} + #[post("/~///comment?", data = "")] -fn create(blog_name: String, slug: String, query: CommentQuery, data: Form, user: User, conn: DbConn) -> Redirect { +fn create_response(blog_name: String, slug: String, query: Option, data: Form, user: User, conn: DbConn) -> Redirect { let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let form = data.get(); - let comment = Comment::insert(&*conn, NewComment { - content: SafeString::new(&form.content.clone()), - in_response_to_id: query.responding_to, - post_id: post.id, - author_id: user.id, - ap_url: None, - sensitive: false, - spoiler_text: "".to_string() - }); - Comment::notify(&*conn, comment.into_activity(&*conn), user.clone().into_id()); - broadcast(&*conn, &user, comment.create_activity(&*conn), user.get_followers(&*conn)); + let (new_comment, id) = NewComment::build() + .content(form.content.clone()) + .in_response_to_id(query.and_then(|q| q.responding_to)) + .post(post) + .author(user.clone()) + .create(&*conn); - Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, comment.id)) + // Comment::notify(&*conn, new_comment, user.clone().into_id()); + let instance = Instance::get_local(&*conn).unwrap(); + instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")); + broadcast(&*conn, &user, new_comment, user.get_followers(&*conn)); + + Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) } diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 5cc3262..f79256d 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -21,7 +21,7 @@ use safe_string::SafeString; fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Template { may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { - let comments = Comment::find_by_post(&*conn, post.id); + let comments = Comment::list_by_post(&*conn, post.id); Template::render("posts/details", json!({ "author": post.get_authors(&*conn)[0].to_json(&*conn), diff --git a/src/safe_string.rs b/src/safe_string.rs index 3b17a6e..9889743 100644 --- a/src/safe_string.rs +++ b/src/safe_string.rs @@ -9,7 +9,7 @@ use diesel::{self, deserialize::Queryable, sql_types::Text, serialize::{self, Output}}; -#[derive(Debug,Clone,AsExpression,FromSqlRow)] +#[derive(Debug, Clone, AsExpression, FromSqlRow, Default)] #[sql_type = "Text"] pub struct SafeString{ value: String, From 5a5c1a8d994e0b76618c863f3a5af361978e2470 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 11:38:07 +0100 Subject: [PATCH 02/27] List all comments --- src/models/comments.rs | 4 ++-- src/routes/comments.rs | 17 +---------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/models/comments.rs b/src/models/comments.rs index ae6bcf3..2829b70 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -1,7 +1,7 @@ use activitypub::{ activity::Create, link, - object::{Note, properties::ObjectProperties} + object::{Note} }; use chrono; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::any}; @@ -51,7 +51,7 @@ pub struct NewComment { impl Comment { insert!(comments, NewComment); get!(comments); - find_by!(comments, find_by_post, post_id as i32); + list_by!(comments, list_by_post, post_id as i32); find_by!(comments, find_by_ap_url, ap_url as String); pub fn get_author(&self, conn: &PgConnection) -> User { diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 171701a..a2eddaf 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -5,7 +5,7 @@ use rocket::{ use rocket_contrib::Template; use serde_json; -use activity_pub::{broadcast, inbox::Inbox, inbox::Notify}; +use activity_pub::{broadcast, inbox::Inbox}; use db_conn::DbConn; use models::{ blogs::Blog, @@ -55,7 +55,6 @@ fn create_response(blog_name: String, slug: String, query: Option, let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let form = data.get(); -<<<<<<< HEAD let (new_comment, id) = NewComment::build() .content(form.content.clone()) @@ -68,20 +67,6 @@ fn create_response(blog_name: String, slug: String, query: Option, let instance = Instance::get_local(&*conn).unwrap(); instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")); broadcast(&*conn, &user, new_comment, user.get_followers(&*conn)); -======= - let comment = Comment::insert(&*conn, NewComment { - content: SafeString::new(&form.content.clone()), - in_response_to_id: query.responding_to, - post_id: post.id, - author_id: user.id, - ap_url: None, // TODO: set it - sensitive: false, - spoiler_text: "".to_string() - }); - comment.notify(&*conn); - - broadcast(&*conn, &user, comment.create_activity(&*conn), user.get_followers(&*conn)); ->>>>>>> dbdcbe71049e181c1c7649169c0153b3c9d81ad8 Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) } From 0fd181e7ea2d601abc26c9c8855d587d529f6cc0 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 12:00:30 +0100 Subject: [PATCH 03/27] Add support for markdown in comments + Correctly federate mentions in comments --- src/models/comments.rs | 7 ++++++- src/models/mentions.rs | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/models/comments.rs b/src/models/comments.rs index 2829b70..8e30e35 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -22,6 +22,7 @@ use models::{ }; use schema::comments; use safe_string::SafeString; +use utils; #[derive(Queryable, Identifiable, Serialize, Clone)] pub struct Comment { @@ -159,6 +160,8 @@ impl NewComment { self.sensitive = false; self.spoiler_text = String::new(); + let (html, mentions) = utils::md_to_html(self.content.get().as_ref()); + let author = User::get(conn, self.author_id).unwrap(); let mut note = Note::default(); let mut to = author.get_followers(conn).into_iter().map(User::into_id).collect::>(); @@ -172,7 +175,7 @@ impl NewComment { note.object_props.set_id_string(self.ap_url.clone().unwrap_or(String::new())).expect("NewComment::create: note.id error"); note.object_props.set_summary_string(self.spoiler_text.clone()).expect("NewComment::create: note.summary error"); - note.object_props.set_content_string(self.content.get().clone()).expect("NewComment::create: note.content error"); + note.object_props.set_content_string(html).expect("NewComment::create: note.content error"); note.object_props.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(|| Post::get(conn, self.post_id).unwrap().ap_url, |id| { let comm = Comment::get(conn, id).unwrap(); comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) @@ -180,6 +183,8 @@ impl NewComment { note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("NewComment::create: note.published error"); note.object_props.set_attributed_to_link(author.clone().into_id()).expect("NewComment::create: note.attributed_to error"); note.object_props.set_to_link_vec(to).expect("NewComment::create: note.to error"); + note.object_props.set_tag_link_vec(mentions.into_iter().map(|m| Mention::build_activity(conn, m)).collect::>()) + .expect("NewComment::create: note.tag error"); let mut act = Create::default(); act.create_props.set_actor_link(author.into_id()).expect("NewComment::create: actor error"); diff --git a/src/models/mentions.rs b/src/models/mentions.rs index fe4fe90..7d0fe57 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -49,7 +49,6 @@ impl Mention { pub fn build_activity(conn: &PgConnection, ment: String) -> link::Mention { let user = User::find_by_fqn(conn, ment.clone()); - println!("building act : {} -> {:?}", ment, user); let mut mention = link::Mention::default(); mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Error setting mention's href"); mention.link_props.set_name_string(format!("@{}", ment)).expect("Error setting mention's name"); From 7ba6f77e0f9b2bee87bc6ebad7cd28c6041e5d24 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 14:05:35 +0100 Subject: [PATCH 04/27] Automatically insert mentions in comments Fix some bug with mentions too Fix #52 --- src/main.rs | 1 + src/models/comments.rs | 24 ++++++++++++------- src/models/mentions.rs | 41 ++++++++++++++++---------------- src/models/posts.rs | 23 +++++++++--------- src/routes/comments.rs | 14 ++++++++--- src/routes/posts.rs | 4 ++-- templates/comments/new.html.tera | 3 ++- 7 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/main.rs b/src/main.rs index 51390e2..786d2a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ fn main() { routes::blogs::create, routes::comments::new, + routes::comments::new_response, routes::comments::new_auth, routes::comments::create, routes::comments::create_response, diff --git a/src/models/comments.rs b/src/models/comments.rs index 8e30e35..326567d 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -75,6 +75,11 @@ impl Comment { pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { let mut json = serde_json::to_value(self).unwrap(); json["author"] = self.get_author(conn).to_json(conn); + let mentions = Mention::list_for_comment(conn, self.id).into_iter() + .map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new())) + .collect::>(); + println!("{:?}", mentions); + json["mentions"] = serde_json::to_value(mentions).unwrap(); json } @@ -88,15 +93,6 @@ impl FromActivity for Comment { let previous_url = note.object_props.in_reply_to.clone().unwrap().as_str().unwrap().to_string(); let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone()); - // save mentions - if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { - for tag in tags.into_iter() { - serde_json::from_value::(tag) - .map(|m| Mention::from_activity(conn, m, Id::new(note.clone().object_props.clone().url_string().unwrap_or(String::from(""))))) - .ok(); - } - } - let comm = Comment::insert(conn, NewComment { content: SafeString::new(¬e.object_props.content_string().unwrap()), spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), @@ -108,6 +104,16 @@ impl FromActivity for Comment { author_id: User::from_url(conn, actor.clone().into()).unwrap().id, sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate }); + + // save mentions + if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { + for tag in tags.into_iter() { + serde_json::from_value::(tag) + .map(|m| Mention::from_activity(conn, m, comm.id, false)) + .ok(); + } + } + comm.notify(conn); comm } diff --git a/src/models/mentions.rs b/src/models/mentions.rs index 7d0fe57..45b8abb 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -1,7 +1,7 @@ use activitypub::link; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; -use activity_pub::{Id, inbox::Notify}; +use activity_pub::inbox::Notify; use models::{ comments::Comment, notifications::*, @@ -10,13 +10,13 @@ use models::{ }; use schema::mentions; -#[derive(Queryable, Identifiable)] +#[derive(Queryable, Identifiable, Serialize, Deserialize)] pub struct Mention { pub id: i32, pub mentioned_id: i32, pub post_id: Option, pub comment_id: Option, - pub ap_url: String + pub ap_url: String // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake } #[derive(Insertable)] @@ -34,6 +34,7 @@ impl Mention { find_by!(mentions, find_by_ap_url, ap_url as String); list_by!(mentions, list_for_user, mentioned_id as i32); list_by!(mentions, list_for_post, post_id as i32); + list_by!(mentions, list_for_comment, comment_id as i32); pub fn get_mentioned(&self, conn: &PgConnection) -> Option { User::get(conn, self.mentioned_id) @@ -44,7 +45,7 @@ impl Mention { } pub fn get_comment(&self, conn: &PgConnection) -> Option { - self.post_id.and_then(|id| Comment::get(conn, id)) + self.comment_id.and_then(|id| Comment::get(conn, id)) } pub fn build_activity(conn: &PgConnection, ment: String) -> link::Mention { @@ -63,21 +64,23 @@ impl Mention { mention } - pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: Id) -> Option { + pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: i32, in_post: bool) -> Option { let ap_url = ment.link_props.href_string().unwrap(); let mentioned = User::find_by_ap_url(conn, ap_url).unwrap(); - if let Some(post) = Post::find_by_ap_url(conn, inside.clone().into()) { - let res = Mention::insert(conn, NewMention { - mentioned_id: mentioned.id, - post_id: Some(post.id), - comment_id: None, - ap_url: ment.link_props.href_string().unwrap_or(String::new()) - }); - res.notify(conn); - Some(res) + if in_post { + Post::get(conn, inside.clone().into()).map(|post| { + let res = Mention::insert(conn, NewMention { + mentioned_id: mentioned.id, + post_id: Some(post.id), + comment_id: None, + ap_url: ment.link_props.href_string().unwrap_or(String::new()) + }); + res.notify(conn); + res + }) } else { - if let Some(comment) = Comment::find_by_ap_url(conn, inside.into()) { + Comment::get(conn, inside.into()).map(|comment| { let res = Mention::insert(conn, NewMention { mentioned_id: mentioned.id, post_id: None, @@ -85,10 +88,8 @@ impl Mention { ap_url: ment.link_props.href_string().unwrap_or(String::new()) }); res.notify(conn); - Some(res) - } else { - None - } + res + }) } } } @@ -97,7 +98,7 @@ impl Notify for Mention { fn notify(&self, conn: &PgConnection) { let author = self.get_comment(conn) .map(|c| c.get_author(conn).display_name.clone()) - .unwrap_or(self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone()); + .unwrap_or_else(|| self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone()); self.get_mentioned(conn).map(|m| { Notification::insert(conn, NewNotification { diff --git a/src/models/posts.rs b/src/models/posts.rs index 368d283..bf0e7fe 100644 --- a/src/models/posts.rs +++ b/src/models/posts.rs @@ -187,16 +187,7 @@ impl Post { impl FromActivity
for Post { fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post { - // save mentions - if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() { - for tag in tags.into_iter() { - serde_json::from_value::(tag) - .map(|m| Mention::from_activity(conn, m, Id::new(article.clone().object_props.clone().url_string().unwrap_or(String::from(""))))) - .ok(); - } - } - - Post::insert(conn, NewPost { + let post = Post::insert(conn, NewPost { blog_id: 0, // TODO slug: String::from(""), // TODO title: article.object_props.name_string().unwrap(), @@ -204,7 +195,17 @@ impl FromActivity
for Post { published: true, license: String::from("CC-0"), ap_url: article.object_props.url_string().unwrap_or(String::from("")) - }) + }); + + // save mentions + if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() { + for tag in tags.into_iter() { + serde_json::from_value::(tag) + .map(|m| Mention::from_activity(conn, m, post.id, true)) + .ok(); + } + } + post } } diff --git a/src/routes/comments.rs b/src/routes/comments.rs index a2eddaf..ca40a3a 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -19,17 +19,25 @@ use utils; #[get("/~///comment")] fn new(blog: String, slug: String, user: User, conn: DbConn) -> Template { - may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { + new_response(blog, slug, None, user, conn) +} + +// See: https://github.com/SergioBenitez/Rocket/pull/454 +#[get("/~///comment?")] +fn new_response(blog_name: String, slug: String, query: Option, user: User, conn: DbConn) -> Template { + may_fail!(Blog::find_by_fqn(&*conn, blog_name), "Couldn't find this blog", |blog| { may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { Template::render("comments/new", json!({ "post": post, - "account": user + "account": user, + "previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn))), + "user_fqn": user.get_fqn(&*conn) })) }) }) } -#[get("/~///comment", rank=2)] +#[get("/~///comment", rank = 2)] fn new_auth(blog: String, slug: String) -> Flash{ utils::requires_login("You need to be logged in order to post a comment", uri!(new: blog = blog, slug = slug)) } diff --git a/src/routes/posts.rs b/src/routes/posts.rs index d06f609..fb8c990 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -4,7 +4,7 @@ use rocket::response::{Redirect, Flash}; use rocket_contrib::Template; use serde_json; -use activity_pub::{broadcast, context, activity_pub, ActivityPub, Id}; +use activity_pub::{broadcast, context, activity_pub, ActivityPub}; use db_conn::DbConn; use models::{ blogs::*, @@ -106,7 +106,7 @@ fn create(blog_name: String, data: Form, user: User, conn: DbConn) }); for m in mentions.into_iter() { - Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), Id::new(post.compute_id(&*conn))); + Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), post.id, true); } let act = post.create_activity(&*conn); diff --git a/templates/comments/new.html.tera b/templates/comments/new.html.tera index a02de4a..4c1bb83 100644 --- a/templates/comments/new.html.tera +++ b/templates/comments/new.html.tera @@ -8,7 +8,8 @@

{{ 'Comment "{{ post }}"' | _(post=post.title) }}

- + {# Ugly, but we don't have the choice if we don't want weird paddings #} +
{% endblock content %} From 461c26f1594e3ef6b75ad66d67a45f82d3dc2d77 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 14:40:00 +0100 Subject: [PATCH 05/27] Move comment form to the post page --- po/plume.pot | 3 +++ templates/posts/details.html.tera | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/po/plume.pot b/po/plume.pot index 683ee16..64032f4 100644 --- a/po/plume.pot +++ b/po/plume.pot @@ -278,3 +278,6 @@ msgstr "" msgid "{{ data }} mentioned you." msgstr "" + +msgid "Your comment" +msgstr "" diff --git a/templates/posts/details.html.tera b/templates/posts/details.html.tera index 097ff3a..63a93f6 100644 --- a/templates/posts/details.html.tera +++ b/templates/posts/details.html.tera @@ -60,7 +60,16 @@

{{ "Comments" | _ }}

- {{ "Comment" | _ }} + + {% if account %} +
+ + {# Ugly, but we don't have the choice if we don't want weird paddings #} + + +
+ {% endif %} +
{% for comment in comments %} {% if comment.author.display_name %} From 4c211b4308d718e468ed6b3444f5a152400b746b Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 15:00:25 +0100 Subject: [PATCH 06/27] Remove the routes and the template for the comment form --- po/en.po | 3 +++ po/fr.po | 4 ++++ po/pl.po | 4 ++++ src/main.rs | 4 +--- src/routes/comments.rs | 35 +++---------------------------- src/routes/posts.rs | 11 +++++++++- templates/comments/new.html.tera | 16 -------------- templates/posts/details.html.tera | 2 +- 8 files changed, 26 insertions(+), 53 deletions(-) delete mode 100644 templates/comments/new.html.tera diff --git a/po/en.po b/po/en.po index 67cd876..80ceeab 100644 --- a/po/en.po +++ b/po/en.po @@ -283,3 +283,6 @@ msgstr "" msgid "{{ data }} mentioned you." msgstr "" + +msgid "Your comment" +msgstr "" diff --git a/po/fr.po b/po/fr.po index 79f89a3..a38c24e 100644 --- a/po/fr.po +++ b/po/fr.po @@ -282,3 +282,7 @@ msgstr "Vous n'êtes pas auteur dans ce blog." msgid "{{ data }} mentioned you." msgstr "" + +#, fuzzy +msgid "Your comment" +msgstr "Envoyer le commentaire" diff --git a/po/pl.po b/po/pl.po index 440fa54..5f0d98e 100644 --- a/po/pl.po +++ b/po/pl.po @@ -289,5 +289,9 @@ msgstr "" msgid "{{ data }} mentioned you." msgstr "{{ data }} skomentował Twój artykuł" +#, fuzzy +msgid "Your comment" +msgstr "Wyślij komentarz" + #~ msgid "Logowanie" #~ msgstr "Zaloguj się" diff --git a/src/main.rs b/src/main.rs index 786d2a8..3a42db2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,9 +68,6 @@ fn main() { routes::blogs::new_auth, routes::blogs::create, - routes::comments::new, - routes::comments::new_response, - routes::comments::new_auth, routes::comments::create, routes::comments::create_response, @@ -85,6 +82,7 @@ fn main() { routes::notifications::notifications_auth, routes::posts::details, + routes::posts::details_response, routes::posts::activity_details, routes::posts::new, routes::posts::new_auth, diff --git a/src/routes/comments.rs b/src/routes/comments.rs index ca40a3a..d54403c 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -1,8 +1,7 @@ use rocket::{ request::Form, - response::{Redirect, Flash} + response::Redirect }; -use rocket_contrib::Template; use serde_json; use activity_pub::{broadcast, inbox::Inbox}; @@ -15,36 +14,9 @@ use models::{ users::User }; -use utils; - -#[get("/~///comment")] -fn new(blog: String, slug: String, user: User, conn: DbConn) -> Template { - new_response(blog, slug, None, user, conn) -} - -// See: https://github.com/SergioBenitez/Rocket/pull/454 -#[get("/~///comment?")] -fn new_response(blog_name: String, slug: String, query: Option, user: User, conn: DbConn) -> Template { - may_fail!(Blog::find_by_fqn(&*conn, blog_name), "Couldn't find this blog", |blog| { - may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { - Template::render("comments/new", json!({ - "post": post, - "account": user, - "previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn))), - "user_fqn": user.get_fqn(&*conn) - })) - }) - }) -} - -#[get("/~///comment", rank = 2)] -fn new_auth(blog: String, slug: String) -> Flash{ - utils::requires_login("You need to be logged in order to post a comment", uri!(new: blog = blog, slug = slug)) -} - #[derive(FromForm)] -struct CommentQuery { - responding_to: Option +pub struct CommentQuery { + pub responding_to: Option } #[derive(FromForm)] @@ -71,7 +43,6 @@ fn create_response(blog_name: String, slug: String, query: Option, .author(user.clone()) .create(&*conn); - // Comment::notify(&*conn, new_comment, user.clone().into_id()); let instance = Instance::get_local(&*conn).unwrap(); instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")); broadcast(&*conn, &user, new_comment, user.get_followers(&*conn)); diff --git a/src/routes/posts.rs b/src/routes/posts.rs index fb8c990..7f5007e 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -14,11 +14,18 @@ use models::{ posts::*, users::User }; +use routes::comments::CommentQuery; use safe_string::SafeString; use utils; +// See: https://github.com/SergioBenitez/Rocket/pull/454 #[get("/~//", rank = 4)] fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Template { + details_response(blog, slug, conn, user, None) +} + +#[get("/~//?")] +fn details_response(blog: String, slug: String, conn: DbConn, user: Option, query: Option) -> Template { may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { let comments = Comment::list_by_post(&*conn, post.id); @@ -33,7 +40,9 @@ fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Temp "n_reshares": post.get_reshares(&*conn).len(), "has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false), "account": user, - "date": &post.creation_date.timestamp() + "date": &post.creation_date.timestamp(), + "previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn))), + "user_fqn": user.map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()) })) }) }) diff --git a/templates/comments/new.html.tera b/templates/comments/new.html.tera deleted file mode 100644 index 4c1bb83..0000000 --- a/templates/comments/new.html.tera +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "base" %} - -{% block title %} -{{ 'Comment "{{ post }}"' | _(post=post.title) }} -{% endblock title %} - -{% block content %} -

{{ 'Comment "{{ post }}"' | _(post=post.title) }}

-
- - {# Ugly, but we don't have the choice if we don't want weird paddings #} - - -
-{% endblock content %} - diff --git a/templates/posts/details.html.tera b/templates/posts/details.html.tera index 63a93f6..5f4be6c 100644 --- a/templates/posts/details.html.tera +++ b/templates/posts/details.html.tera @@ -84,7 +84,7 @@ @{{ comment.author.username }}
{{ comment.content | safe }}
- {{ "Respond" | _ }} + {{ "Respond" | _ }}
{% endfor %}
From 514689cfc11b48eb4dd52b349a5a218ce42b71b5 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 15:33:28 +0100 Subject: [PATCH 07/27] Try to improve the style of forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one to write a new comment is still ugly… --- static/main.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/main.css b/static/main.css index b141505..2f033c3 100644 --- a/static/main.css +++ b/static/main.css @@ -257,7 +257,7 @@ input { transition: all 0.1s ease-in; display: block; width: 100%; - margin: auto; + margin: auto auto 5em; padding: 0.5em; box-sizing: border-box; @@ -266,7 +266,7 @@ input { border: none; border-bottom: solid #DADADA 2px; } -input[type="submit"] { margin: 2em auto; } +form input[type="submit"] { margin: 2em auto; } input:focus { background: #FAFAFA; border-bottom-color: #7765E3; From 5457a80eecf2e7c80e8c0c6e6c980b0c7f1a8b78 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 15:48:54 +0100 Subject: [PATCH 08/27] Avoid calling compute_id when we shouldn't It should only be used at initialization, after we should prefer ap_url, as not everybody is using the same URLs as Plume. --- src/models/blogs.rs | 10 +++++----- src/models/comments.rs | 2 +- src/models/likes.rs | 4 ++-- src/models/posts.rs | 2 +- src/models/reshares.rs | 4 ++-- src/models/users.rs | 16 ++++++++-------- src/routes/user.rs | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/models/blogs.rs b/src/models/blogs.rs index dc779f6..eb2d3ab 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -175,12 +175,12 @@ impl Blog { pub fn webfinger(&self, conn: &PgConnection) -> Webfinger { Webfinger { subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain), - aliases: vec![self.compute_id(conn)], + aliases: vec![self.ap_url.clone()], links: vec![ Link { rel: String::from("http://webfinger.net/rel/profile-page"), mime_type: None, - href: self.compute_id(conn) + href: self.ap_url.clone() }, Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), @@ -190,7 +190,7 @@ impl Blog { Link { rel: String::from("self"), mime_type: Some(String::from("application/activity+json")), - href: self.compute_id(conn) + href: self.ap_url.clone() } ] } @@ -259,8 +259,8 @@ impl APActor for Blog { } impl sign::Signer for Blog { - fn get_key_id(&self, conn: &PgConnection) -> String { - format!("{}#main-key", self.compute_id(conn)) + fn get_key_id(&self, _conn: &PgConnection) -> String { + format!("{}#main-key", self.ap_url) } fn sign(&self, to_sign: String) -> Vec { diff --git a/src/models/comments.rs b/src/models/comments.rs index 326567d..9deaa1a 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -84,7 +84,7 @@ impl Comment { } pub fn compute_id(&self, conn: &PgConnection) -> String { - ap_url(format!("{}#comment-{}", self.get_post(conn).compute_id(conn), self.id)) + ap_url(format!("{}#comment-{}", self.get_post(conn).ap_url, self.id)) } } diff --git a/src/models/likes.rs b/src/models/likes.rs index 38a4a19..0f18b10 100644 --- a/src/models/likes.rs +++ b/src/models/likes.rs @@ -70,8 +70,8 @@ impl Like { pub fn compute_id(&self, conn: &PgConnection) -> String { format!( "{}/like/{}", - User::get(conn, self.user_id).unwrap().compute_id(conn), - Post::get(conn, self.post_id).unwrap().compute_id(conn) + User::get(conn, self.user_id).unwrap().ap_url, + Post::get(conn, self.post_id).unwrap().ap_url ) } } diff --git a/src/models/posts.rs b/src/models/posts.rs index bf0e7fe..9907ac5 100644 --- a/src/models/posts.rs +++ b/src/models/posts.rs @@ -155,7 +155,7 @@ impl Post { content: Some(serde_json::to_value(self.content.clone()).unwrap()), published: Some(serde_json::to_value(self.creation_date).unwrap()), tag: Some(serde_json::to_value(mentions).unwrap()), - url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + url: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), to: Some(serde_json::to_value(to).unwrap()), cc: Some(serde_json::to_value(Vec::::new()).unwrap()), ..ObjectProperties::default() diff --git a/src/models/reshares.rs b/src/models/reshares.rs index 40a210d..bc29f4f 100644 --- a/src/models/reshares.rs +++ b/src/models/reshares.rs @@ -34,8 +34,8 @@ impl Reshare { diesel::update(self) .set(reshares::ap_url.eq(format!( "{}/reshare/{}", - User::get(conn, self.user_id).unwrap().compute_id(conn), - Post::get(conn, self.post_id).unwrap().compute_id(conn) + User::get(conn, self.user_id).unwrap().ap_url, + Post::get(conn, self.post_id).unwrap().ap_url ))) .get_result::(conn).expect("Couldn't update AP URL"); } diff --git a/src/models/users.rs b/src/models/users.rs index ee7639c..ef5d83f 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -309,10 +309,10 @@ impl User { pub fn into_activity(&self, conn: &PgConnection) -> Person { let mut actor = Person::default(); actor.object_props = ObjectProperties { - id: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + id: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), name: Some(serde_json::to_value(self.get_display_name()).unwrap()), summary: Some(serde_json::to_value(self.get_summary()).unwrap()), - url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + url: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), ..ObjectProperties::default() }; actor.ap_actor_props = ApActorProperties { @@ -339,12 +339,12 @@ impl User { pub fn webfinger(&self, conn: &PgConnection) -> Webfinger { Webfinger { subject: format!("acct:{}@{}", self.username, self.get_instance(conn).public_domain), - aliases: vec![self.compute_id(conn)], + aliases: vec![self.ap_url.clone()], links: vec![ Link { rel: String::from("http://webfinger.net/rel/profile-page"), mime_type: None, - href: self.compute_id(conn) + href: self.ap_url.clone() }, Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), @@ -354,7 +354,7 @@ impl User { Link { rel: String::from("self"), mime_type: Some(String::from("application/activity+json")), - href: self.compute_id(conn) + href: self.ap_url.clone() } ] } @@ -411,7 +411,7 @@ impl APActor for User { let mut res = serde_json::Map::new(); res.insert("publicKey".to_string(), json!({ "id": self.get_key_id(conn), - "owner": self.compute_id(conn), + "owner": self.ap_url, "publicKeyPem": self.public_key })); res.insert("followers".to_string(), serde_json::Value::String(self.compute_box(conn, "followers"))); @@ -461,8 +461,8 @@ impl Inbox for User { } impl Signer for User { - fn get_key_id(&self, conn: &PgConnection) -> String { - format!("{}#main-key", self.compute_id(conn)) + fn get_key_id(&self, _conn: &PgConnection) -> String { + format!("{}#main-key", self.ap_url) } fn sign(&self, to_sign: String) -> Vec { diff --git a/src/routes/user.rs b/src/routes/user.rs index da78cf6..e49df65 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -206,7 +206,7 @@ fn inbox(name: String, conn: DbConn, data: String) -> String { #[get("/@//followers", format = "application/activity+json")] fn ap_followers(name: String, conn: DbConn) -> ActivityPub { let user = User::find_local(&*conn, name).unwrap(); - let followers = user.get_followers(&*conn).into_iter().map(|f| f.compute_id(&*conn)).collect::>(); + let followers = user.get_followers(&*conn).into_iter().map(|f| f.ap_url).collect::>(); let json = json!({ "@context": context(), From 2621c0304ec252379cb9b3ef3f204f3993bed663 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 16:14:26 +0100 Subject: [PATCH 09/27] Better use of the activitypub crate in User::into_activity --- src/models/users.rs | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/models/users.rs b/src/models/users.rs index ef5d83f..0f99abb 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,8 +1,7 @@ use activitypub::{ - Actor, Object, - actor::{Person, properties::ApActorProperties}, - collection::OrderedCollection, - object::properties::ObjectProperties + Actor, Object, Endpoint, + actor::Person, + collection::OrderedCollection }; use bcrypt; use chrono::NaiveDateTime; @@ -306,27 +305,19 @@ impl User { PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() } - pub fn into_activity(&self, conn: &PgConnection) -> Person { + pub fn into_activity(&self, _conn: &PgConnection) -> Person { let mut actor = Person::default(); - actor.object_props = ObjectProperties { - id: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), - name: Some(serde_json::to_value(self.get_display_name()).unwrap()), - summary: Some(serde_json::to_value(self.get_summary()).unwrap()), - url: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), - ..ObjectProperties::default() - }; - actor.ap_actor_props = ApActorProperties { - inbox: serde_json::to_value(self.compute_inbox(conn)).unwrap(), - outbox: serde_json::to_value(self.compute_outbox(conn)).unwrap(), - preferred_username: Some(serde_json::to_value(self.get_actor_id()).unwrap()), - endpoints: Some(json!({ - "sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str())) - })), - followers: None, - following: None, - liked: None, - streams: None - }; + actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error"); + actor.object_props.set_name_string(self.get_display_name()).expect("User::into_activity: name error"); + actor.object_props.set_summary_string(self.get_summary()).expect("User::into_activity: summary error"); + actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error"); + actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error"); + actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error"); + actor.ap_actor_props.set_preferred_username_string(self.get_actor_id()).expect("User::into_activity: preferredUsername error"); + + let mut endpoints = Endpoint::default(); + endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error"); + actor.ap_actor_props.set_endpoints_endpoint(endpoints).expect("User::into_activity: endpoints error"); actor } From c0a946cbcb4bbe4be2869412e02044b2cff570ff Mon Sep 17 00:00:00 2001 From: Banjo Fox Date: Thu, 21 Jun 2018 11:24:50 -0400 Subject: [PATCH 10/27] Adding "Linux" Adding a Linux heading as that will cover most other installations. Although it should be noted that the Prerequisite Installation it references will also include MS Windows --- DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 391f3a7..fbd9db8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -6,6 +6,10 @@ All commands are run in the Mac Terminal or terminal emulator of your choice, such as iTerm2. First, you will need [Git](https://git-scm.com/download/mac), [Homebrew](https://brew.sh/), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Follow the instructions to install Homebrew before continuing if you don't already have it. +### Linux + +Similar to Mac OSX all commands should be run from a terminal (a.k.a command line). First, you will need [Git](https://git-scm.com/download/mac), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Step-by-step instructions are also available here: [Installing Prerequisites](No Such URL yet) + #### Download the Repository Navigate to the directory on your machine where you would like to install the repository, such as in `~/dev` by running `cd dev`. Now, clone the remote repository by running `git clone https://github.com/Plume-org/Plume.git`. This will install the codebase to the `Plume` subdirectory. Navigate into that directory by running `cd Plume`. From 7ddad124202aa93855e6fc11d4aed532fb49849b Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 16:25:32 +0100 Subject: [PATCH 11/27] Simplify the signature of activity_pub::sign::Signer::get_key_id --- src/activity_pub/sign.rs | 2 +- src/models/blogs.rs | 2 +- src/models/users.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/activity_pub/sign.rs b/src/activity_pub/sign.rs index cdc4c74..6ad6275 100644 --- a/src/activity_pub/sign.rs +++ b/src/activity_pub/sign.rs @@ -17,7 +17,7 @@ pub fn gen_keypair() -> (Vec, Vec) { } pub trait Signer { - fn get_key_id(&self, conn: &PgConnection) -> String; + fn get_key_id(&self) -> String; /// Sign some data with the signer keypair fn sign(&self, to_sign: String) -> Vec; diff --git a/src/models/blogs.rs b/src/models/blogs.rs index eb2d3ab..ea3d1be 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -259,7 +259,7 @@ impl APActor for Blog { } impl sign::Signer for Blog { - fn get_key_id(&self, _conn: &PgConnection) -> String { + fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) } diff --git a/src/models/users.rs b/src/models/users.rs index 0f99abb..63d5aec 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -452,7 +452,7 @@ impl Inbox for User { } impl Signer for User { - fn get_key_id(&self, _conn: &PgConnection) -> String { + fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) } From 08f02f47bd25203f66f9f9882f3114746a914d5f Mon Sep 17 00:00:00 2001 From: Banjo Fox Date: Thu, 21 Jun 2018 11:30:03 -0400 Subject: [PATCH 12/27] Create PREREQUISITES.md --- doc/PREREQUISITES.md | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 doc/PREREQUISITES.md diff --git a/doc/PREREQUISITES.md b/doc/PREREQUISITES.md new file mode 100644 index 0000000..5d0b874 --- /dev/null +++ b/doc/PREREQUISITES.md @@ -0,0 +1,85 @@ +# Installing Software Prerequisites + +These instructions have been adapted from the Aardwolf documentation, and may not be accurate. +As such, this notification should be updated once verified for Plume installs. + +> NOTE: These instructions may help in installing a production version, but are +intended for developers to be able to build and test their changes. If in doubt, +seek out documentation from your distribution package or from [the `doc` folder](doc). + +## Installing Requirements + +### Installing PostgreSQL +In order to run the Aardwolf backend, you will need to have access to a +[PostgreSQL](https://www.postgresql.org/) database. There are a few options for doing this, but for +this guide we’re going to assume you are running the database on your +development machine. + +#### Linux/OSX Instructions + +If you're on an Ubuntu-like machine, you should be able to install +PostgreSQL like this: + + $ sudo apt-get update + $ sudo apt-get install postgresql postgresql-contrib + +If you see an error like: + + = note: /usr/bin/ld: cannot find -lpq + collect2: error: ld returned 1 exit statusb + +Then you may need to install the libpq (PostgreSQL C-library) package as well : + + $ sudo apt-get install libpq-dev + +If you're on OSX and using `brew`, do + + $ brew update + $ brew install postgres + +For Gentoo (eselect-postgresql is optional), + + # emerge --sync + # emerge -av postgresql eselect-postgresql + +For Fedora/CentOS/RHEL, do + + # dnf install postgresql-server postgresql-contrib + +#### Windows Instructions + +For Windows, just download the installer [here](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads#windows) and run it. After installing, make sure to add the /lib directory to your PATH system variable. + +### Installing rustup + +> Note: Rustup managed installations do appear to co-exist with system + installations on Gentoo, and should work on most other distributions. + If not, please file an issue with the Rust and Rustup teams or your distribution’s + managers. + +Next, you’ll need to have the [Rust](https://rust-lang.org/) toolchain +installed. The best way to do this is to install +[rustup](https://rustup.rs), which is a Rust toolchain manager. + +#### Linux/OSX Instructions + +Open your terminal and run the following command: + + $ curl https://sh.rustup.rs -sSf | sh + +For those who are (understandably) uncomfortable with piping a shell +script from the internet directly into `sh`, you can also +[use an alternate installation method](https://github.com/rust-lang-nursery/rustup.rs/#other-installation-methods). + +#### Windows Instructions + +If you don't already have them, download and install the [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools). + +Then, download the [rustup installer](https://www.rust-lang.org/en-US/install.html) and run it. That's it! + +### Installing Rust Toolchain + +Once you have `rustup` installed, make sure you have the `nightly` rust +toolchain installed: + + $ rustup toolchain install nightly From 2217ec0d5637b01b1ecbb3596688f92c193e9d9b Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 16:31:42 +0100 Subject: [PATCH 13/27] Remove PgConnection when we don't need it Massive simplification in the ActivityPub module! --- src/activity_pub/mod.rs | 7 +++---- src/activity_pub/request.rs | 5 ++--- src/activity_pub/sign.rs | 7 +++---- src/models/follows.rs | 2 +- src/models/users.rs | 2 +- src/routes/comments.rs | 2 +- src/routes/likes.rs | 4 ++-- src/routes/posts.rs | 2 +- src/routes/reshares.rs | 4 ++-- src/routes/user.rs | 2 +- 10 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index 4943f58..4f48c16 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -1,6 +1,5 @@ use activitypub::{Activity, Actor, Object, Link}; use array_tool::vec::Uniq; -use diesel::PgConnection; use reqwest::Client; use rocket::{ http::{ContentType, Status}, @@ -74,7 +73,7 @@ impl<'r, O: Object> Responder<'r> for ActivityStream { } } -pub fn broadcast(conn: &PgConnection, sender: &S, act: A, to: Vec) { +pub fn broadcast(sender: &S, act: A, to: Vec) { let boxes = to.into_iter() .map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url())) .collect::>() @@ -82,14 +81,14 @@ pub fn broadcast(conn let mut act = serde_json::to_value(act).unwrap(); act["@context"] = context(); - let signed = act.sign(sender, conn); + let signed = act.sign(sender); for inbox in boxes { // TODO: run it in Sidekiq or something like that let res = Client::new() .post(&inbox[..]) .headers(request::headers()) - .header(request::signature(sender, request::headers(), conn)) + .header(request::signature(sender, request::headers())) .header(request::digest(signed.to_string())) .body(signed.to_string()) .send(); diff --git a/src/activity_pub/request.rs b/src/activity_pub/request.rs index afde1b8..e0e2517 100644 --- a/src/activity_pub/request.rs +++ b/src/activity_pub/request.rs @@ -1,5 +1,4 @@ use base64; -use diesel::PgConnection; use openssl::hash::{Hasher, MessageDigest}; use reqwest::header::{Date, Headers, UserAgent}; use std::time::SystemTime; @@ -23,7 +22,7 @@ pub fn headers() -> Headers { headers } -pub fn signature(signer: &S, headers: Headers, conn: &PgConnection) -> Signature { +pub fn signature(signer: &S, headers: Headers) -> Signature { let signed_string = headers.iter().map(|h| format!("{}: {}", h.name().to_lowercase(), h.value_string())).collect::>().join("\n"); let signed_headers = headers.iter().map(|h| h.name().to_string()).collect::>().join(" ").to_lowercase(); @@ -32,7 +31,7 @@ pub fn signature(signer: &S, headers: Headers, conn: &PgConnection) - Signature(format!( "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{signed_headers}\",signature=\"{signature}\"", - key_id = signer.get_key_id(conn), + key_id = signer.get_key_id(), signed_headers = signed_headers, signature = sign )) diff --git a/src/activity_pub/sign.rs b/src/activity_pub/sign.rs index 6ad6275..d71e6db 100644 --- a/src/activity_pub/sign.rs +++ b/src/activity_pub/sign.rs @@ -1,6 +1,5 @@ use base64; use chrono::Utc; -use diesel::PgConnection; use hex; use openssl::{ pkey::PKey, @@ -24,7 +23,7 @@ pub trait Signer { } pub trait Signable { - fn sign(&mut self, creator: &T, conn: &PgConnection) -> &mut Self where T: Signer; + fn sign(&mut self, creator: &T) -> &mut Self where T: Signer; fn hash(data: String) -> String { let bytes = data.into_bytes(); @@ -33,11 +32,11 @@ pub trait Signable { } impl Signable for serde_json::Value { - fn sign(&mut self, creator: &T, conn: &PgConnection) -> &mut serde_json::Value { + fn sign(&mut self, creator: &T) -> &mut serde_json::Value { let creation_date = Utc::now().to_rfc3339(); let mut options = json!({ "type": "RsaSignature2017", - "creator": creator.get_key_id(conn), + "creator": creator.get_key_id(), "created": creation_date }); diff --git a/src/models/follows.rs b/src/models/follows.rs index 3518816..638a257 100644 --- a/src/models/follows.rs +++ b/src/models/follows.rs @@ -44,7 +44,7 @@ impl Follow { let mut accept = Accept::default(); accept.accept_props.set_actor_link::(from.clone().into_id()).unwrap(); accept.accept_props.set_object_object(follow).unwrap(); - broadcast(conn, &*from, accept, vec![target.clone()]); + broadcast(&*from, accept, vec![target.clone()]); res } } diff --git a/src/models/users.rs b/src/models/users.rs index 63d5aec..5190c61 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -401,7 +401,7 @@ impl APActor for User { fn custom_props(&self, conn: &PgConnection) -> serde_json::Map { let mut res = serde_json::Map::new(); res.insert("publicKey".to_string(), json!({ - "id": self.get_key_id(conn), + "id": self.get_key_id(), "owner": self.ap_url, "publicKeyPem": self.public_key })); diff --git a/src/routes/comments.rs b/src/routes/comments.rs index d54403c..2bb48d5 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -45,7 +45,7 @@ fn create_response(blog_name: String, slug: String, query: Option, let instance = Instance::get_local(&*conn).unwrap(); instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")); - broadcast(&*conn, &user, new_comment, user.get_followers(&*conn)); + broadcast(&user, new_comment, user.get_followers(&*conn)); Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) } diff --git a/src/routes/likes.rs b/src/routes/likes.rs index 216a990..c6b0a5c 100644 --- a/src/routes/likes.rs +++ b/src/routes/likes.rs @@ -25,11 +25,11 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect { like.update_ap_url(&*conn); like.notify(&*conn); - broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn)); + broadcast(&user, like.into_activity(&*conn), user.get_followers(&*conn)); } else { let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).unwrap(); let delete_act = like.delete(&*conn); - broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); + broadcast(&user, delete_act, user.get_followers(&*conn)); } Redirect::to(uri!(super::posts::details: blog = blog, slug = slug)) diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 7f5007e..38d5563 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -119,7 +119,7 @@ fn create(blog_name: String, data: Form, user: User, conn: DbConn) } let act = post.create_activity(&*conn); - broadcast(&*conn, &user, act, user.get_followers(&*conn)); + broadcast(&user, act, user.get_followers(&*conn)); Redirect::to(uri!(details: blog = blog_name, slug = slug)) } diff --git a/src/routes/reshares.rs b/src/routes/reshares.rs index 5212faa..4166f26 100644 --- a/src/routes/reshares.rs +++ b/src/routes/reshares.rs @@ -25,11 +25,11 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect { reshare.update_ap_url(&*conn); reshare.notify(&*conn); - broadcast(&*conn, &user, reshare.into_activity(&*conn), user.get_followers(&*conn)); + broadcast(&user, reshare.into_activity(&*conn), user.get_followers(&*conn)); } else { let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id).unwrap(); let delete_act = reshare.delete(&*conn); - broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); + broadcast(&user, delete_act, user.get_followers(&*conn)); } Redirect::to(uri!(super::posts::details: blog = blog, slug = slug)) diff --git a/src/routes/user.rs b/src/routes/user.rs index e49df65..cc125d3 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -82,7 +82,7 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect { act.follow_props.set_object_object(user.into_activity(&*conn)).unwrap(); act.object_props.set_id_string(format!("{}/follow/{}", user.ap_url, target.ap_url)).unwrap(); - broadcast(&*conn, &user, act, vec![target]); + broadcast(&user, act, vec![target]); Redirect::to(uri!(details: name = name)) } From 8efabca55302f6f3c707065e362cfd60468442ba Mon Sep 17 00:00:00 2001 From: Banjo Fox Date: Thu, 21 Jun 2018 11:32:11 -0400 Subject: [PATCH 14/27] Adding link for PREREQUISITES.md --- DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index fbd9db8..8b9773b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,7 +8,7 @@ All commands are run in the Mac Terminal or terminal emulator of your choice, su ### Linux -Similar to Mac OSX all commands should be run from a terminal (a.k.a command line). First, you will need [Git](https://git-scm.com/download/mac), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Step-by-step instructions are also available here: [Installing Prerequisites](No Such URL yet) +Similar to Mac OSX all commands should be run from a terminal (a.k.a command line). First, you will need [Git](https://git-scm.com/download/mac), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Step-by-step instructions are also available here: [Installing Prerequisites](/doc/PREREQUISITES.md) #### Download the Repository From 5193ad6f65c89f2a40e16336d6f788d45b318419 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 16:45:54 +0100 Subject: [PATCH 15/27] Remove legacy Inbox::unlike function --- src/activity_pub/inbox.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index 0305567..f1a3e2b 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -52,12 +52,6 @@ pub trait Deletable { pub trait Inbox { fn received(&self, conn: &PgConnection, act: serde_json::Value); - fn unlike(&self, conn: &PgConnection, undo: Undo) -> Result<(), Error> { - let like = likes::Like::find_by_ap_url(conn, undo.undo_props.object_object::()?.object_props.id_string()?).unwrap(); - like.delete(conn); - Ok(()) - } - fn save(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> { let actor_id = Id::new(act["actor"].as_str().unwrap()); match act["type"].as_str() { From 3fe2625e86b2a1152bc9a4ffe4029947ebc02887 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 17:00:37 +0100 Subject: [PATCH 16/27] Simplify the Inbox trait If we want to add, for instance, streams in the future, we could introduce a new trait for that, similar to FromActivity or Notify We also display inbox errors to the "client" if something fails, which could be useful for debugging. --- src/activity_pub/inbox.rs | 4 +--- src/models/instance.rs | 9 +-------- src/models/users.rs | 10 +--------- src/routes/comments.rs | 3 ++- src/routes/instance.rs | 9 +++++++-- src/routes/user.rs | 9 +++++++-- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index f1a3e2b..012108e 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -50,9 +50,7 @@ pub trait Deletable { } pub trait Inbox { - fn received(&self, conn: &PgConnection, act: serde_json::Value); - - fn save(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> { + fn received(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> { let actor_id = Id::new(act["actor"].as_str().unwrap()); match act["type"].as_str() { Some(t) => { diff --git a/src/models/instance.rs b/src/models/instance.rs index b7128da..b627718 100644 --- a/src/models/instance.rs +++ b/src/models/instance.rs @@ -1,6 +1,5 @@ use chrono::NaiveDateTime; use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection}; -use serde_json; use std::iter::Iterator; use activity_pub::inbox::Inbox; @@ -61,10 +60,4 @@ impl Instance { } } -impl Inbox for Instance { - fn received(&self, conn: &PgConnection, act: serde_json::Value) { - self.save(conn, act.clone()).expect("Shared Inbox: Couldn't save activity"); - - // TODO: add to stream, or whatever needs to be done - } -} +impl Inbox for Instance {} diff --git a/src/models/users.rs b/src/models/users.rs index 5190c61..300c5fa 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -441,15 +441,7 @@ impl WithInbox for User { } } -impl Inbox for User { - fn received(&self, conn: &PgConnection, act: serde_json::Value) { - if let Err(err) = self.save(conn, act.clone()) { - println!("Inbox error:\n{}\n{}\n\nActivity was: {}", err.cause(), err.backtrace(), act.to_string()); - } - - // TODO: add to stream, or whatever needs to be done - } -} +impl Inbox for User {} impl Signer for User { fn get_key_id(&self) -> String { diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 2bb48d5..aa9997f 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -44,7 +44,8 @@ fn create_response(blog_name: String, slug: String, query: Option, .create(&*conn); let instance = Instance::get_local(&*conn).unwrap(); - instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")); + instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")) + .expect("We are not compatible with ourselve: local broadcast failed (new comment)"); broadcast(&user, new_comment, user.get_followers(&*conn)); Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 9463511..4c7816f 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -35,8 +35,13 @@ fn index(conn: DbConn, user: Option) -> Template { fn shared_inbox(conn: DbConn, data: String) -> String { let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); let instance = Instance::get_local(&*conn).unwrap(); - instance.received(&*conn, act); - String::from("") + match instance.received(&*conn, act) { + Ok(_) => String::new(), + Err(e) => { + println!("Shared inbox error: {}\n{}", e.cause(), e.backtrace()); + format!("Error: {}", e.cause()) + } + } } #[get("/nodeinfo")] diff --git a/src/routes/user.rs b/src/routes/user.rs index cc125d3..a6d0052 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -199,8 +199,13 @@ fn outbox(name: String, conn: DbConn) -> ActivityStream { fn inbox(name: String, conn: DbConn, data: String) -> String { let user = User::find_local(&*conn, name).unwrap(); let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); - user.received(&*conn, act); - String::from("") + match user.received(&*conn, act) { + Ok(_) => String::new(), + Err(e) => { + println!("User inbox error: {}\n{}", e.cause(), e.backtrace()); + format!("Error: {}", e.cause()) + } + } } #[get("/@//followers", format = "application/activity+json")] From ea29dd91dc4e60b7f70d7880c9696415e53ab203 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 17:25:29 +0100 Subject: [PATCH 17/27] Set the correct content type for the ActivityStream responder --- src/activity_pub/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index 4f48c16..44e564f 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -69,7 +69,9 @@ impl ActivityStream { impl<'r, O: Object> Responder<'r> for ActivityStream { fn respond_to(self, request: &Request) -> Result, Status> { - serde_json::to_string(&self.0).respond_to(request) + serde_json::to_string(&self.0).respond_to(request).map(|r| Response::build_from(r) + .raw_header("Content-Type", "application/activity+json") + .finalize()) } } From 6df4b703184a92adc89d6eb809326dfa13d67180 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 17:30:07 +0100 Subject: [PATCH 18/27] Set @context for ActivityStream responder --- src/activity_pub/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index 44e564f..eff1e42 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -69,7 +69,9 @@ impl ActivityStream { impl<'r, O: Object> Responder<'r> for ActivityStream { fn respond_to(self, request: &Request) -> Result, Status> { - serde_json::to_string(&self.0).respond_to(request).map(|r| Response::build_from(r) + let mut json = serde_json::to_value(&self.0).map_err(|e| Status::InternalServerError)?; + json["@context"] = context(); + serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r) .raw_header("Content-Type", "application/activity+json") .finalize()) } From b2e8d54161b455710fdfe09038025e7d91751214 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 18:09:18 +0100 Subject: [PATCH 19/27] Drop activity_pub::activitypub and activity_pub::ActivityPub and only use the ActivityStream responder --- src/activity_pub/actor.rs | 24 +----------------------- src/activity_pub/mod.rs | 13 +++---------- src/models/blogs.rs | 6 +++++- src/routes/blogs.rs | 8 ++++---- src/routes/posts.rs | 9 ++++----- src/routes/user.rs | 26 ++++++++++++-------------- 6 files changed, 29 insertions(+), 57 deletions(-) diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs index ab5f6c5..25ea3d2 100644 --- a/src/activity_pub/actor.rs +++ b/src/activity_pub/actor.rs @@ -1,8 +1,7 @@ use diesel::PgConnection; use serde_json; -use BASE_URL; -use activity_pub::{activity_pub, ActivityPub, context, ap_url}; +use activity_pub::ap_url; use models::instance::Instance; pub enum ActorType { @@ -40,27 +39,6 @@ pub trait Actor: Sized { serde_json::Map::new() } - fn as_activity_pub (&self, conn: &PgConnection) -> ActivityPub { - let mut repr = json!({ - "@context": context(), - "id": self.compute_id(conn), - "type": Self::get_actor_type().to_string(), - "inbox": self.compute_inbox(conn), - "outbox": self.compute_outbox(conn), - "preferredUsername": self.get_actor_id(), - "name": self.get_display_name(), - "summary": self.get_summary(), - "url": self.compute_id(conn), - "endpoints": { - "sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str())) - } - }); - - self.custom_props(conn).iter().for_each(|p| repr[p.0] = p.1.clone()); - - activity_pub(repr) - } - fn compute_outbox(&self, conn: &PgConnection) -> String { self.compute_box(conn, "outbox") } diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index eff1e42..9903ac8 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -2,11 +2,10 @@ use activitypub::{Activity, Actor, Object, Link}; use array_tool::vec::Uniq; use reqwest::Client; use rocket::{ - http::{ContentType, Status}, - response::{Response, Responder, Content}, + http::Status, + response::{Response, Responder}, request::Request }; -use rocket_contrib::Json; use serde_json; use self::sign::Signable; @@ -16,8 +15,6 @@ pub mod inbox; pub mod request; pub mod sign; -pub type ActivityPub = Content>; - pub const CONTEXT_URL: &'static str = "https://www.w3.org/ns/activitystreams"; pub const PUBLIC_VISIBILTY: &'static str = "https://www.w3.org/ns/activitystreams#Public"; @@ -55,10 +52,6 @@ pub fn context() -> serde_json::Value { ]) } -pub fn activity_pub(json: serde_json::Value) -> ActivityPub { - Content(ContentType::new("application", "activity+json"), Json(json)) -} - pub struct ActivityStream (T); impl ActivityStream { @@ -69,7 +62,7 @@ impl ActivityStream { impl<'r, O: Object> Responder<'r> for ActivityStream { fn respond_to(self, request: &Request) -> Result, Status> { - let mut json = serde_json::to_value(&self.0).map_err(|e| Status::InternalServerError)?; + let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; json["@context"] = context(); serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r) .raw_header("Content-Type", "application/activity+json") diff --git a/src/models/blogs.rs b/src/models/blogs.rs index ea3d1be..12cd9c9 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -1,4 +1,4 @@ -use activitypub::{Actor, Object, collection::OrderedCollection}; +use activitypub::{Actor, Object, actor::Group, collection::OrderedCollection}; use reqwest::{ Client, header::{Accept, qitem}, @@ -137,6 +137,10 @@ impl Blog { }) } + pub fn into_activity(&self, _conn: &PgConnection) -> Group { + Group::default() // TODO + } + pub fn update_boxes(&self, conn: &PgConnection) { if self.outbox_url.len() == 0 { diesel::update(self) diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index ce53a64..6b09194 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -1,4 +1,4 @@ -use activitypub::collection::OrderedCollection; +use activitypub::{actor::Group, collection::OrderedCollection}; use rocket::{ request::Form, response::{Redirect, Flash} @@ -6,7 +6,7 @@ use rocket::{ use rocket_contrib::Template; use serde_json; -use activity_pub::{ActivityStream, ActivityPub, actor::Actor}; +use activity_pub::ActivityStream; use db_conn::DbConn; use models::{ blog_authors::*, @@ -32,9 +32,9 @@ fn details(name: String, conn: DbConn, user: Option) -> Template { } #[get("/~/", format = "application/activity+json", rank = 1)] -fn activity_details(name: String, conn: DbConn) -> ActivityPub { +fn activity_details(name: String, conn: DbConn) -> ActivityStream { let blog = Blog::find_local(&*conn, name).unwrap(); - blog.as_activity_pub(&*conn) + ActivityStream::new(blog.into_activity(&*conn)) } #[get("/blogs/new")] diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 38d5563..79e0a8f 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -1,10 +1,11 @@ +use activitypub::object::Article; use heck::KebabCase; use rocket::request::Form; use rocket::response::{Redirect, Flash}; use rocket_contrib::Template; use serde_json; -use activity_pub::{broadcast, context, activity_pub, ActivityPub}; +use activity_pub::{broadcast, ActivityStream}; use db_conn::DbConn; use models::{ blogs::*, @@ -49,13 +50,11 @@ fn details_response(blog: String, slug: String, conn: DbConn, user: Option } #[get("/~//", rank = 3, format = "application/activity+json")] -fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityPub { +fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityStream
{ let blog = Blog::find_by_fqn(&*conn, blog).unwrap(); let post = Post::find_by_slug(&*conn, slug, blog.id).unwrap(); - let mut act = serde_json::to_value(post.into_activity(&*conn)).unwrap(); - act["@context"] = context(); - activity_pub(act) + ActivityStream::new(post.into_activity(&*conn)) } #[get("/~//new", rank = 2)] diff --git a/src/routes/user.rs b/src/routes/user.rs index a6d0052..cfc416f 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -1,5 +1,6 @@ use activitypub::{ activity::Follow, + actor::Person, collection::OrderedCollection }; use rocket::{request::Form, @@ -9,7 +10,7 @@ use rocket_contrib::Template; use serde_json; use activity_pub::{ - activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId, + ActivityStream, broadcast, Id, IntoId, inbox::{Inbox, Notify}, actor::Actor }; @@ -110,9 +111,9 @@ fn followers(name: String, conn: DbConn, account: Option) -> Template { } #[get("/@/", format = "application/activity+json", rank = 1)] -fn activity_details(name: String, conn: DbConn) -> ActivityPub { +fn activity_details(name: String, conn: DbConn) -> ActivityStream { let user = User::find_local(&*conn, name).unwrap(); - user.as_activity_pub(&*conn) + ActivityStream::new(user.into_activity(&*conn)) } #[get("/users/new")] @@ -209,16 +210,13 @@ fn inbox(name: String, conn: DbConn, data: String) -> String { } #[get("/@//followers", format = "application/activity+json")] -fn ap_followers(name: String, conn: DbConn) -> ActivityPub { +fn ap_followers(name: String, conn: DbConn) -> ActivityStream { let user = User::find_local(&*conn, name).unwrap(); - let followers = user.get_followers(&*conn).into_iter().map(|f| f.ap_url).collect::>(); - - let json = json!({ - "@context": context(), - "id": user.compute_box(&*conn, "followers"), - "type": "OrderedCollection", - "totalItems": followers.len(), - "orderedItems": followers - }); - activity_pub(json) + let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::>(); + + let mut coll = OrderedCollection::default(); + coll.object_props.set_id_string(format!("{}/followers", user.ap_url)).expect("Follower collection: id error"); + coll.collection_props.set_total_items_u64(followers.len() as u64).expect("Follower collection: totalItems error"); + coll.collection_props.set_items_link_vec(followers).expect("Follower collection: items error"); + ActivityStream::new(coll) } From 7812b9bc8f52073c9888da53817db0e457d7d4dd Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 18:11:33 +0100 Subject: [PATCH 20/27] Aardwolf -> Plume Copy/pasting is bad :p --- doc/PREREQUISITES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/PREREQUISITES.md b/doc/PREREQUISITES.md index 5d0b874..e159e50 100644 --- a/doc/PREREQUISITES.md +++ b/doc/PREREQUISITES.md @@ -10,7 +10,8 @@ seek out documentation from your distribution package or from [the `doc` folder] ## Installing Requirements ### Installing PostgreSQL -In order to run the Aardwolf backend, you will need to have access to a + +In order to run the Plume backend, you will need to have access to a [PostgreSQL](https://www.postgresql.org/) database. There are a few options for doing this, but for this guide we’re going to assume you are running the database on your development machine. From 606a3d12c708981c6d55bba83b79a64d1d5eba10 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 18:23:01 +0100 Subject: [PATCH 21/27] Remove useless functions from activity_pub::actor::Actor --- src/activity_pub/actor.rs | 31 -------------------- src/models/blogs.rs | 39 +++++++------------------ src/models/comments.rs | 1 - src/models/follows.rs | 2 +- src/models/likes.rs | 1 - src/models/reshares.rs | 2 +- src/models/users.rs | 61 ++++++++++----------------------------- 7 files changed, 27 insertions(+), 110 deletions(-) diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs index 25ea3d2..e500128 100644 --- a/src/activity_pub/actor.rs +++ b/src/activity_pub/actor.rs @@ -1,44 +1,15 @@ use diesel::PgConnection; -use serde_json; use activity_pub::ap_url; use models::instance::Instance; -pub enum ActorType { - Person, - Blog -} - -impl ToString for ActorType { - fn to_string(&self) -> String { - String::from(match self { - ActorType::Person => "Person", - ActorType::Blog => "Blog" - }) - } -} - pub trait Actor: Sized { fn get_box_prefix() -> &'static str; fn get_actor_id(&self) -> String; - fn get_display_name(&self) -> String; - - fn get_summary(&self) -> String; - fn get_instance(&self, conn: &PgConnection) -> Instance; - fn get_actor_type() -> ActorType; - - fn get_inbox_url(&self) -> String; - - fn get_shared_inbox_url(&self) -> Option; - - fn custom_props(&self, _conn: &PgConnection) -> serde_json::Map { - serde_json::Map::new() - } - fn compute_outbox(&self, conn: &PgConnection) -> String { self.compute_box(conn, "outbox") } @@ -59,6 +30,4 @@ pub trait Actor: Sized { user = self.get_actor_id() )) } - - fn from_url(conn: &PgConnection, url: String) -> Option; } diff --git a/src/models/blogs.rs b/src/models/blogs.rs index 12cd9c9..e62fa08 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -18,7 +18,7 @@ use webfinger::*; use activity_pub::{ ActivityStream, Id, IntoId, - actor::{Actor as APActor, ActorType}, + actor::{Actor as APActor}, inbox::WithInbox, sign }; @@ -199,6 +199,15 @@ impl Blog { ] } } + + // FIXME: see User::from_url for correct behavior + pub fn from_url(conn: &PgConnection, url: String) -> Option { + blogs::table.filter(blogs::ap_url.eq(url)) + .limit(1) + .load::(conn) + .expect("Error loading blog from url") + .into_iter().nth(0) + } } impl IntoId for Blog { @@ -229,37 +238,9 @@ impl APActor for Blog { self.actor_id.to_string() } - fn get_display_name(&self) -> String { - self.title.clone() - } - - fn get_summary(&self) -> String { - self.summary.clone() - } - fn get_instance(&self, conn: &PgConnection) -> Instance { Instance::get(conn, self.instance_id).unwrap() } - - fn get_actor_type () -> ActorType { - ActorType::Blog - } - - fn get_inbox_url(&self) -> String { - self.inbox_url.clone() - } - - fn get_shared_inbox_url(&self) -> Option { - None - } - - fn from_url(conn: &PgConnection, url: String) -> Option { - blogs::table.filter(blogs::ap_url.eq(url)) - .limit(1) - .load::(conn) - .expect("Error loading blog from url") - .into_iter().nth(0) - } } impl sign::Signer for Blog { diff --git a/src/models/comments.rs b/src/models/comments.rs index 9deaa1a..a6578f2 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -9,7 +9,6 @@ use serde_json; use activity_pub::{ ap_url, Id, IntoId, PUBLIC_VISIBILTY, - actor::Actor, inbox::{FromActivity, Notify} }; use models::{ diff --git a/src/models/follows.rs b/src/models/follows.rs index 638a257..c7bffb5 100644 --- a/src/models/follows.rs +++ b/src/models/follows.rs @@ -1,7 +1,7 @@ use activitypub::{Actor, activity::{Accept, Follow as FollowAct}}; use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl}; -use activity_pub::{broadcast, Id, IntoId, actor::Actor as ApActor, inbox::{FromActivity, Notify, WithInbox}, sign::Signer}; +use activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox}, sign::Signer}; use models::{ blogs::Blog, notifications::*, diff --git a/src/models/likes.rs b/src/models/likes.rs index 0f18b10..f7c6fb9 100644 --- a/src/models/likes.rs +++ b/src/models/likes.rs @@ -5,7 +5,6 @@ use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use activity_pub::{ Id, IntoId, - actor::Actor, inbox::{FromActivity, Deletable, Notify} }; use models::{ diff --git a/src/models/reshares.rs b/src/models/reshares.rs index bc29f4f..69e4191 100644 --- a/src/models/reshares.rs +++ b/src/models/reshares.rs @@ -2,7 +2,7 @@ use activitypub::activity::{Announce, Undo}; use chrono::NaiveDateTime; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; -use activity_pub::{Id, IntoId, actor::Actor, inbox::{FromActivity, Notify, Deletable}}; +use activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}}; use models::{notifications::*, posts::Post, users::User}; use schema::reshares; diff --git a/src/models/users.rs b/src/models/users.rs index 300c5fa..8ddf5a3 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -28,7 +28,7 @@ use webfinger::*; use BASE_URL; use activity_pub::{ ap_url, ActivityStream, Id, IntoId, - actor::{ActorType, Actor as APActor}, + actor::{Actor as APActor}, inbox::{Inbox, WithInbox}, sign::{Signer, gen_keypair} }; @@ -308,8 +308,8 @@ impl User { pub fn into_activity(&self, _conn: &PgConnection) -> Person { let mut actor = Person::default(); actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error"); - actor.object_props.set_name_string(self.get_display_name()).expect("User::into_activity: name error"); - actor.object_props.set_summary_string(self.get_summary()).expect("User::into_activity: summary error"); + actor.object_props.set_name_string(self.display_name.clone()).expect("User::into_activity: name error"); + actor.object_props.set_summary_string(self.summary.get().clone()).expect("User::into_activity: summary error"); actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error"); actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error"); actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error"); @@ -350,6 +350,18 @@ impl User { ] } } + + pub fn from_url(conn: &PgConnection, url: String) -> Option { + User::find_by_ap_url(conn, url.clone()).or_else(|| { + // The requested user was not in the DB + // We try to fetch it if it is remote + if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != BASE_URL.as_str() { + Some(User::fetch_from_url(conn, url).unwrap()) + } else { + None + } + }) + } } impl<'a, 'r> FromRequest<'a, 'r> for User { @@ -374,52 +386,9 @@ impl APActor for User { self.username.to_string() } - fn get_display_name(&self) -> String { - self.display_name.clone() - } - - fn get_summary(&self) -> String { - self.summary.get().clone() - } - fn get_instance(&self, conn: &PgConnection) -> Instance { Instance::get(conn, self.instance_id).unwrap() } - - fn get_actor_type() -> ActorType { - ActorType::Person - } - - fn get_inbox_url(&self) -> String { - self.inbox_url.clone() - } - - fn get_shared_inbox_url(&self) -> Option { - self.shared_inbox_url.clone() - } - - fn custom_props(&self, conn: &PgConnection) -> serde_json::Map { - let mut res = serde_json::Map::new(); - res.insert("publicKey".to_string(), json!({ - "id": self.get_key_id(), - "owner": self.ap_url, - "publicKeyPem": self.public_key - })); - res.insert("followers".to_string(), serde_json::Value::String(self.compute_box(conn, "followers"))); - res - } - - fn from_url(conn: &PgConnection, url: String) -> Option { - User::find_by_ap_url(conn, url.clone()).or_else(|| { - // The requested user was not in the DB - // We try to fetch it if it is remote - if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != BASE_URL.as_str() { - Some(User::fetch_from_url(conn, url).unwrap()) - } else { - None - } - }) - } } impl IntoId for User { From 9a8472bdccd44268738a40711807fe0b5ab38aa1 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 18:42:17 +0100 Subject: [PATCH 22/27] Move compute_box from Actor to Instance --- src/activity_pub/actor.rs | 29 ++++++++++++----------------- src/models/blogs.rs | 11 +++++++---- src/models/instance.rs | 12 +++++++++++- src/models/users.rs | 13 ++++++++----- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs index e500128..b699652 100644 --- a/src/activity_pub/actor.rs +++ b/src/activity_pub/actor.rs @@ -10,24 +10,19 @@ pub trait Actor: Sized { fn get_instance(&self, conn: &PgConnection) -> Instance; - fn compute_outbox(&self, conn: &PgConnection) -> String { - self.compute_box(conn, "outbox") - } + // fn compute_outbox(&self, conn: &PgConnection) -> String { + // self.compute_box(conn, "outbox") + // } - fn compute_inbox(&self, conn: &PgConnection) -> String { - self.compute_box(conn, "inbox") - } + // fn compute_inbox(&self, conn: &PgConnection) -> String { + // self.compute_box(conn, "inbox") + // } - fn compute_box(&self, conn: &PgConnection, box_name: &str) -> String { - format!("{id}/{name}", id = self.compute_id(conn), name = box_name) - } + // fn compute_box(&self, conn: &PgConnection, box_name: &str) -> String { + // format!("{id}/{name}", id = self.compute_id(conn), name = box_name) + // } - fn compute_id(&self, conn: &PgConnection) -> String { - ap_url(format!( - "{instance}/{prefix}/{user}", - instance = self.get_instance(conn).public_domain, - prefix = Self::get_box_prefix(), - user = self.get_actor_id() - )) - } + // fn compute_id(&self, conn: &PgConnection) -> String { + // String::new() + // } } diff --git a/src/models/blogs.rs b/src/models/blogs.rs index e62fa08..f2fdd9a 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -55,6 +55,8 @@ pub struct NewBlog { pub public_key: String } +const BLOG_PREFIX: &'static str = "~"; + impl Blog { insert!(blogs, NewBlog); get!(blogs); @@ -142,21 +144,22 @@ impl Blog { } pub fn update_boxes(&self, conn: &PgConnection) { + let instance = self.get_instance(conn); if self.outbox_url.len() == 0 { diesel::update(self) - .set(blogs::outbox_url.eq(self.compute_outbox(conn))) + .set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox"))) .get_result::(conn).expect("Couldn't update outbox URL"); } if self.inbox_url.len() == 0 { diesel::update(self) - .set(blogs::inbox_url.eq(self.compute_inbox(conn))) + .set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox"))) .get_result::(conn).expect("Couldn't update inbox URL"); } if self.ap_url.len() == 0 { diesel::update(self) - .set(blogs::ap_url.eq(self.compute_id(conn))) + .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), ""))) .get_result::(conn).expect("Couldn't update AP URL"); } } @@ -189,7 +192,7 @@ impl Blog { Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), mime_type: Some(String::from("application/atom+xml")), - href: self.compute_box(conn, "feed.atom") + href: self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom") }, Link { rel: String::from("self"), diff --git a/src/models/instance.rs b/src/models/instance.rs index b627718..b93a92a 100644 --- a/src/models/instance.rs +++ b/src/models/instance.rs @@ -2,7 +2,7 @@ use chrono::NaiveDateTime; use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection}; use std::iter::Iterator; -use activity_pub::inbox::Inbox; +use activity_pub::{ap_url, inbox::Inbox}; use models::users::User; use schema::{instances, users}; @@ -58,6 +58,16 @@ impl Instance { .expect("Couldn't load admins") .len() > 0 } + + pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String { + ap_url(format!( + "{instance}/{prefix}/{name}/{box_name}", + instance = self.public_domain, + prefix = prefix, + name = name, + box_name = box_name + )) + } } impl Inbox for Instance {} diff --git a/src/models/users.rs b/src/models/users.rs index 8ddf5a3..f55edce 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -83,6 +83,8 @@ pub struct NewUser { pub shared_inbox_url: Option } +const USER_PREFIX: &'static str = "@"; + impl User { insert!(users, NewUser); get!(users); @@ -196,21 +198,22 @@ impl User { } pub fn update_boxes(&self, conn: &PgConnection) { + let instance = self.get_instance(conn); if self.outbox_url.len() == 0 { diesel::update(self) - .set(users::outbox_url.eq(self.compute_outbox(conn))) - .get_result::(conn).expect("Couldn't update outbox URL"); + .set(users::outbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "outbox"))) + .get_result::(conn).expect("Couldn't update outbox URL"); } if self.inbox_url.len() == 0 { diesel::update(self) - .set(users::inbox_url.eq(self.compute_inbox(conn))) + .set(users::inbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "inbox"))) .get_result::(conn).expect("Couldn't update inbox URL"); } if self.ap_url.len() == 0 { diesel::update(self) - .set(users::ap_url.eq(self.compute_id(conn))) + .set(users::ap_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), ""))) .get_result::(conn).expect("Couldn't update AP URL"); } @@ -340,7 +343,7 @@ impl User { Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), mime_type: Some(String::from("application/atom+xml")), - href: self.compute_box(conn, "feed.atom") + href: self.get_instance(conn).compute_box(USER_PREFIX, self.username.clone(), "feed.atom") }, Link { rel: String::from("self"), From f5f2aa7c599111e66cea3df8d962d96b490acb2a Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 18:53:57 +0100 Subject: [PATCH 23/27] Get rid of the activity_pub::actor::Actor trait --- src/activity_pub/actor.rs | 28 ---------------------------- src/activity_pub/mod.rs | 1 - src/models/blogs.rs | 19 ++++--------------- src/models/users.rs | 21 +++++---------------- src/routes/user.rs | 3 +-- 5 files changed, 10 insertions(+), 62 deletions(-) delete mode 100644 src/activity_pub/actor.rs diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs deleted file mode 100644 index b699652..0000000 --- a/src/activity_pub/actor.rs +++ /dev/null @@ -1,28 +0,0 @@ -use diesel::PgConnection; - -use activity_pub::ap_url; -use models::instance::Instance; - -pub trait Actor: Sized { - fn get_box_prefix() -> &'static str; - - fn get_actor_id(&self) -> String; - - fn get_instance(&self, conn: &PgConnection) -> Instance; - - // fn compute_outbox(&self, conn: &PgConnection) -> String { - // self.compute_box(conn, "outbox") - // } - - // fn compute_inbox(&self, conn: &PgConnection) -> String { - // self.compute_box(conn, "inbox") - // } - - // fn compute_box(&self, conn: &PgConnection, box_name: &str) -> String { - // format!("{id}/{name}", id = self.compute_id(conn), name = box_name) - // } - - // fn compute_id(&self, conn: &PgConnection) -> String { - // String::new() - // } -} diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index 9903ac8..b7eb615 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -10,7 +10,6 @@ use serde_json; use self::sign::Signable; -pub mod actor; pub mod inbox; pub mod request; pub mod sign; diff --git a/src/models/blogs.rs b/src/models/blogs.rs index f2fdd9a..16aa13f 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -18,7 +18,6 @@ use webfinger::*; use activity_pub::{ ActivityStream, Id, IntoId, - actor::{Actor as APActor}, inbox::WithInbox, sign }; @@ -61,6 +60,10 @@ impl Blog { insert!(blogs, NewBlog); get!(blogs); + pub fn get_instance(&self, conn: &PgConnection) -> Instance { + Instance::get(conn, self.instance_id).expect("Couldn't find instance") + } + pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec { use schema::blog_authors; let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id); @@ -232,20 +235,6 @@ impl WithInbox for Blog { } } -impl APActor for Blog { - fn get_box_prefix() -> &'static str { - "~" - } - - fn get_actor_id(&self) -> String { - self.actor_id.to_string() - } - - fn get_instance(&self, conn: &PgConnection) -> Instance { - Instance::get(conn, self.instance_id).unwrap() - } -} - impl sign::Signer for Blog { fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) diff --git a/src/models/users.rs b/src/models/users.rs index f55edce..3cbf1df 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -28,7 +28,6 @@ use webfinger::*; use BASE_URL; use activity_pub::{ ap_url, ActivityStream, Id, IntoId, - actor::{Actor as APActor}, inbox::{Inbox, WithInbox}, sign::{Signer, gen_keypair} }; @@ -92,6 +91,10 @@ impl User { find_by!(users, find_by_name, username as String, instance_id as i32); find_by!(users, find_by_ap_url, ap_url as String); + pub fn get_instance(&self, conn: &PgConnection) -> Instance { + Instance::get(conn, self.instance_id).expect("Couldn't find instance") + } + pub fn grant_admin_rights(&self, conn: &PgConnection) { diesel::update(self) .set(users::is_admin.eq(true)) @@ -316,7 +319,7 @@ impl User { actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error"); actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error"); actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error"); - actor.ap_actor_props.set_preferred_username_string(self.get_actor_id()).expect("User::into_activity: preferredUsername error"); + actor.ap_actor_props.set_preferred_username_string(self.username.clone()).expect("User::into_activity: preferredUsername error"); let mut endpoints = Endpoint::default(); endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error"); @@ -380,20 +383,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { } } -impl APActor for User { - fn get_box_prefix() -> &'static str { - "@" - } - - fn get_actor_id(&self) -> String { - self.username.to_string() - } - - fn get_instance(&self, conn: &PgConnection) -> Instance { - Instance::get(conn, self.instance_id).unwrap() - } -} - impl IntoId for User { fn into_id(self) -> Id { Id::new(self.ap_url.clone()) diff --git a/src/routes/user.rs b/src/routes/user.rs index cfc416f..2f9ae0b 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -11,8 +11,7 @@ use serde_json; use activity_pub::{ ActivityStream, broadcast, Id, IntoId, - inbox::{Inbox, Notify}, - actor::Actor + inbox::{Inbox, Notify} }; use db_conn::DbConn; use models::{ From e7e557612e88955d1fc3e7253ba985e00cbb5725 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 21:30:56 +0100 Subject: [PATCH 24/27] Rewrite User::from_activity to use the activitypub crate instead of raw JSON --- Cargo.lock | 2 ++ Cargo.toml | 7 ++++++- src/activity_pub/mod.rs | 24 ++++++++++++++++++++++++ src/main.rs | 3 +++ src/models/users.rs | 28 ++++++++++++++++------------ 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02988cf..50c8e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -949,6 +949,8 @@ name = "plume" version = "0.1.0" dependencies = [ "activitypub 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "activitystreams-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "activitystreams-traits 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "array_tool 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 957e693..12a6825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ name = "plume" version = "0.1.0" [dependencies] activitypub = "0.1.1" +activitystreams-derive = "0.1.0" +activitystreams-traits = "0.1.0" ammonia = "1.1.0" array_tool = "1.0" base64 = "0.9" @@ -18,7 +20,6 @@ hex = "0.3" hyper = "*" lazy_static = "*" openssl = "0.10.6" -pulldown-cmark = { version = "0.1.2", default-features = false } reqwest = "0.8" rpassword = "2.0" serde = "*" @@ -36,6 +37,10 @@ version = "0.4" features = ["postgres", "r2d2", "chrono"] version = "*" +[dependencies.pulldown-cmark] +default-features = false +version = "0.1.2" + [dependencies.rocket] git = "https://github.com/SergioBenitez/Rocket" rev = "df7111143e466c18d1f56377a8d9530a5a306aba" diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index b7eb615..87fb579 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -115,3 +115,27 @@ pub trait IntoId { } impl Link for Id {} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct ApSignature { + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(PublicKey), functional)] + pub public_key: Option +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct PublicKey { + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(String), functional)] + pub id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(String), functional)] + pub owner: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(String), functional)] + pub public_key_pem: Option +} diff --git a/src/main.rs b/src/main.rs index 3a42db2..85b4584 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,9 @@ #![plugin(rocket_codegen)] extern crate activitypub; +#[macro_use] +extern crate activitystreams_derive; +extern crate activitystreams_traits; extern crate ammonia; extern crate array_tool; extern crate base64; diff --git a/src/models/users.rs b/src/models/users.rs index 3cbf1df..538cc86 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,5 +1,5 @@ use activitypub::{ - Actor, Object, Endpoint, + Actor, Object, Endpoint, CustomObject, actor::Person, collection::OrderedCollection }; @@ -27,7 +27,7 @@ use webfinger::*; use BASE_URL; use activity_pub::{ - ap_url, ActivityStream, Id, IntoId, + ap_url, ActivityStream, Id, IntoId, ApSignature, inbox::{Inbox, WithInbox}, sign::{Signer, gen_keypair} }; @@ -45,6 +45,8 @@ use safe_string::SafeString; pub const AUTH_COOKIE: &'static str = "user_id"; +pub type CustomPerson = CustomObject; + #[derive(Queryable, Identifiable, Serialize, Deserialize, Clone, Debug)] pub struct User { pub id: i32, @@ -157,14 +159,14 @@ impl User { .send(); match req { Ok(mut res) => { - let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); + let json: CustomPerson = serde_json::from_str(&res.text().unwrap()).unwrap(); Some(User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())) }, Err(_) => None } } - fn from_activity(conn: &PgConnection, acct: serde_json::Value, inst: String) -> User { + fn from_activity(conn: &PgConnection, acct: CustomPerson, inst: String) -> User { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { @@ -176,19 +178,21 @@ impl User { } }; User::insert(conn, NewUser { - username: acct["preferredUsername"].as_str().unwrap().to_string(), - display_name: acct["name"].as_str().unwrap().to_string(), - outbox_url: acct["outbox"].as_str().unwrap().to_string(), - inbox_url: acct["inbox"].as_str().unwrap().to_string(), + username: acct.object.ap_actor_props.preferred_username_string().expect("User::from_activity: preferredUsername error"), + display_name: acct.object.object_props.name_string().expect("User::from_activity: name error"), + outbox_url: acct.object.ap_actor_props.outbox_string().expect("User::from_activity: outbox error"), + inbox_url: acct.object.ap_actor_props.inbox_string().expect("User::from_activity: inbox error"), is_admin: false, - summary: SafeString::new(&acct["summary"].as_str().unwrap().to_string()), + summary: SafeString::new(&acct.object.object_props.summary_string().expect("User::from_activity: summary error")), email: None, hashed_password: None, instance_id: instance.id, - ap_url: acct["id"].as_str().unwrap().to_string(), - public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap().to_string(), + ap_url: acct.object.object_props.id_string().expect("User::from_activity: id error"), + public_key: acct.custom_props.public_key_publickey().expect("User::from_activity: publicKey error") + .public_key_pem_string().expect("User::from_activity: publicKey.publicKeyPem error"), private_key: None, - shared_inbox_url: acct["endpoints"]["sharedInbox"].as_str().map(|s| s.to_string()) + shared_inbox_url: acct.object.ap_actor_props.endpoints_endpoint() + .and_then(|e| e.shared_inbox_string()).ok() }) } From a0f680a9b678fb46bef47efda28d44e46bbf48d5 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 21:39:44 +0100 Subject: [PATCH 25/27] Rewrite Blog::from_activity to use the activitypub crate instead of raw JSON --- src/models/blogs.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/models/blogs.rs b/src/models/blogs.rs index 16aa13f..6fd78b5 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -1,4 +1,4 @@ -use activitypub::{Actor, Object, actor::Group, collection::OrderedCollection}; +use activitypub::{Actor, Object, CustomObject, actor::Group, collection::OrderedCollection}; use reqwest::{ Client, header::{Accept, qitem}, @@ -17,13 +17,14 @@ use openssl::{ use webfinger::*; use activity_pub::{ - ActivityStream, Id, IntoId, + ApSignature, ActivityStream, Id, IntoId, inbox::WithInbox, sign }; use models::instance::*; use schema::blogs; +pub type CustomGroup = CustomObject; #[derive(Queryable, Identifiable, Serialize, Deserialize, Clone)] pub struct Blog { @@ -111,14 +112,14 @@ impl Blog { .send(); match req { Ok(mut res) => { - let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); + let json = serde_json::from_str(&res.text().unwrap()).unwrap(); Some(Blog::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())) }, Err(_) => None } } - fn from_activity(conn: &PgConnection, acct: serde_json::Value, inst: String) -> Blog { + fn from_activity(conn: &PgConnection, acct: CustomGroup, inst: String) -> Blog { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { @@ -130,14 +131,15 @@ impl Blog { } }; Blog::insert(conn, NewBlog { - actor_id: acct["preferredUsername"].as_str().unwrap().to_string(), - title: acct["name"].as_str().unwrap().to_string(), - outbox_url: acct["outbox"].as_str().unwrap().to_string(), - inbox_url: acct["inbox"].as_str().unwrap().to_string(), - summary: acct["summary"].as_str().unwrap().to_string(), + actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"), + title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"), + outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"), + inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"), + summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"), instance_id: instance.id, - ap_url: acct["id"].as_str().unwrap().to_string(), - public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap_or("").to_string(), + ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"), + public_key: acct.custom_props.public_key_publickey().expect("Blog::from_activity: publicKey error") + .public_key_pem_string().expect("Blog::from_activity: publicKey.publicKeyPem error"), private_key: None }) } From dccab062e34f57aa67e5aaa444fb3cafbb5eb890 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 22:07:04 +0100 Subject: [PATCH 26/27] Implement Blog::into_activity --- src/models/blogs.rs | 41 ++++++++++++++++++++++++++++++----------- src/routes/blogs.rs | 4 ++-- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/models/blogs.rs b/src/models/blogs.rs index 6fd78b5..fd5ec9b 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -16,8 +16,9 @@ use openssl::{ }; use webfinger::*; +use BASE_URL; use activity_pub::{ - ApSignature, ActivityStream, Id, IntoId, + ApSignature, ActivityStream, Id, IntoId, PublicKey, inbox::WithInbox, sign }; @@ -60,6 +61,8 @@ const BLOG_PREFIX: &'static str = "~"; impl Blog { insert!(blogs, NewBlog); get!(blogs); + find_by!(blogs, find_by_ap_url, ap_url as String); + find_by!(blogs, find_by_name, actor_id as String, instance_id as i32); pub fn get_instance(&self, conn: &PgConnection) -> Instance { Instance::get(conn, self.instance_id).expect("Couldn't find instance") @@ -73,8 +76,6 @@ impl Blog { .expect("Couldn't load blogs ") } - find_by!(blogs, find_by_name, actor_id as String, instance_id as i32); - pub fn find_local(conn: &PgConnection, name: String) -> Option { Blog::find_by_name(conn, name, Instance::local_id(conn)) } @@ -144,8 +145,23 @@ impl Blog { }) } - pub fn into_activity(&self, _conn: &PgConnection) -> Group { - Group::default() // TODO + pub fn into_activity(&self, _conn: &PgConnection) -> CustomGroup { + let mut blog = Group::default(); + blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error"); + blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error"); + blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error"); + blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error"); + blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error"); + blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error"); + + let mut public_key = PublicKey::default(); + public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error"); + public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error"); + public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error"); + let mut ap_signature = ApSignature::default(); + ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error"); + + CustomGroup::new(blog, ap_signature) } pub fn update_boxes(&self, conn: &PgConnection) { @@ -208,13 +224,16 @@ impl Blog { } } - // FIXME: see User::from_url for correct behavior pub fn from_url(conn: &PgConnection, url: String) -> Option { - blogs::table.filter(blogs::ap_url.eq(url)) - .limit(1) - .load::(conn) - .expect("Error loading blog from url") - .into_iter().nth(0) + Blog::find_by_ap_url(conn, url.clone()).or_else(|| { + // The requested user was not in the DB + // We try to fetch it if it is remote + if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != BASE_URL.as_str() { + Some(Blog::fetch_from_url(conn, url).unwrap()) + } else { + None + } + }) } } diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index 6b09194..c843968 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -1,4 +1,4 @@ -use activitypub::{actor::Group, collection::OrderedCollection}; +use activitypub::collection::OrderedCollection; use rocket::{ request::Form, response::{Redirect, Flash} @@ -32,7 +32,7 @@ fn details(name: String, conn: DbConn, user: Option) -> Template { } #[get("/~/", format = "application/activity+json", rank = 1)] -fn activity_details(name: String, conn: DbConn) -> ActivityStream { +fn activity_details(name: String, conn: DbConn) -> ActivityStream { let blog = Blog::find_local(&*conn, name).unwrap(); ActivityStream::new(blog.into_activity(&*conn)) } From e34d12922c15c416792f268baed0fd8e7c549ae2 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 22:12:24 +0100 Subject: [PATCH 27/27] Serialize publicKey in Person representation --- src/models/users.rs | 14 +++++++++++--- src/routes/user.rs | 3 +-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/models/users.rs b/src/models/users.rs index 538cc86..9a3f776 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -27,7 +27,7 @@ use webfinger::*; use BASE_URL; use activity_pub::{ - ap_url, ActivityStream, Id, IntoId, ApSignature, + ap_url, ActivityStream, Id, IntoId, ApSignature, PublicKey, inbox::{Inbox, WithInbox}, sign::{Signer, gen_keypair} }; @@ -315,7 +315,7 @@ impl User { PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() } - pub fn into_activity(&self, _conn: &PgConnection) -> Person { + pub fn into_activity(&self, _conn: &PgConnection) -> CustomPerson { let mut actor = Person::default(); actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error"); actor.object_props.set_name_string(self.display_name.clone()).expect("User::into_activity: name error"); @@ -328,7 +328,15 @@ impl User { let mut endpoints = Endpoint::default(); endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error"); actor.ap_actor_props.set_endpoints_endpoint(endpoints).expect("User::into_activity: endpoints error"); - actor + + let mut public_key = PublicKey::default(); + public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error"); + public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error"); + public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error"); + let mut ap_signature = ApSignature::default(); + ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error"); + + CustomPerson::new(actor, ap_signature) } pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { diff --git a/src/routes/user.rs b/src/routes/user.rs index 2f9ae0b..a2bbfc0 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -1,6 +1,5 @@ use activitypub::{ activity::Follow, - actor::Person, collection::OrderedCollection }; use rocket::{request::Form, @@ -110,7 +109,7 @@ fn followers(name: String, conn: DbConn, account: Option) -> Template { } #[get("/@/", format = "application/activity+json", rank = 1)] -fn activity_details(name: String, conn: DbConn) -> ActivityStream { +fn activity_details(name: String, conn: DbConn) -> ActivityStream { let user = User::find_local(&*conn, name).unwrap(); ActivityStream::new(user.into_activity(&*conn)) }