if deleted {
updated_comment.send_delete(&user, &conn)?;
} else {
- // TODO: undo delete
+ updated_comment.send_undo_delete(&user, &conn)?;
}
} else {
updated_comment.send_update(&user, &conn)?;
if deleted {
updated_community.send_delete(&user, &conn)?;
} else {
- // TODO: undo delete
+ updated_community.send_undo_delete(&user, &conn)?;
}
}
if deleted {
updated_post.send_delete(&user, &conn)?;
} else {
- // TODO: undo delete
+ updated_post.send_undo_delete(&user, &conn)?;
}
} else {
updated_post.send_update(&user, &conn)?;
)?;
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 {
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.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
Ok(
use crate::websocket::server::SendCommunityRoomMessage;
use activitystreams::object::kind::{NoteType, PageType};
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},
collection::UnorderedCollection,
context,
fn send_create(&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_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
}
pub trait ApubLikeableType {
fn send_like(&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 {
}
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.
#[allow(unused_variables)]
)?;
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 {
Like(Like),
Dislike(Dislike),
Delete(Delete),
+ Undo(Undo),
}
impl SharedAcceptedObjects {
SharedAcceptedObjects::Like(l) => l.like_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::Undo(d) => d.undo_props.get_object_base_box(),
}
}
}
(SharedAcceptedObjects::Delete(d), Some("Group")) => {
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.")),
}
}
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(¬e, &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())
+}
fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
+
+ fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+ unimplemented!()
+ }
}
impl FromApub for UserForm {
});
});
- describe('delete community', () => {
- test('/u/lemmy_beta deletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => {
+ describe('delete things', () => {
+ test('/u/lemmy_beta deletes and undeletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => {
// Create a test community
let communityName = 'test_community';
let communityForm: CommunityForm = {
}).then(d => d.json());
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
let deletePostForm: PostForm = {
name: postName,
}).then(d => d.json());
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
let deleteCommunityForm: CommunityForm = {
name: communityName,
}).then(d => d.json());
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);
});
});
});