From 586c8efbd8e2ef024c5bba73b0e7d4b590a61df0 Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Tue, 10 Nov 2020 15:40:48 +1100 Subject: [PATCH] FEATURE: the ability to permanently destroy the private message (#11115) PostDestroyer should accept the option to permanently destroy post from the database. In addition, when the first post is destroyed it destroys the whole topic. Currently, that feature is limited to private messages and creator of the post. It will be used by discourse-encrypt to explode encrypted private messages. --- .../discourse/app/controllers/topic.js | 6 +++++ .../discourse/app/models/post-stream.js | 6 +++++ .../app/models/topic-tracking-state.js | 12 ++++++++++ app/models/topic_tracking_state.rb | 11 +++++++++ lib/post_destroyer.rb | 17 ++++++++++---- lib/svg_sprite/svg_sprite.rb | 1 + spec/components/post_destroyer_spec.rb | 23 +++++++++++++++++++ 7 files changed, 71 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index b5e7e400dbf..9d73b1b4bf8 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -1424,6 +1424,12 @@ export default Controller.extend(bufferedProperty("model"), { .then(() => refresh({ id: data.id })); break; } + case "destroyed": { + postStream + .triggerDestroyedPost(data.id) + .then(() => refresh({ id: data.id })); + break; + } case "recovered": { postStream .triggerRecoveredPost(data.id) diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js index d68c2cac413..42e5da69fea 100644 --- a/app/assets/javascripts/discourse/app/models/post-stream.js +++ b/app/assets/javascripts/discourse/app/models/post-stream.js @@ -737,6 +737,12 @@ export default RestModel.extend({ return Promise.resolve(); }, + triggerDestroyedPost(postId) { + const existing = this._identityMap[postId]; + this.removePosts([existing]); + return Promise.resolve(); + }, + triggerChangedPost(postId, updatedAt, opts) { opts = opts || {}; diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index ee02683517f..4d209f5fee7 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -6,6 +6,7 @@ import PreloadStore from "discourse/lib/preload-store"; import Category from "discourse/models/category"; import User from "discourse/models/user"; import { deepEqual } from "discourse-common/lib/object"; +import DiscourseURL from "discourse/lib/url"; function isNew(topic) { return ( @@ -148,6 +149,17 @@ const TopicTrackingState = EmberObject.extend({ } tracker.incrementMessageCount(); }); + + this.messageBus.subscribe("/destroy", (msg) => { + tracker.incrementMessageCount(); + const currentRoute = DiscourseURL.router.currentRoute.parent; + if ( + currentRoute.name === "topic" && + parseInt(currentRoute.params.id, 10) === msg.topic_id + ) { + DiscourseURL.redirectTo("/"); + } + }); }, mutedTopics() { diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 9f8b5a66af3..e018d961328 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -171,6 +171,17 @@ class TopicTrackingState MessageBus.publish("/delete", message.as_json, group_ids: group_ids) end + def self.publish_destroy(topic) + group_ids = topic.category && topic.category.secure_group_ids + + message = { + topic_id: topic.id, + message_type: "destroy" + } + + MessageBus.publish("/destroy", message.as_json, group_ids: group_ids) + end + def self.publish_read(topic_id, last_read_post_number, user_id, notification_level = nil) highest_post_number = DB.query_single("SELECT highest_post_number FROM topics WHERE id = ?", topic_id).first diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 0b569ff405d..e5bff487bb0 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -66,7 +66,7 @@ class PostDestroyer delete_removed_posts_after = @opts[:delete_removed_posts_after] || SiteSetting.delete_removed_posts_after - if delete_removed_posts_after < 1 || post_is_reviewable? || Guardian.new(@user).can_moderate_topic?(topic) + if delete_removed_posts_after < 1 || post_is_reviewable? || Guardian.new(@user).can_moderate_topic?(topic) || permanent? perform_delete elsif @user.id == @post.user_id mark_for_deletion(delete_removed_posts_after) @@ -140,9 +140,10 @@ class PostDestroyer # When a post is properly deleted. Well, it's still soft deleted, but it will no longer # show up in the topic + # Permanent option allows to hard delete. def perform_delete Post.transaction do - @post.trash!(@user) + permanent? ? @post.destroy! : @post.trash!(@user) if @post.topic make_previous_post_the_last_one mark_topic_changed @@ -162,7 +163,9 @@ class PostDestroyer end end - @post.topic.trash!(@user) if @post.topic && @post.is_first_post? + if @post.topic && @post.is_first_post? + permanent? ? @post.topic.destroy! : @post.topic.trash!(@user) + end update_associated_category_latest_topic update_user_counts TopicUser.update_post_action_cache(post_id: @post.id) @@ -178,8 +181,12 @@ class PostDestroyer update_imap_sync(@post, true) if @post.topic&.deleted_at feature_users_in_the_topic if @post.topic - @post.publish_change_to_clients! :deleted if @post.topic - TopicTrackingState.publish_delete(@post.topic) if @post.topic && @post.post_number == 1 + @post.publish_change_to_clients!(permanent? ? :destroyed : :deleted) if @post.topic + TopicTrackingState.send(permanent? ? :publish_destroy : :publish_delete, @post.topic) if @post.topic && @post.post_number == 1 + end + + def permanent? + @opts[:permanent] && @user == @post.user && @post.topic.private_message? end # When a user 'deletes' their own post. We just change the text. diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index 364e70aecb5..be68b980f5e 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -174,6 +174,7 @@ module SvgSprite "star", "step-backward", "step-forward", + "stopwatch", "stream", "sync-alt", "sync", diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index c6ac8e5dd7b..be1f3aa9c27 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -961,4 +961,27 @@ describe PostDestroyer do expect(user.user_profile.reload.featured_topic).to eq(nil) end end + + describe "permanent destroy" do + fab!(:private_message_topic) { Fabricate(:private_message_topic) } + fab!(:private_post) { Fabricate(:private_message_post, topic: private_message_topic) } + fab!(:reply) { Fabricate(:private_message_post, topic: private_message_topic) } + it "destroys the post and topic if deleting first post" do + PostDestroyer.new(reply.user, reply, permanent: true).destroy + expect { reply.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(private_message_topic.reload.persisted?).to be true + + PostDestroyer.new(private_post.user, private_post, permanent: true).destroy + expect { private_post.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { private_message_topic.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'soft delete if not creator of post or not private message' do + PostDestroyer.new(moderator, reply, permanent: true).destroy + expect(reply.deleted_at).not_to eq(nil) + + PostDestroyer.new(post.user, post, permanent: true).destroy + expect(post.user_deleted).to be true + end + end end