From 413e34ac0ecac168e5b20a02a3f8d8c7e5f3ff1f Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 6 Sep 2018 22:39:22 +0100 Subject: [PATCH] Federate article updating --- plume-models/src/lib.rs | 11 ++++ plume-models/src/posts.rs | 77 ++++++++++++++++++++------- src/inbox.rs | 7 ++- src/routes/posts.rs | 106 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 20 deletions(-) diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index acfc2b1..68f01ee 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -71,6 +71,17 @@ macro_rules! insert { }; } +macro_rules! update { + ($table:ident) => { + pub fn update(&self, conn: &PgConnection) -> Self { + diesel::update(self) + .set(self) + .get_result(conn) + .expect(concat!("Error updating ", stringify!($table))) + } + }; +} + 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); diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 967edf7..a55f113 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -1,5 +1,5 @@ use activitypub::{ - activity::{Create, Delete}, + activity::{Create, Delete, Update}, link, object::{Article, Tombstone} }; @@ -25,7 +25,7 @@ use users::User; use schema::posts; use safe_string::SafeString; -#[derive(Queryable, Identifiable, Serialize, Clone)] +#[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)] pub struct Post { pub id: i32, pub blog_id: i32, @@ -58,6 +58,7 @@ pub struct NewPost { impl Post { insert!(posts, NewPost); get!(posts); + update!(posts); find_by!(posts, find_by_slug, slug as String, blog_id as i32); find_by!(posts, find_by_ap_url, ap_url as String); @@ -234,39 +235,77 @@ impl Post { let mut tags_json = Tag::for_post(conn, self.id).into_iter().map(|t| json!(t.into_activity(conn))).collect::>(); let mut article = Article::default(); - article.object_props.set_name_string(self.title.clone()).expect("Article::into_activity: name error"); - article.object_props.set_id_string(self.ap_url.clone()).expect("Article::into_activity: id error"); + article.object_props.set_name_string(self.title.clone()).expect("Post::into_activity: name error"); + article.object_props.set_id_string(self.ap_url.clone()).expect("Post::into_activity: id error"); let mut authors = self.get_authors(conn).into_iter().map(|x| Id::new(x.ap_url)).collect::>(); authors.push(self.get_blog(conn).into_id()); // add the blog URL here too - article.object_props.set_attributed_to_link_vec::(authors).expect("Article::into_activity: attributedTo error"); - article.object_props.set_content_string(self.content.get().clone()).expect("Article::into_activity: content error"); + article.object_props.set_attributed_to_link_vec::(authors).expect("Post::into_activity: attributedTo error"); + article.object_props.set_content_string(self.content.get().clone()).expect("Post::into_activity: content error"); article.ap_object_props.set_source_object(Source { content: self.source.clone(), media_type: String::from("text/markdown"), - }).expect("Article::into_activity: source error"); - article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Article::into_activity: published error"); - article.object_props.set_summary_string(self.subtitle.clone()).expect("Article::into_activity: summary error"); + }).expect("Post::into_activity: source error"); + article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Post::into_activity: published error"); + article.object_props.set_summary_string(self.subtitle.clone()).expect("Post::into_activity: summary error"); article.object_props.tag = Some(json!(mentions_json.append(&mut tags_json))); - article.object_props.set_url_string(self.ap_url.clone()).expect("Article::into_activity: url error"); - article.object_props.set_to_link_vec::(to.into_iter().map(Id::new).collect()).expect("Article::into_activity: to error"); - article.object_props.set_cc_link_vec::(vec![]).expect("Article::into_activity: cc error"); + article.object_props.set_url_string(self.ap_url.clone()).expect("Post::into_activity: url error"); + article.object_props.set_to_link_vec::(to.into_iter().map(Id::new).collect()).expect("Post::into_activity: to error"); + article.object_props.set_cc_link_vec::(vec![]).expect("Post::into_activity: cc error"); article } pub fn create_activity(&self, conn: &PgConnection) -> Create { let article = self.into_activity(conn); let mut act = Create::default(); - act.object_props.set_id_string(format!("{}/activity", self.ap_url)).expect("Article::create_activity: id error"); - act.object_props.set_to_link_vec::(article.object_props.to_link_vec().expect("Article::create_activity: Couldn't copy 'to'")) - .expect("Article::create_activity: to error"); - act.object_props.set_cc_link_vec::(article.object_props.cc_link_vec().expect("Article::create_activity: Couldn't copy 'cc'")) - .expect("Article::create_activity: cc error"); - act.create_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Article::create_activity: actor error"); - act.create_props.set_object_object(article).expect("Article::create_activity: object error"); + act.object_props.set_id_string(format!("{}/activity", self.ap_url)).expect("Post::create_activity: id error"); + act.object_props.set_to_link_vec::(article.object_props.to_link_vec().expect("Post::create_activity: Couldn't copy 'to'")) + .expect("Post::create_activity: to error"); + act.object_props.set_cc_link_vec::(article.object_props.cc_link_vec().expect("Post::create_activity: Couldn't copy 'cc'")) + .expect("Post::create_activity: cc error"); + act.create_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::create_activity: actor error"); + act.create_props.set_object_object(article).expect("Post::create_activity: object error"); act } + pub fn update_activity(&self, conn: &PgConnection) -> Update { + let article = self.into_activity(conn); + let mut act = Update::default(); + act.object_props.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp())).expect("Post::update_activity: id error"); + act.object_props.set_to_link_vec::(article.object_props.to_link_vec().expect("Post::update_activity: Couldn't copy 'to'")) + .expect("Post::update_activity: to error"); + act.object_props.set_cc_link_vec::(article.object_props.cc_link_vec().expect("Post::update_activity: Couldn't copy 'cc'")) + .expect("Post::update_activity: cc error"); + act.update_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::update_activity: actor error"); + act.update_props.set_object_object(article).expect("Article::update_activity: object error"); + act + } + + pub fn handle_update(&mut self, conn: &PgConnection, updated: Article) { + if let Ok(title) = updated.object_props.name_string() { + self.slug = title.to_kebab_case(); + self.title = title; + } + + if let Ok(content) = updated.object_props.content_string() { + self.content = SafeString::new(&content); + } + + if let Ok(subtitle) = updated.object_props.summary_string() { + self.subtitle = subtitle; + } + + if let Ok(ap_url) = updated.object_props.url_string() { + self.ap_url = ap_url; + } + + if let Ok(source) = updated.ap_object_props.source_object::() { + self.source = source.content; + } + + self.update(conn); + } + pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { let blog = self.get_blog(conn); json!({ diff --git a/src/inbox.rs b/src/inbox.rs index 414e960..19e4efe 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -1,4 +1,4 @@ -use activitypub::{activity::{Announce, Create, Delete, Like, Undo}, object::Tombstone}; +use activitypub::{activity::{Announce, Create, Delete, Like, Undo, Update}, object::Tombstone}; use diesel::PgConnection; use failure::Error; use serde_json; @@ -63,6 +63,11 @@ pub trait Inbox { _ => Err(InboxError::CantUndo)? } } + "Update" => { + let act: Update = serde_json::from_value(act.clone())?; + Post::handle_update(conn, act.update_props.object_object()?); + Ok(()) + } _ => Err(InboxError::InvalidType)? } }, diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 85eb86c..e7614c7 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -90,12 +90,116 @@ fn new(blog: String, user: User, conn: DbConn) -> Template { Template::render("posts/new", json!({ "account": user.to_json(&*conn), "instance": Instance::get_local(&*conn), + "editing": false, "errors": null, "form": null })) } } +#[get("/~///edit")] +fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Template { + let b = Blog::find_by_fqn(&*conn, blog.to_string()); + let post = b.clone().and_then(|blog| Post::find_by_slug(&*conn, slug, blog.id)).expect("Post to edit not found"); + + if !user.is_author_in(&*conn, b.clone().unwrap()) { + Template::render("errors/403", json!({ + "error_message": "You are not author in this blog." + })) + } else { + Template::render("posts/new", json!({ + "account": user.to_json(&*conn), + "instance": Instance::get_local(&*conn), + "editing": true, + "errors": null, + "form": NewPostForm { + title: post.title.clone(), + subtitle: post.subtitle.clone(), + content: post.source.clone(), + tags: Tag::for_post(&*conn, post.id) + .into_iter() + .map(|t| t.tag) + .collect::>() + .join(", "), + license: post.license.clone(), + } + })) + } +} + +#[post("/~///edit", data = "")] +fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientForm, worker: State>>) -> Result { + let b = Blog::find_by_fqn(&*conn, blog.to_string()); + let mut post = b.clone().and_then(|blog| Post::find_by_slug(&*conn, slug, blog.id)).expect("Post to update not found"); + + let form = data.get(); + let new_slug = form.title.to_string().to_kebab_case(); + + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e + }; + if let Some(_) = Post::find_by_slug(&*conn, new_slug.clone(), b.unwrap().id) { + errors.add("title", ValidationError { + code: Cow::from("existing_slug"), + message: Some(Cow::from("A post with the same title already exists.")), + params: HashMap::new() + }); + } + + if errors.is_empty() { + if !user.is_author_in(&*conn, b.clone().unwrap()) { + // actually it's not "Ok"… + Ok(Redirect::to(uri!(super::blogs::details: name = blog))) + } else { + let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref()); + + let license = if form.license.len() > 0 { + form.license.to_string() + } else { + Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or(String::from("CC-0")) + }; + + post.slug = new_slug.clone(); + post.title = form.title.clone(); + post.subtitle = form.subtitle.clone(); + post.content = SafeString::new(&content); + post.source = form.content.clone(); + post.license = license; + post.update(&*conn); + let post = post.update_ap_url(&*conn); + + for m in mentions.into_iter() { + Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), post.id, true); + } + + let old_tags = Tag::for_post(&*conn, post.id).into_iter().map(|t| t.tag).collect::>(); + let tags = form.tags.split(",").map(|t| t.trim().to_camel_case()).filter(|t| t.len() > 0 && !old_tags.contains(t)); + for tag in tags { + Tag::insert(&*conn, NewTag { + tag: tag, + is_hastag: false, + post_id: post.id + }); + } + + let act = post.update_activity(&*conn); + let followers = user.get_followers(&*conn); + worker.execute(Thunk::of(move || broadcast(&user, act, followers))); + + Ok(Redirect::to(uri!(details: blog = blog, slug = slug))) + } + } else { + Err(Template::render("posts/new", json!({ + "account": user.to_json(&*conn), + "instance": Instance::get_local(&*conn), + "editing": false, + "errors": errors.inner(), + "form": form + }))) + } +} + #[derive(FromForm, Validate, Serialize)] struct NewPostForm { #[validate(custom(function = "valid_slug", message = "Invalid title"))] @@ -187,12 +291,14 @@ fn create(blog_name: String, data: LenientForm, user: User, conn: D Err(Template::render("posts/new", json!({ "account": user.to_json(&*conn), "instance": Instance::get_local(&*conn), + "editing": false, "errors": errors.inner(), "form": form }))) } } + #[get("/~///delete")] fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: State>>) -> Redirect { let post = Blog::find_by_fqn(&*conn, blog_name.clone())