From e6b8943085df30ab0339bfaf464df0e429c47c08 Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 21 Jun 2018 11:28:42 +0100 Subject: [PATCH] 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 57ecff65..e041e03d 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 5926170a..13057095 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 d0f01f3c..fa120b10 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 49e678be..a2eddaf9 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 5cc3262e..f79256d5 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 3b17a6ea..98897434 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,