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 }}