Add soft delete for statuses for instant deletes through API (#11623)
* Add soft delete for statuses to allow them to appear instant * Allow reporting soft-deleted statuses and show them in the admin UI * Change index for getting an account's statuses
This commit is contained in:
		
					parent
					
						
							
								5ab1e0e738
							
						
					
				
			
			
				commit
				
					
						282ea17078
					
				
			
		
					 14 changed files with 42 additions and 8 deletions
				
			
		
							
								
								
									
										1
									
								
								Gemfile
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
										
									
									
									
								
							|  | @ -43,6 +43,7 @@ gem 'omniauth-cas', '~> 1.1' | ||||||
| gem 'omniauth-saml', '~> 1.10' | gem 'omniauth-saml', '~> 1.10' | ||||||
| gem 'omniauth', '~> 1.9' | gem 'omniauth', '~> 1.9' | ||||||
| 
 | 
 | ||||||
|  | gem 'discard', '~> 1.1' | ||||||
| gem 'doorkeeper', '~> 5.1' | gem 'doorkeeper', '~> 5.1' | ||||||
| gem 'fast_blank', '~> 1.0' | gem 'fast_blank', '~> 1.0' | ||||||
| gem 'fastimage' | gem 'fastimage' | ||||||
|  |  | ||||||
|  | @ -204,6 +204,8 @@ GEM | ||||||
|       devise (>= 4.0.0) |       devise (>= 4.0.0) | ||||||
|       rpam2 (~> 4.0) |       rpam2 (~> 4.0) | ||||||
|     diff-lcs (1.3) |     diff-lcs (1.3) | ||||||
|  |     discard (1.1.0) | ||||||
|  |       activerecord (>= 4.2, < 7) | ||||||
|     docile (1.3.2) |     docile (1.3.2) | ||||||
|     domain_name (0.5.20180417) |     domain_name (0.5.20180417) | ||||||
|       unf (>= 0.0.5, < 1.0.0) |       unf (>= 0.0.5, < 1.0.0) | ||||||
|  | @ -692,6 +694,7 @@ DEPENDENCIES | ||||||
|   devise (~> 4.6) |   devise (~> 4.6) | ||||||
|   devise-two-factor (~> 3.1) |   devise-two-factor (~> 3.1) | ||||||
|   devise_pam_authenticatable2 (~> 9.2) |   devise_pam_authenticatable2 (~> 9.2) | ||||||
|  |   discard (~> 1.1) | ||||||
|   doorkeeper (~> 5.1) |   doorkeeper (~> 5.1) | ||||||
|   dotenv-rails (~> 2.7) |   dotenv-rails (~> 2.7) | ||||||
|   fabrication (~> 2.20) |   fabrication (~> 2.20) | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def reported_status_ids |   def reported_status_ids | ||||||
|     reported_account.statuses.find(status_ids).pluck(:id) |     reported_account.statuses.with_discarded.find(status_ids).pluck(:id) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def status_ids |   def status_ids | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController | ||||||
|     @reblogs_map = { @status.id => false } |     @reblogs_map = { @status.id => false } | ||||||
| 
 | 
 | ||||||
|     authorize status_for_destroy, :unreblog? |     authorize status_for_destroy, :unreblog? | ||||||
|  |     status_for_destroy.discard | ||||||
|     RemovalWorker.perform_async(status_for_destroy.id) |     RemovalWorker.perform_async(status_for_destroy.id) | ||||||
| 
 | 
 | ||||||
|     render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map) |     render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map) | ||||||
|  | @ -30,7 +31,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def status_for_destroy |   def status_for_destroy | ||||||
|     current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! |     @status_for_destroy ||= current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def reblog_params |   def reblog_params | ||||||
|  |  | ||||||
|  | @ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController | ||||||
|     @status = Status.where(account_id: current_user.account).find(params[:id]) |     @status = Status.where(account_id: current_user.account).find(params[:id]) | ||||||
|     authorize @status, :destroy? |     authorize @status, :destroy? | ||||||
| 
 | 
 | ||||||
|  |     @status.discard | ||||||
|     RemovalWorker.perform_async(@status.id, redraft: true) |     RemovalWorker.perform_async(@status.id, redraft: true) | ||||||
| 
 | 
 | ||||||
|     render json: @status, serializer: REST::StatusSerializer, source_requested: true |     render json: @status, serializer: REST::StatusSerializer, source_requested: true | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ class Form::StatusBatch | ||||||
| 
 | 
 | ||||||
|   def delete_statuses |   def delete_statuses | ||||||
|     Status.where(id: status_ids).reorder(nil).find_each do |status| |     Status.where(id: status_ids).reorder(nil).find_each do |status| | ||||||
|  |       status.discard | ||||||
|       RemovalWorker.perform_async(status.id, redraft: false) |       RemovalWorker.perform_async(status.id, redraft: false) | ||||||
|       Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) |       Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) | ||||||
|       log_action :destroy, status |       log_action :destroy, status | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ class Report < ApplicationRecord | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def statuses |   def statuses | ||||||
|     Status.where(id: status_ids).includes(:account, :media_attachments, :mentions) |     Status.with_discarded.where(id: status_ids).includes(:account, :media_attachments, :mentions) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def media_attachments |   def media_attachments | ||||||
|  |  | ||||||
|  | @ -22,15 +22,19 @@ | ||||||
| #  application_id         :bigint(8) | #  application_id         :bigint(8) | ||||||
| #  in_reply_to_account_id :bigint(8) | #  in_reply_to_account_id :bigint(8) | ||||||
| #  poll_id                :bigint(8) | #  poll_id                :bigint(8) | ||||||
|  | #  deleted_at             :datetime | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| class Status < ApplicationRecord | class Status < ApplicationRecord | ||||||
|   before_destroy :unlink_from_conversations |   before_destroy :unlink_from_conversations | ||||||
| 
 | 
 | ||||||
|  |   include Discard::Model | ||||||
|   include Paginable |   include Paginable | ||||||
|   include Cacheable |   include Cacheable | ||||||
|   include StatusThreadingConcern |   include StatusThreadingConcern | ||||||
| 
 | 
 | ||||||
|  |   self.discard_column = :deleted_at | ||||||
|  | 
 | ||||||
|   # If `override_timestamps` is set at creation time, Snowflake ID creation |   # If `override_timestamps` is set at creation time, Snowflake ID creation | ||||||
|   # will be based on current time instead of `created_at` |   # will be based on current time instead of `created_at` | ||||||
|   attr_accessor :override_timestamps |   attr_accessor :override_timestamps | ||||||
|  | @ -72,7 +76,7 @@ class Status < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   accepts_nested_attributes_for :poll |   accepts_nested_attributes_for :poll | ||||||
| 
 | 
 | ||||||
|   default_scope { recent } |   default_scope { recent.kept } | ||||||
| 
 | 
 | ||||||
|   scope :recent, -> { reorder(id: :desc) } |   scope :recent, -> { reorder(id: :desc) } | ||||||
|   scope :remote, -> { where(local: false).where.not(uri: nil) } |   scope :remote, -> { where(local: false).where.not(uri: nil) } | ||||||
|  |  | ||||||
|  | @ -16,11 +16,14 @@ | ||||||
|         - video = status.proper.media_attachments.first |         - video = status.proper.media_attachments.first | ||||||
|         = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description |         = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description | ||||||
|       - else |       - else | ||||||
|         = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } |         = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } | ||||||
| 
 | 
 | ||||||
|     .detailed-status__meta |     .detailed-status__meta | ||||||
|       = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do |       = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do | ||||||
|         %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) |         %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) | ||||||
|  |       - if status.discarded? | ||||||
|  |         · | ||||||
|  |         %span.negative-hint= t('admin.statuses.deleted') | ||||||
|       · |       · | ||||||
|       - if status.reblog? |       - if status.reblog? | ||||||
|         = fa_icon('retweet fw') |         = fa_icon('retweet fw') | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ class RemovalWorker | ||||||
|   include Sidekiq::Worker |   include Sidekiq::Worker | ||||||
| 
 | 
 | ||||||
|   def perform(status_id, options = {}) |   def perform(status_id, options = {}) | ||||||
|     RemoveStatusService.new.call(Status.find(status_id), **options.symbolize_keys) |     RemoveStatusService.new.call(Status.with_discarded.find(status_id), **options.symbolize_keys) | ||||||
|   rescue ActiveRecord::RecordNotFound |   rescue ActiveRecord::RecordNotFound | ||||||
|     true |     true | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -499,6 +499,7 @@ en: | ||||||
|         delete: Delete |         delete: Delete | ||||||
|         nsfw_off: Mark as not sensitive |         nsfw_off: Mark as not sensitive | ||||||
|         nsfw_on: Mark as sensitive |         nsfw_on: Mark as sensitive | ||||||
|  |       deleted: Deleted | ||||||
|       failed_to_execute: Failed to execute |       failed_to_execute: Failed to execute | ||||||
|       media: |       media: | ||||||
|         title: Media |         title: Media | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								db/migrate/20190819134503_add_deleted_at_to_statuses.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20190819134503_add_deleted_at_to_statuses.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | class AddDeletedAtToStatuses < ActiveRecord::Migration[5.2] | ||||||
|  |   def change | ||||||
|  |     add_column :statuses, :deleted_at, :datetime | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										13
									
								
								db/migrate/20190820003045_update_statuses_index.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								db/migrate/20190820003045_update_statuses_index.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | class UpdateStatusesIndex < ActiveRecord::Migration[5.2] | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], where: 'deleted_at IS NULL', order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20190820 } | ||||||
|  |     remove_index :statuses, name: :index_statuses_20180106 | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 } | ||||||
|  |     remove_index :statuses, name: :index_statuses_20190820 | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
| 
 | 
 | ||||||
| ActiveRecord::Schema.define(version: 2019_08_15_225426) do | ActiveRecord::Schema.define(version: 2019_08_20_003045) do | ||||||
| 
 | 
 | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
|  | @ -644,7 +644,8 @@ ActiveRecord::Schema.define(version: 2019_08_15_225426) do | ||||||
|     t.bigint "application_id" |     t.bigint "application_id" | ||||||
|     t.bigint "in_reply_to_account_id" |     t.bigint "in_reply_to_account_id" | ||||||
|     t.bigint "poll_id" |     t.bigint "poll_id" | ||||||
|     t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc } |     t.datetime "deleted_at" | ||||||
|  |     t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)" | ||||||
|     t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id" |     t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id" | ||||||
|     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id" |     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id" | ||||||
|     t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id" |     t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue