diff --git a/plume-common/src/activity_pub/inbox.rs b/plume-common/src/activity_pub/inbox.rs index 6ce93c1..ffbdbe6 100644 --- a/plume-common/src/activity_pub/inbox.rs +++ b/plume-common/src/activity_pub/inbox.rs @@ -1,4 +1,4 @@ -use activitypub::{Object, activity::Create}; +use activitypub::{Object, activity::{Create, Delete}}; use activity_pub::Id; @@ -29,9 +29,10 @@ pub trait Notify { fn notify(&self, conn: &C); } -pub trait Deletable { - /// true if success - fn delete_activity(conn: &C, id: Id) -> bool; +pub trait Deletable { + fn delete(&self, conn: &C) -> A; + fn delete_id(id: String, conn: &C); + } pub trait WithInbox { diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index 651b4d8..f0a3973 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -48,19 +48,6 @@ impl Like { } } - pub fn delete(&self, conn: &PgConnection) -> activity::Undo { - diesel::delete(self).execute(conn).unwrap(); - - let mut act = activity::Undo::default(); - act.undo_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).expect("Like::delete: actor error"); - act.undo_props.set_object_object(self.into_activity(conn)).expect("Like::delete: object error"); - act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Like::delete: id error"); - act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::delete: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Like::delete: cc error"); - - act - } - pub fn into_activity(&self, conn: &PgConnection) -> activity::Like { let mut act = activity::Like::default(); act.like_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).expect("Like::into_activity: actor error"); @@ -100,13 +87,23 @@ impl Notify for Like { } } -impl Deletable for Like { - fn delete_activity(conn: &PgConnection, id: Id) -> bool { +impl Deletable for Like { + fn delete(&self, conn: &PgConnection) -> activity::Undo { + diesel::delete(self).execute(conn).unwrap(); + + let mut act = activity::Undo::default(); + act.undo_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).expect("Like::delete: actor error"); + act.undo_props.set_object_object(self.into_activity(conn)).expect("Like::delete: object error"); + act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Like::delete: id error"); + act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::delete: to error"); + act.object_props.set_cc_link_vec::(vec![]).expect("Like::delete: cc error"); + + act + } + + fn delete_id(id: String, conn: &PgConnection) { if let Some(like) = Like::find_by_ap_url(conn, id.into()) { like.delete(conn); - true - } else { - false } } } diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 7ed756f..5b67a66 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -1,7 +1,7 @@ use activitypub::{ - activity::Create, + activity::{Create, Delete}, link, - object::Article + object::{Article, Tombstone} }; use chrono::{NaiveDateTime, TimeZone, Utc}; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl, dsl::any}; @@ -10,7 +10,7 @@ use serde_json; use plume_common::activity_pub::{ PUBLIC_VISIBILTY, Id, IntoId, - inbox::FromActivity + inbox::{Deletable, FromActivity} }; use {BASE_URL, ap_url}; use blogs::Blog; @@ -273,6 +273,27 @@ impl FromActivity for Post { } } +impl Deletable for Post { + fn delete(&self, conn: &PgConnection) -> Delete { + let mut act = Delete::default(); + act.delete_props.set_actor_link(self.get_authors(conn)[0].clone().into_id()).expect("Post::delete: actor error"); + + let mut tombstone = Tombstone::default(); + tombstone.object_props.set_id_string(self.ap_url.clone()).expect("Post::delete: object.id error"); + act.delete_props.set_object_object(tombstone).expect("Post::delete: object error"); + + act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Post::delete: id error"); + act.object_props.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]).expect("Post::delete: to error"); + + diesel::delete(self).execute(conn).expect("Post::delete: DB error"); + act + } + + fn delete_id(id: String, conn: &PgConnection) { + Post::find_by_ap_url(conn, id).map(|p| p.delete(conn)); + } +} + impl IntoId for Post { fn into_id(self) -> Id { Id::new(self.ap_url.clone()) diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index 2acb0a4..4fd1a63 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -59,19 +59,6 @@ impl Reshare { User::get(conn, self.user_id) } - pub fn delete(&self, conn: &PgConnection) -> Undo { - diesel::delete(self).execute(conn).unwrap(); - - let mut act = Undo::default(); - act.undo_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); - act.undo_props.set_object_object(self.into_activity(conn)).unwrap(); - act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Reshare::delete: id error"); - act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::delete: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Reshare::delete: cc error"); - - act - } - pub fn into_activity(&self, conn: &PgConnection) -> Announce { let mut act = Announce::default(); act.announce_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); @@ -111,13 +98,23 @@ impl Notify for Reshare { } } -impl Deletable for Reshare { - fn delete_activity(conn: &PgConnection, id: Id) -> bool { - if let Some(reshare) = Reshare::find_by_ap_url(conn, id.into()) { +impl Deletable for Reshare { + fn delete(&self, conn: &PgConnection) -> Undo { + diesel::delete(self).execute(conn).unwrap(); + + let mut act = Undo::default(); + act.undo_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); + act.undo_props.set_object_object(self.into_activity(conn)).unwrap(); + act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Reshare::delete: id error"); + act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::delete: to error"); + act.object_props.set_cc_link_vec::(vec![]).expect("Reshare::delete: cc error"); + + act + } + + fn delete_id(id: String, conn: &PgConnection) { + if let Some(reshare) = Reshare::find_by_ap_url(conn, id) { reshare.delete(conn); - true - } else { - false } } } diff --git a/po/plume.pot b/po/plume.pot index 51dc694..aeff569 100644 --- a/po/plume.pot +++ b/po/plume.pot @@ -420,3 +420,6 @@ msgstr "" msgid "Read the detailed rules" msgstr "" + +msgid "Delete this article" +msgstr "" diff --git a/src/inbox.rs b/src/inbox.rs index d5b2dce..5a4d0bb 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -1,4 +1,4 @@ -use activitypub::activity::{Announce, Create, Like, Undo}; +use activitypub::{activity::{Announce, Create, Delete, Like, Undo}, object::Tombstone}; use diesel::PgConnection; use failure::Error; use serde_json; @@ -32,6 +32,11 @@ pub trait Inbox { Err(InboxError::InvalidType)? } }, + "Delete" => { + let act: Delete = serde_json::from_value(act.clone())?; + Post::delete_id(act.delete_props.object_object::()?.object_props.id_string()?, conn); + Ok(()) + }, "Follow" => { Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); Ok(()) @@ -44,11 +49,11 @@ pub trait Inbox { let act: Undo = serde_json::from_value(act.clone())?; match act.undo_props.object["type"].as_str().unwrap() { "Like" => { - likes::Like::delete_activity(conn, Id::new(act.undo_props.object_object::()?.object_props.id_string()?)); + likes::Like::delete_id(act.undo_props.object_object::()?.object_props.id_string()?, conn); Ok(()) }, "Announce" => { - Reshare::delete_activity(conn, Id::new(act.undo_props.object_object::()?.object_props.id_string()?)); + Reshare::delete_id(act.undo_props.object_object::()?.object_props.id_string()?, conn); Ok(()) } _ => Err(InboxError::CantUndo)? diff --git a/src/main.rs b/src/main.rs index 2e0c76f..1b7acb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,7 @@ fn main() { routes::posts::new, routes::posts::new_auth, routes::posts::create, + routes::posts::delete, routes::reshares::create, routes::reshares::create_auth, diff --git a/src/routes/likes.rs b/src/routes/likes.rs index a3a2dd3..8e68ff4 100644 --- a/src/routes/likes.rs +++ b/src/routes/likes.rs @@ -1,7 +1,7 @@ use rocket::{State, response::{Redirect, Flash}}; use workerpool::{Pool, thunk::*}; -use plume_common::activity_pub::{broadcast, inbox::Notify}; +use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}}; use plume_common::utils; use plume_models::{ blogs::Blog, diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 1fd4f4f..63d5620 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -8,7 +8,7 @@ use std::{collections::HashMap, borrow::Cow}; use validator::{Validate, ValidationError, ValidationErrors}; use workerpool::{Pool, thunk::*}; -use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest}; +use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, inbox::Deletable}; use plume_common::utils; use plume_models::{ blogs::*, @@ -53,10 +53,11 @@ fn details_response(blog: String, slug: String, conn: DbConn, user: Option "has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false), "n_reshares": post.get_reshares(&*conn).len(), "has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false), - "account": user, + "account": &user, "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, &vec![]))), - "user_fqn": user.map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()) + "user_fqn": user.clone().map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()), + "is_author": user.map(|u| post.get_authors(&*conn).into_iter().any(|a| u.id == a.id)).unwrap_or(false) })) }) }) @@ -176,3 +177,23 @@ fn create(blog_name: String, data: LenientForm, user: User, conn: D }))) } } + +#[post("/~///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()) + .and_then(|blog| Post::find_by_slug(&*conn, slug.clone(), blog.id)); + + if let Some(post) = post { + if !post.get_authors(&*conn).into_iter().any(|a| a.id == user.id) { + Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone())) + } else { + let audience = user.get_followers(&*conn); + let delete_activity = post.delete(&*conn); + worker.execute(Thunk::of(move || broadcast(&user, delete_activity, audience))); + + Redirect::to(uri!(super::blogs::details: name = blog_name)) + } + } else { + Redirect::to(uri!(super::blogs::details: name = blog_name)) + } +} diff --git a/src/routes/reshares.rs b/src/routes/reshares.rs index 1ba673a..f57efc4 100644 --- a/src/routes/reshares.rs +++ b/src/routes/reshares.rs @@ -1,7 +1,7 @@ use rocket::{State, response::{Redirect, Flash}}; use workerpool::{Pool, thunk::*}; -use plume_common::activity_pub::{broadcast, inbox::Notify}; +use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}}; use plume_common::utils; use plume_models::{ blogs::Blog, diff --git a/src/setup.rs b/src/setup.rs index 8efd038..9239196 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -215,7 +215,7 @@ fn create_admin(instance: Instance, conn: DbConn) { fn check_native_deps() { let mut not_found = Vec::new(); if !try_run("psql") { - not_found.push(("PostgreSQL", "sudo apt install postgres")); + not_found.push(("PostgreSQL", "sudo apt install postgresql")); } if !try_run("gettext") { not_found.push(("GetText", "sudo apt install gettext")) diff --git a/templates/posts/details.html.tera b/templates/posts/details.html.tera index c20fa49..f17ad56 100644 --- a/templates/posts/details.html.tera +++ b/templates/posts/details.html.tera @@ -22,6 +22,10 @@ }} — {{ date | date(format="%B %e, %Y") }} + — + {% if is_author %} + {{ "Delete this article" | _ }} + {% endif %}

{{ article.post.content | safe }}