Add undos for delete community, post, and comment.

This commit is contained in:
Dessalines 2020-05-01 15:01:29 -04:00
parent 2f1cd9976d
commit 5366797a4b
10 changed files with 495 additions and 6 deletions

View file

@ -341,7 +341,7 @@ impl Perform for Oper<EditComment> {
if deleted { if deleted {
updated_comment.send_delete(&user, &conn)?; updated_comment.send_delete(&user, &conn)?;
} else { } else {
// TODO: undo delete updated_comment.send_undo_delete(&user, &conn)?;
} }
} else { } else {
updated_comment.send_update(&user, &conn)?; updated_comment.send_update(&user, &conn)?;

View file

@ -384,7 +384,7 @@ impl Perform for Oper<EditCommunity> {
if deleted { if deleted {
updated_community.send_delete(&user, &conn)?; updated_community.send_delete(&user, &conn)?;
} else { } else {
// TODO: undo delete updated_community.send_undo_delete(&user, &conn)?;
} }
} }

View file

@ -545,7 +545,7 @@ impl Perform for Oper<EditPost> {
if deleted { if deleted {
updated_post.send_delete(&user, &conn)?; updated_post.send_delete(&user, &conn)?;
} else { } else {
// TODO: undo delete updated_post.send_undo_delete(&user, &conn)?;
} }
} else { } else {
updated_post.send_update(&user, &conn)?; updated_post.send_update(&user, &conn)?;

View file

@ -202,6 +202,59 @@ impl ApubObjectType for Comment {
)?; )?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
// Generate a fake delete activity, with the correct object
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
&community.get_followers_url(),
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
&community.get_followers_url(),
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: self.creator_id,
data: serde_json::to_value(&undo)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
send_activity(
&undo,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
community.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
} }
impl ApubLikeableType for Comment { impl ApubLikeableType for Comment {

View file

@ -137,6 +137,50 @@ impl ActorType for Community {
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let group = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(&mut delete.object_props, &self.get_followers_url(), &id)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(group)?;
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(&mut undo.object_props, &self.get_followers_url(), &undo_id)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: self.creator_id,
data: serde_json::to_value(&undo)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
// Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor.
// But for delete, the creator is the actor, and does the signing
send_activity(
&undo,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
self.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
/// For a given community, returns the inboxes of all followers. /// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> { fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
Ok( Ok(

View file

@ -13,7 +13,7 @@ use crate::api::community::CommunityResponse;
use crate::websocket::server::SendCommunityRoomMessage; use crate::websocket::server::SendCommunityRoomMessage;
use activitystreams::object::kind::{NoteType, PageType}; use activitystreams::object::kind::{NoteType, PageType};
use activitystreams::{ use activitystreams::{
activity::{Accept, Create, Delete, Dislike, Follow, Like, Update}, activity::{Accept, Create, Delete, Dislike, Follow, Like, Undo, Update},
actor::{properties::ApActorProperties, Actor, Group, Person}, actor::{properties::ApActorProperties, Actor, Group, Person},
collection::UnorderedCollection, collection::UnorderedCollection,
context, context,
@ -196,11 +196,13 @@ pub trait ApubObjectType {
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
} }
pub trait ApubLikeableType { pub trait ApubLikeableType {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
// TODO add send_undo_like / undo_dislike
} }
pub fn get_shared_inbox(actor_id: &str) -> String { pub fn get_shared_inbox(actor_id: &str) -> String {
@ -235,6 +237,7 @@ pub trait ActorType {
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
// TODO default because there is no user following yet. // TODO default because there is no user following yet.
#[allow(unused_variables)] #[allow(unused_variables)]

View file

@ -212,6 +212,57 @@ impl ApubObjectType for Post {
)?; )?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
&community.get_followers_url(),
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
&community.get_followers_url(),
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: self.creator_id,
data: serde_json::to_value(&undo)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let community = Community::read(conn, self.community_id)?;
send_activity(
&undo,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
community.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
} }
impl ApubLikeableType for Post { impl ApubLikeableType for Post {

View file

@ -8,6 +8,7 @@ pub enum SharedAcceptedObjects {
Like(Like), Like(Like),
Dislike(Dislike), Dislike(Dislike),
Delete(Delete), Delete(Delete),
Undo(Undo),
} }
impl SharedAcceptedObjects { impl SharedAcceptedObjects {
@ -18,6 +19,7 @@ impl SharedAcceptedObjects {
SharedAcceptedObjects::Like(l) => l.like_props.get_object_base_box(), SharedAcceptedObjects::Like(l) => l.like_props.get_object_base_box(),
SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_object_base_box(), SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_object_base_box(),
SharedAcceptedObjects::Delete(d) => d.delete_props.get_object_base_box(), SharedAcceptedObjects::Delete(d) => d.delete_props.get_object_base_box(),
SharedAcceptedObjects::Undo(d) => d.undo_props.get_object_base_box(),
} }
} }
} }
@ -72,6 +74,9 @@ pub async fn shared_inbox(
(SharedAcceptedObjects::Delete(d), Some("Group")) => { (SharedAcceptedObjects::Delete(d), Some("Group")) => {
receive_delete_community(&d, &request, &conn, chat_server) receive_delete_community(&d, &request, &conn, chat_server)
} }
(SharedAcceptedObjects::Undo(u), Some("Delete")) => {
receive_undo_delete(&u, &request, &conn, chat_server)
}
_ => Err(format_err!("Unknown incoming activity type.")), _ => Err(format_err!("Unknown incoming activity type.")),
} }
} }
@ -721,3 +726,241 @@ fn receive_delete_comment(
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_undo_delete(
undo: &Undo,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let delete = undo
.undo_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Delete>()?;
let type_ = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.kind()
.unwrap();
match type_ {
"Note" => receive_undo_delete_comment(&delete, &request, &conn, chat_server),
"Page" => receive_undo_delete_post(&delete, &request, &conn, chat_server),
"Group" => receive_undo_delete_community(&delete, &request, &conn, chat_server),
d => Err(format_err!("Undo Delete type {} not supported", d)),
}
}
fn receive_undo_delete_comment(
delete: &Delete,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let note = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&delete)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
let comment = Comment::read_from_apub_id(conn, &comment_ap_id)?;
let comment_form = CommentForm {
content: comment.content.to_owned(),
parent_id: comment.parent_id,
post_id: comment.post_id,
creator_id: comment.creator_id,
removed: None,
deleted: Some(false),
read: None,
published: None,
updated: Some(naive_now()),
ap_id: comment.ap_id,
local: comment.local,
};
Comment::update(&conn, comment.id, &comment_form)?;
// Refetch the view
let comment_view = CommentView::read(&conn, comment.id, None)?;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment: comment_view,
recipient_ids,
};
chat_server.do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_undo_delete_post(
delete: &Delete,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let page = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Page>()?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&delete)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
let post = Post::read_from_apub_id(conn, &post_ap_id)?;
let post_form = PostForm {
name: post.name.to_owned(),
url: post.url.to_owned(),
body: post.body.to_owned(),
creator_id: post.creator_id.to_owned(),
community_id: post.community_id,
removed: None,
deleted: Some(false),
nsfw: post.nsfw,
locked: None,
stickied: None,
updated: Some(naive_now()),
embed_title: post.embed_title,
embed_description: post.embed_description,
embed_html: post.embed_html,
thumbnail_url: post.thumbnail_url,
ap_id: post.ap_id,
local: post.local,
published: None,
};
Post::update(&conn, post.id, &post_form)?;
// Refetch the view
let post_view = PostView::read(&conn, post.id, None)?;
let res = PostResponse { post: post_view };
chat_server.do_send(SendPost {
op: UserOperation::EditPost,
post: res,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_undo_delete_community(
delete: &Delete,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let group = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<GroupExt>()?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&delete)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
let community = Community::read_from_actor_id(conn, &community_actor_id)?;
let community_form = CommunityForm {
name: community.name.to_owned(),
title: community.title.to_owned(),
description: community.description.to_owned(),
category_id: community.category_id, // Note: need to keep this due to foreign key constraint
creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint
removed: None,
published: None,
updated: Some(naive_now()),
deleted: Some(false),
nsfw: community.nsfw,
actor_id: community.actor_id,
local: community.local,
private_key: community.private_key,
public_key: community.public_key,
last_refreshed_at: None,
};
Community::update(&conn, community.id, &community_form)?;
let res = CommunityResponse {
community: CommunityView::read(&conn, community.id, None)?,
};
chat_server.do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id: community.id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}

View file

@ -94,6 +94,10 @@ impl ActorType for User_ {
fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!() unimplemented!()
} }
fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
} }
impl FromApub for UserForm { impl FromApub for UserForm {

View file

@ -328,8 +328,8 @@ describe('main', () => {
}); });
}); });
describe('delete community', () => { describe('delete things', () => {
test('/u/lemmy_beta deletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => { test('/u/lemmy_beta deletes and undeletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => {
// Create a test community // Create a test community
let communityName = 'test_community'; let communityName = 'test_community';
let communityForm: CommunityForm = { let communityForm: CommunityForm = {
@ -452,6 +452,34 @@ describe('main', () => {
}).then(d => d.json()); }).then(d => d.json());
expect(getPostRes.comments[0].deleted).toBe(true); expect(getPostRes.comments[0].deleted).toBe(true);
// lemmy_beta undeletes the comment
let undeleteCommentForm: CommentForm = {
content: commentContent,
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: false,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let undeleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(undeleteCommentForm),
}
).then(d => d.json());
expect(undeleteCommentRes.comment.deleted).toBe(false);
// lemmy_alpha sees that the comment is undeleted
let getPostUndeleteRes: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
}).then(d => d.json());
expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
// lemmy_beta deletes the post // lemmy_beta deletes the post
let deletePostForm: PostForm = { let deletePostForm: PostForm = {
name: postName, name: postName,
@ -478,6 +506,35 @@ describe('main', () => {
}).then(d => d.json()); }).then(d => d.json());
expect(getPostResAgain.post.deleted).toBe(true); expect(getPostResAgain.post.deleted).toBe(true);
// lemmy_beta undeletes the post
let undeletePostForm: PostForm = {
name: postName,
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: false,
};
let undeletePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(undeletePostForm),
}
).then(d => d.json());
expect(undeletePostRes.post.deleted).toBe(false);
// Make sure lemmy_alpha sees the post is undeleted
let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
}).then(d => d.json());
expect(getPostResAgainTwo.post.deleted).toBe(false);
// lemmy_beta deletes the community // lemmy_beta deletes the community
let deleteCommunityForm: CommunityForm = { let deleteCommunityForm: CommunityForm = {
name: communityName, name: communityName,
@ -510,6 +567,40 @@ describe('main', () => {
}).then(d => d.json()); }).then(d => d.json());
expect(getCommunityRes.community.deleted).toBe(true); expect(getCommunityRes.community.deleted).toBe(true);
// lemmy_beta undeletes the community
let undeleteCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: false,
auth: lemmyBetaAuth,
};
let undeleteCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(undeleteCommunityForm),
}
).then(d => d.json());
// Make sure the delete went through
expect(undeleteCommunityRes.community.deleted).toBe(false);
// Re-get it from alpha, make sure its deleted there too
let getCommunityResAgain: GetCommunityResponse = await fetch(
getCommunityUrl,
{
method: 'GET',
}
).then(d => d.json());
expect(getCommunityResAgain.community.deleted).toBe(false);
}); });
}); });
}); });