Allow for comment deletion (#363)

* Allow for comment deletion

Receive and emit deletion activity
Add button to delete comment

* Remove debug print and fix copy-past typo

* Improve style of comment deletion button
This commit is contained in:
fdb-hiroshima 2018-12-23 11:13:36 +01:00 committed by GitHub
parent 0df9c4d400
commit 5c5cf36b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 86 additions and 12 deletions

View File

@ -1,4 +1,4 @@
use activitypub::{activity::Create, link, object::Note}; use activitypub::{activity::{Create, Delete}, link, object::{Note, Tombstone}};
use chrono::{self, NaiveDateTime}; use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use serde_json; use serde_json;
@ -7,7 +7,7 @@ use instance::Instance;
use mentions::Mention; use mentions::Mention;
use notifications::*; use notifications::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{FromActivity, Notify}, inbox::{FromActivity, Notify, Deletable},
Id, IntoId, PUBLIC_VISIBILTY, Id, IntoId, PUBLIC_VISIBILTY,
}; };
use plume_common::utils; use plume_common::utils;
@ -254,3 +254,49 @@ impl Notify<Connection> for Comment {
} }
} }
} }
impl<'a> Deletable<Connection, Delete> for Comment {
fn delete(&self, conn: &Connection) -> Delete {
let mut act = Delete::default();
act.delete_props
.set_actor_link(self.get_author(conn).into_id())
.expect("Comment::delete: actor error");
let mut tombstone = Tombstone::default();
tombstone
.object_props
.set_id_string(self.ap_url.clone().expect("Comment::delete: no ap_url"))
.expect("Comment::delete: object.id error");
act.delete_props
.set_object_object(tombstone)
.expect("Comment::delete: object error");
act.object_props
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))
.expect("Comment::delete: id error");
act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)])
.expect("Comment::delete: to error");
for m in Mention::list_for_comment(&conn, self.id) {
m.delete(conn);
}
diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(conn)
.expect("Comment::delete: DB error could not update other comments");
diesel::delete(self)
.execute(conn)
.expect("Comment::delete: DB error");
act
}
fn delete_id(id: &str, actor_id: &str, conn: &Connection) {
let actor = User::find_by_ap_url(conn, actor_id);
let comment = Comment::find_by_ap_url(conn, id);
if let Some(comment) = comment.filter(|c| c.author_id == actor.unwrap().id) {
comment.delete(conn);
}
}
}

View File

@ -65,6 +65,14 @@ pub trait Inbox {
actor_id.as_ref(), actor_id.as_ref(),
&(conn, searcher), &(conn, searcher),
); );
Comment::delete_id(
&act.delete_props
.object_object::<Tombstone>()?
.object_props
.id_string()?,
actor_id.as_ref(),
conn,
);
Ok(()) Ok(())
} }
"Follow" => { "Follow" => {

View File

@ -91,6 +91,7 @@ fn main() {
routes::blogs::atom_feed, routes::blogs::atom_feed,
routes::comments::create, routes::comments::create,
routes::comments::delete,
routes::comments::activity_pub, routes::comments::activity_pub,
routes::instance::index, routes::instance::index,

View File

@ -7,7 +7,8 @@ use rocket_i18n::I18n;
use validator::Validate; use validator::Validate;
use template_utils::Ructe; use template_utils::Ructe;
use plume_common::{utils, activity_pub::{broadcast, ApRequest, ActivityStream}}; use plume_common::{utils, activity_pub::{broadcast, ApRequest,
ActivityStream, inbox::Deletable}};
use plume_models::{ use plume_models::{
blogs::Blog, blogs::Blog,
comments::*, comments::*,
@ -86,6 +87,18 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
}) })
} }
#[post("/~/<blog>/<slug>/comment/<id>/delete")]
pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Redirect {
if let Some(comment) = Comment::get(&*conn, id) {
if comment.author_id == user.id {
let dest = User::one_by_instance(&*conn);
let delete_activity = comment.delete(&*conn);
worker.execute(move || broadcast(&user, delete_activity, dest));
}
}
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))
}
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> { pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> {
Comment::get(&*conn, id).map(|c| ActivityStream::new(c.to_activity(&*conn))) Comment::get(&*conn, id).map(|c| ActivityStream::new(c.to_activity(&*conn)))

View File

@ -202,17 +202,18 @@ main .article-meta {
} }
// New comment form // New comment form
form input[type="submit"] { > form input[type="submit"] {
font-size: 1em; font-size: 1em;
} }
// Response button // Response/delete buttons
a.button { a.button, form.inline, form.inline input {
display: inline-block; display: inline-block;
padding: 0; padding: 0;
background: none; background: none;
color: $black; color: $black;
border: none; border: none;
margin-right: 2em;
&::before { &::before {
color: $purple; color: $purple;

View File

@ -17,7 +17,7 @@
fill: none; fill: none;
} }
.icon { .icon:before {
font-family: "Feather"; font-family: "Feather";
speak: none; speak: none;
font-style: normal; font-style: normal;

View File

@ -3,14 +3,14 @@
@use plume_models::users::User; @use plume_models::users::User;
@use routes::*; @use routes::*;
@(ctx: BaseContext, comm: &Comment, author: User, in_reply_to: Option<&str>) @(ctx: BaseContext, comm: &Comment, author: User, in_reply_to: Option<&str>, blog: &str, slug: &str)
<div class="comment u-comment h-cite" id="comment-@comm.id"> <div class="comment u-comment h-cite" id="comment-@comm.id">
<a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))"> <a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))">
@avatar(ctx.0, &author, Size::Small, true, ctx.1) @avatar(ctx.0, &author, Size::Small, true, ctx.1)
<span class="display-name p-name">@author.name(ctx.0)</span> <span class="display-name p-name">@author.name(ctx.0)</span>
<small>@author.get_fqn(ctx.0)</small> <small>@author.get_fqn(ctx.0)</small>
</a> </a>
@if let Some(ref ap_url) = comm.ap_url { @if let Some(ref ap_url) = comm.ap_url {
<a class="u-url" href="@ap_url"></a> <a class="u-url" href="@ap_url"></a>
} }
@ -28,7 +28,12 @@
} }
</div> </div>
<a class="button icon icon-message-circle" href="?responding_to=@comm.id">@i18n!(ctx.1, "Respond")</a> <a class="button icon icon-message-circle" href="?responding_to=@comm.id">@i18n!(ctx.1, "Respond")</a>
@if ctx.2.clone().map(|u| u.id == author.id).unwrap_or(false) {
<form class="inline icon icon-trash" method="post" action="@uri!(comments::delete: blog = blog, slug = slug, id = comm.id)">
<input onclick="return confirm('Are you sure you?')" type="submit" value="@i18n!(ctx.1, "Delete this comment")">
</form>
}
@for res in comm.get_responses(ctx.0) { @for res in comm.get_responses(ctx.0) {
@:comment(ctx, &res, res.get_author(ctx.0), comm.ap_url.as_ref().map(|u| &**u)) @:comment(ctx, &res, res.get_author(ctx.0), comm.ap_url.as_ref().map(|u| &**u), blog, slug)
} }
</div> </div>

View File

@ -146,7 +146,7 @@
@if !comments.is_empty() { @if !comments.is_empty() {
<div class="list"> <div class="list">
@for comm in comments { @for comm in comments {
@:comment(ctx, &comm, comm.get_author(ctx.0), Some(&article.ap_url)) @:comment(ctx, &comm, comm.get_author(ctx.0), Some(&article.ap_url), &blog.get_fqn(ctx.0), &article.slug)
} }
</div> </div>
} else { } else {