forked from treehouse/mastodon
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 statusessignup-info-prompt
parent
5ab1e0e738
commit
282ea17078
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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddDeletedAtToStatuses < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :statuses, :deleted_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -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…
Reference in New Issue