redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
end
+ def unsensitive
+ authorize @account, :unsensitive?
+ @account.unsensitize!
+ log_action :unsensitive, @account
+ redirect_to admin_account_path(@account.id)
+ end
+
def unsilence
authorize @account, :unsilence?
@account.unsilence!
active
pending
disabled
+ sensitized
silenced
suspended
username
render json: @account, serializer: REST::Admin::AccountSerializer
end
+ def unsensitive
+ authorize @account, :unsensitive?
+ @account.unsensitize!
+ log_action :unsensitive, @account
+ render json: @account, serializer: REST::Admin::AccountSerializer
+ end
+
def unsilence
authorize @account, :unsilence?
@account.unsilence!
end
end
+ def sensitized?(status, account)
+ if !account.nil? && account.id == status.account_id
+ status.sensitive
+ else
+ status.account.sensitized? || status.sensitive
+ end
+ end
+
private
def simplified_text(text)
created_at: @object['published'],
override_timestamps: @options[:override_timestamps],
reply: @object['inReplyTo'].present?,
- sensitive: @object['sensitive'] || false,
+ sensitive: @account.sensitized? || @object['sensitive'] || false,
visibility: visibility_from_audience,
thread: replied_to_status,
conversation: conversation_from_uri(@object['conversation']),
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
+# sensitized_at :datetime
#
class Account < ApplicationRecord
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
scope :silenced, -> { where.not(silenced_at: nil) }
scope :suspended, -> { where.not(suspended_at: nil) }
+ scope :sensitized, -> { where.not(sensitized_at: nil) }
scope :without_suspended, -> { where(suspended_at: nil) }
scope :without_silenced, -> { where(silenced_at: nil) }
scope :recent, -> { reorder(id: :desc) }
end
end
+ def sensitized?
+ sensitized_at.present?
+ end
+
+ def sensitize!(date = Time.now.utc)
+ update!(sensitized_at: date)
+ end
+
+ def unsensitize!
+ update!(sensitized_at: nil)
+ end
+
def memorialize!
update!(memorial: true)
end
#
class AccountWarning < ApplicationRecord
- enum action: %i(none disable silence suspend), _suffix: :action
+ enum action: %i(none disable sensitive silence suspend), _suffix: :action
belongs_to :account, inverse_of: :account_warnings
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings
TYPES = %w(
none
disable
+ sensitive
silence
suspend
).freeze
case type
when 'disable'
handle_disable!
+ when 'sensitive'
+ handle_sensitive!
when 'silence'
handle_silence!
when 'suspend'
target_account.user&.disable!
end
+ def handle_sensitive!
+ authorize(target_account, :sensitive?)
+ log_action(:sensitive, target_account)
+ target_account.sensitize!
+ end
+
def handle_silence!
authorize(target_account, :silence?)
log_action(:silence, target_account)
reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
+ sensitive_account: { target_type: 'Account', action: 'sensitive' }.freeze,
silence_account: { target_type: 'Account', action: 'silence' }.freeze,
suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
+ unsensitive_account: { target_type: 'Account', action: 'unsensitive' }.freeze,
unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
staff?
end
+ def sensitive?
+ staff? && !record.user&.staff?
+ end
+
+ def unsensitive?
+ staff?
+ end
+
def silence?
staff? && !record.user&.staff?
end
ActivityPub::TagManager.instance.cc(object)
end
+ def sensitive
+ object.account.sensitized? || object.sensitive
+ end
+
def virtual_tags
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis
end
end
end
+ def sensitive
+ if current_user? && current_user.account_id == object.account_id
+ object.sensitive
+ else
+ object.account.sensitized? || object.sensitive
+ end
+ end
+
def uri
ActivityPub::TagManager.instance.uri_for(object)
end
= t('admin.accounts.confirming')
- elsif @account.local? && !@account.user_approved?
= t('admin.accounts.pending')
+ - elsif @account.sensitized?
+ = t('admin.accounts.sensitive')
- else
= t('admin.accounts.no_limits_imposed')
.dashboard__counters__label= t 'admin.accounts.login_status'
- else
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user)
+ - if @account.sensitized?
+ = link_to t('admin.accounts.undo_sensitized'), unsensitive_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsensitive, @account)
+ - elsif !@account.local? || @account.user_approved?
+ = link_to t('admin.accounts.sensitive'), new_admin_account_action_path(@account.id, type: 'sensitive'), class: 'button' if can?(:sensitive, @account)
+
- if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
- elsif !@account.local? || @account.user_approved?
- if !status.media_attachments.empty?
- if status.media_attachments.first.video?
- video = status.media_attachments.first
- = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
+ = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 670, height: 380, detailed: true, inline: true, alt: video.description do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.media_attachments.first.audio?
- audio = status.media_attachments.first
= react_component :audio, src: full_asset_url(audio.file.url(:original)), poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
- = react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
+ = react_component :media_gallery, height: 380, sensitive: sensitized?(status, current_account), standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.preview_card
- = react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+ = react_component :card, sensitive: sensitized?(status, current_account), 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
.detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 }
- if !status.media_attachments.empty?
- if status.media_attachments.first.video?
- video = status.media_attachments.first
- = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do
+ = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 610, height: 343, inline: true, alt: video.description do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.media_attachments.first.audio?
- audio = status.media_attachments.first
= react_component :audio, src: full_asset_url(audio.file.url(:original)), poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
- = react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
+ = react_component :media_gallery, height: 343, sensitive: sensitized?(status, current_account), autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.preview_card
- = react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+ = react_component :card, sensitive: sensitized?(status, current_account), 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
search: Search
search_same_email_domain: Other users with the same e-mail domain
search_same_ip: Other users with the same IP
+ sensitive: Sensitive
+ sensitized: marked as sensitive
shared_inbox_url: Shared inbox URL
show:
created_reports: Made reports
time_in_queue: Waiting in queue %{time}
title: Accounts
unconfirmed_email: Unconfirmed email
+ undo_sensitized: Undo sensitive
undo_silenced: Undo silence
undo_suspension: Undo suspension
unsilenced_msg: Successfully unlimited %{username}'s account
reopen_report: Reopen Report
reset_password_user: Reset Password
resolve_report: Resolve Report
+ sensitive_account: Mark the media in your account as sensitive
silence_account: Silence Account
suspend_account: Suspend Account
unassigned_report: Unassign Report
+ unsensitive_account: Unmark the media in your account as sensitive
unsilence_account: Unsilence Account
unsuspend_account: Unsuspend Account
update_announcement: Update Announcement
reopen_report: "%{name} reopened report %{target}"
reset_password_user: "%{name} reset password of user %{target}"
resolve_report: "%{name} resolved report %{target}"
+ sensitive_account: "%{name} marked %{target}'s media as sensitive"
silence_account: "%{name} silenced %{target}'s account"
suspend_account: "%{name} suspended %{target}'s account"
unassigned_report: "%{name} unassigned report %{target}"
+ unsensitive_account: "%{name} unmarked %{target}'s media as sensitive"
unsilence_account: "%{name} unsilenced %{target}'s account"
unsuspend_account: "%{name} unsuspended %{target}'s account"
update_announcement: "%{name} updated announcement %{target}"
warning:
explanation:
disable: You can no longer login to your account or use it in any other way, but your profile and other data remains intact.
+ sensitive: Your uploaded media files and linked media will be treated as sensitive.
silence: You can still use your account but only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed, but we will retain some data to prevent you from evading the suspension.
get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}.
subject:
disable: Your account %{acct} has been frozen
none: Warning for %{acct}
+ sensitive: Your account %{acct} posting media has been marked as sensitive
silence: Your account %{acct} has been limited
suspend: Your account %{acct} has been suspended
title:
disable: Account frozen
none: Warning
+ sensitive: Your media has been marked as sensitive
silence: Account limited
suspend: Account suspended
welcome:
search: 検索
search_same_email_domain: 同じドメインのメールアドレスを使用しているユーザー
search_same_ip: 同じ IP のユーザーを検索
+ sensitive: 閲覧注意
+ sensitized: 閲覧注意済み
shared_inbox_url: Shared inbox URL
show:
created_reports: このアカウントで作られた通報
time_in_queue: "%{time} 待ち"
title: アカウント
unconfirmed_email: 確認待ちのメールアドレス
+ undo_sensitized: 閲覧注意から戻す
undo_silenced: サイレンスから戻す
undo_suspension: 停止から戻す
unsubscribe: 購読の解除
reopen_report: 通報を再度開く
reset_password_user: パスワードをリセット
resolve_report: 通報を解決済みにする
+ sensitive_account: アカウントのメディアを閲覧注意にマーク
silence_account: アカウントをサイレンス
suspend_account: アカウントを停止
unassigned_report: 通報の担当を解除
+ unsensitive_account: アカウントのメディアの閲覧注意マークを解除
unsilence_account: アカウントのサイレンスを解除
unsuspend_account: アカウントの停止を解除
update_announcement: お知らせを更新
reopen_report: "%{name} さんが通報 %{target} を再び開きました"
reset_password_user: "%{name} さんが %{target} さんのパスワードをリセットしました"
resolve_report: "%{name} さんが通報 %{target} を解決済みにしました"
+ sensitive_account: "%{name} さんが %{target} さんのメディアを閲覧注意にマークしました"
silence_account: "%{name} さんが %{target} さんをサイレンスにしました"
suspend_account: "%{name} さんが %{target} さんを停止しました"
unassigned_report: "%{name} さんが通報 %{target} の担当を外しました"
+ unsensitive_account: "%{name} さんが %{target} さんのメディアの閲覧注意を解除しました"
unsilence_account: "%{name} さんが %{target} さんのサイレンスを解除しました"
unsuspend_account: "%{name} さんが %{target} さんの停止を解除しました"
update_announcement: "%{name} さんがお知らせ %{target} を更新しました"
warning:
explanation:
disable: アカウントが凍結されている間、データはそのまま残りますが、凍結が解除されるまでは何の操作もできません。
+ sensitive: あなたのアップロードしたメディアファイルとリンク先のメディアは、閲覧注意として扱われます。
silence: あなたのアカウントは制限されていますが、あなたをフォローしているユーザーのみ、このサーバー上の投稿を見ることができます。そしてあなたは様々な公開リストから除外されるかもしれません。ただし、他のユーザーは手動であなたをフォローすることができます。
suspend: あなたのアカウントは停止されています。あなたの投稿とアップロードされたメディアファイルは、このサーバーとあなたのフォロワーが参加していたサーバーから完全に削除されました。
get_in_touch: このメールに返信することで %{instance} のスタッフと連絡を取ることができます。
subject:
disable: あなたのアカウント %{acct} は凍結されました
none: "%{acct} に対する警告"
+ sensitive: あなたのアカウント %{acct} の投稿メディアは閲覧注意とマークされました
silence: あなたのアカウント %{acct} はサイレンスにされました
suspend: あなたのアカウント %{acct} は停止されました
title:
disable: アカウントが凍結されました
none: 警告
+ sensitive: あなたのメディアが閲覧注意とマークされました
silence: アカウントがサイレンスにされました
suspend: アカウントが停止されました
welcome:
types:
disable: Freeze
none: Send a warning
+ sensitive: Sensitive
silence: Limit
suspend: Suspend
warning_preset_id: Use a warning preset
types:
disable: ログインを無効化
none: 何もしない
+ sensitive: 閲覧注意
silence: サイレンス
suspend: 停止しアカウントのデータを恒久的に削除する
warning_preset_id: プリセット警告文を使用
resources :accounts, only: [:index, :show, :destroy] do
member do
post :enable
+ post :unsensitive
post :unsilence
post :unsuspend
post :redownload
resources :accounts, only: [:index, :show, :destroy] do
member do
post :enable
+ post :unsensitive
post :unsilence
post :unsuspend
post :approve
--- /dev/null
+class AddSensitizedToAccounts < ActiveRecord::Migration[5.2]
+ def change
+ add_column :accounts, :sensitized_at, :datetime
+ end
+end
t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version"
t.string "devices_url"
+ t.datetime "sensitized_at"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
end
end
+ describe 'POST #unsensitive' do
+ before do
+ account.touch(:sensitized_at)
+ post :unsensitive, params: { id: account.id }
+ end
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', 'user'
+
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'unsensitives account' do
+ expect(account.reload.sensitized?).to be false
+ end
+ end
+
describe 'POST #unsilence' do
before do
account.touch(:silenced_at)
context 'account.local?' do
let(:account) { Fabricate(:account, domain: nil) }
- it 'returns ["none", "disable", "silence", "suspend"]' do
- expect(subject).to eq %w(none disable silence suspend)
+ it 'returns ["none", "disable", "sensitive", "silence", "suspend"]' do
+ expect(subject).to eq %w(none disable sensitive silence suspend)
end
end
context '!account.local?' do
let(:account) { Fabricate(:account, domain: 'hoge.com') }
- it 'returns ["silence", "suspend"]' do
- expect(subject).to eq %w(silence suspend)
+ it 'returns ["sensitive", "silence", "suspend"]' do
+ expect(subject).to eq %w(sensitive silence suspend)
end
end
end
let(:admin) { Fabricate(:user, admin: true).account }
let(:john) { Fabricate(:user).account }
- permissions :index?, :show?, :unsuspend?, :unsilence?, :remove_avatar?, :remove_header? do
+ permissions :index?, :show?, :unsuspend?, :unsensitive?, :unsilence?, :remove_avatar?, :remove_header? do
context 'staff' do
it 'permits' do
expect(subject).to permit(admin)