Add notifications for statuses deleted by moderators (#17204)

This commit is contained in:
Eugen Rochko 2022-01-17 09:41:33 +01:00 committed by GitHub
parent d5c9feb7b7
commit 14f436c457
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1220 additions and 598 deletions

View File

@ -14,7 +14,7 @@ module Admin
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
@warnings = @account.strikes.custom.latest
render template: 'admin/accounts/show'
end

View File

@ -28,7 +28,7 @@ module Admin
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
@warnings = @account.strikes.custom.latest
@domain_block = DomainBlock.rule_for(@account.domain)
end

View File

@ -14,20 +14,17 @@ module Admin
if params[:create_and_resolve]
@report.resolve!(current_account)
log_action :resolve, @report
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
return
end
if params[:create_and_unresolve]
elsif params[:create_and_unresolve]
@report.unresolve!
log_action :reopen, @report
end
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
else
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
@form = Form::StatusBatch.new
@report_notes = @report.notes.includes(:account).order(id: :desc)
@action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes
render template: 'admin/reports/show'
end
@ -41,6 +38,14 @@ module Admin
private
def after_create_redirect_path
if params[:create_and_resolve]
admin_reports_path
else
admin_report_path(@report)
end
end
def resource_params
params.require(:report_note).permit(
:content,

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
module Admin
class ReportedStatusesController < BaseController
before_action :set_report
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_report_path(@report)
end
private
def status_params
params.require(:status).permit(:sensitive)
end
def form_status_batch_params
params.require(:form_status_batch).permit(status_ids: [])
end
def action_from_button
if params[:nsfw_on]
'nsfw_on'
elsif params[:nsfw_off]
'nsfw_off'
elsif params[:delete]
'delete'
end
end
def set_report
@report = Report.find(params[:report_id])
end
end
end

View File

@ -13,8 +13,10 @@ module Admin
authorize @report, :show?
@report_note = @report.notes.new
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
@form = Form::StatusBatch.new
@report_notes = @report.notes.includes(:account).order(id: :desc)
@action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes
end
def assign_to_self

View File

@ -2,71 +2,57 @@
module Admin
class StatusesController < BaseController
helper_method :current_params
before_action :set_account
before_action :set_statuses
PER_PAGE = 20
def index
authorize :status, :index?
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media]
@statuses = @statuses.merge(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc')
@status_batch_action = Admin::StatusBatchAction.new
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
end
def show
authorize :status, :index?
@statuses = @account.statuses.where(id: params[:id])
authorize @statuses.first, :show?
@form = Form::StatusBatch.new
end
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
def batch
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
@status_batch_action.save!
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_account_statuses_path(@account.id, current_params)
ensure
redirect_to after_create_redirect_path
end
private
def form_status_batch_params
params.require(:form_status_batch).permit(:action, status_ids: [])
def admin_status_batch_action_params
params.require(:admin_status_batch_action).permit(status_ids: [])
end
def after_create_redirect_path
if @status_batch_action.report_id.present?
admin_report_path(@status_batch_action.report_id)
else
admin_account_statuses_path(params[:account_id], current_params)
end
end
def set_account
@account = Account.find(params[:account_id])
end
def current_params
page = (params[:page] || 1).to_i
def set_statuses
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
end
{
media: params[:media],
page: page > 1 && page,
}.select { |_, value| value.present? }
def filter_params
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
end
def action_from_button
if params[:nsfw_on]
'nsfw_on'
elsif params[:nsfw_off]
'nsfw_off'
if params[:report]
'report'
elsif params[:remove_from_report]
'remove_from_report'
elsif params[:delete]
'delete'
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
class Api::V1::Admin::AccountActionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
protect_from_forgery with: :exception
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
before_action :require_staff!
before_action :set_account

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
class Api::V1::Admin::AccountsController < Api::BaseController
protect_from_forgery with: :exception
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff!
before_action :set_accounts, only: :index
before_action :set_account, except: :index

View File

@ -3,6 +3,7 @@
class Api::V1::Admin::DimensionsController < Api::BaseController
protect_from_forgery with: :exception
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_dimensions

View File

@ -3,6 +3,7 @@
class Api::V1::Admin::MeasuresController < Api::BaseController
protect_from_forgery with: :exception
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_measures

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
class Api::V1::Admin::ReportsController < Api::BaseController
protect_from_forgery with: :exception
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
before_action :require_staff!
before_action :set_reports, only: :index
before_action :set_report, except: :index
@ -32,6 +34,12 @@ class Api::V1::Admin::ReportsController < Api::BaseController
render json: @report, serializer: REST::Admin::ReportSerializer
end
def update
authorize @report, :update?
@report.update!(report_params)
render json: @report, serializer: REST::Admin::ReportSerializer
end
def assign_to_self
authorize @report, :update?
@report.update!(assigned_account_id: current_account.id)
@ -74,6 +82,10 @@ class Api::V1::Admin::ReportsController < Api::BaseController
ReportFilter.new(filter_params).results
end
def report_params
params.permit(:category, rule_ids: [])
end
def filter_params
params.permit(*FILTER_PARAMS)
end

View File

@ -3,6 +3,7 @@
class Api::V1::Admin::RetentionController < Api::BaseController
protect_from_forgery with: :exception
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_cohorts

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true
class Api::V1::Admin::Trends::TagsController < Api::BaseController
protect_from_forgery with: :exception
before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_tags

View File

@ -13,6 +13,7 @@ module Admin::FilterHelper
RelationshipFilter::KEYS,
AnnouncementFilter::KEYS,
Admin::ActionLogFilter::KEYS,
Admin::StatusFilter::KEYS,
].flatten.freeze
def filter_link_to(text, link_to_params, link_class_params = link_to_params)

View File

@ -0,0 +1,159 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'mastodon/api';
import { injectIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
const messages = defineMessages({
other: { id: 'report.categories.other', defaultMessage: 'Other' },
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
});
class Category extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
selected: PropTypes.bool,
disabled: PropTypes.bool,
onSelect: PropTypes.func,
children: PropTypes.node,
};
handleClick = () => {
const { id, disabled, onSelect } = this.props;
if (!disabled) {
onSelect(id);
}
};
render () {
const { id, text, disabled, selected, children } = this.props;
return (
<div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
{selected && <input type='hidden' name='report[category]' value={id} />}
<div className='report-reason-selector__category__label'>
<span className={classNames('poll__input', { active: selected, disabled })} />
{text}
</div>
{(selected && children) && (
<div className='report-reason-selector__category__rules'>
{children}
</div>
)}
</div>
);
}
}
class Rule extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
selected: PropTypes.bool,
disabled: PropTypes.bool,
onToggle: PropTypes.func,
};
handleClick = () => {
const { id, disabled, onToggle } = this.props;
if (!disabled) {
onToggle(id);
}
};
render () {
const { id, text, disabled, selected } = this.props;
return (
<div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
{text}
</div>
);
}
}
export default @injectIntl
class ReportReasonSelector extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
rule_ids: PropTypes.arrayOf(PropTypes.string),
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
state = {
category: this.props.category,
rule_ids: this.props.rule_ids || [],
rules: [],
};
componentDidMount() {
api().get('/api/v1/instance').then(res => {
this.setState({
rules: res.data.rules,
});
}).catch(err => {
console.error(err);
});
}
_save = () => {
const { id, disabled } = this.props;
const { category, rule_ids } = this.state;
if (disabled) {
return;
}
api().put(`/api/v1/admin/reports/${id}`, {
category,
rule_ids,
}).catch(err => {
console.error(err);
});
};
handleSelect = id => {
this.setState({ category: id }, () => this._save());
};
handleToggle = id => {
const { rule_ids } = this.state;
if (rule_ids.includes(id)) {
this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
} else {
this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
}
};
render () {
const { disabled, intl } = this.props;
const { rules, category, rule_ids } = this.state;
return (
<div className='report-reason-selector'>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
</Category>
</div>
);
}
}

View File

@ -291,7 +291,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
}
}

View File

@ -245,7 +245,7 @@ class ActionBar extends React.PureComponent {
if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
}
}

View File

@ -533,6 +533,10 @@ ul {
}
}
ul.rules-list {
padding-top: 0;
}
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) {
body {
min-height: 1024px !important;

View File

@ -579,39 +579,44 @@ body,
.log-entry {
line-height: 20px;
padding: 15px 0;
padding: 15px;
padding-left: 15px * 2 + 40px;
background: $ui-base-color;
border-bottom: 1px solid lighten($ui-base-color, 4%);
border-bottom: 1px solid darken($ui-base-color, 8%);
position: relative;
&:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: 0;
}
&:hover {
background: lighten($ui-base-color, 4%);
}
&__header {
display: flex;
justify-content: flex-start;
align-items: center;
color: $darker-text-color;
font-size: 14px;
padding: 0 10px;
}
&__avatar {
margin-right: 10px;
position: absolute;
left: 15px;
top: 15px;
.avatar {
display: block;
margin: 0;
border-radius: 50%;
border-radius: 4px;
width: 40px;
height: 40px;
}
}
&__content {
max-width: calc(100% - 90px);
}
&__title {
word-wrap: break-word;
}
@ -627,6 +632,14 @@ body,
text-decoration: none;
font-weight: 500;
}
a {
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
a.name-tag,
@ -655,8 +668,9 @@ a.inline-name-tag,
a.name-tag,
.name-tag {
display: flex;
display: inline-flex;
align-items: center;
vertical-align: top;
.avatar {
display: block;
@ -1114,3 +1128,287 @@ a.sparkline {
}
}
}
.report-reason-selector {
border-radius: 4px;
background: $ui-base-color;
margin-bottom: 20px;
&__category {
cursor: pointer;
border-bottom: 1px solid darken($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
&__label {
padding: 15px;
}
&__rules {
margin-left: 30px;
}
}
&__rule {
cursor: pointer;
padding: 15px;
}
}
.report-header {
display: grid;
grid-gap: 15px;
grid-template-columns: minmax(0, 1fr) 300px;
&__details {
&__item {
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 15px 0;
&:last-child {
border-bottom: 0;
}
&__header {
font-weight: 600;
padding: 4px 0;
}
}
&--horizontal {
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column;
.report-header__details__item {
border-bottom: 0;
}
}
}
}
.account-card {
background: $ui-base-color;
border-radius: 4px;
&__header {
padding: 4px;
border-radius: 4px;
height: 128px;
img {
display: block;
margin: 0;
width: 100%;
height: 100%;
object-fit: cover;
background: darken($ui-base-color, 8%);
}
}
&__title {
margin-top: -25px;
display: flex;
align-items: flex-end;
&__avatar {
padding: 15px;
img {
display: block;
margin: 0;
width: 56px;
height: 56px;
background: darken($ui-base-color, 8%);
border-radius: 8px;
}
}
.display-name {
color: $darker-text-color;
padding-bottom: 15px;
font-size: 15px;
bdi {
display: block;
color: $primary-text-color;
font-weight: 500;
}
}
}
&__bio {
padding: 0 15px;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
max-height: 18px * 2;
position: relative;
&::after {
display: block;
content: "";
width: 50px;
height: 18px;
position: absolute;
bottom: 0;
right: 15px;
background: linear-gradient(to left, $ui-base-color, transparent);
pointer-events: none;
}
}
&__actions {
display: flex;
align-items: center;
padding-top: 10px;
&__button {
flex: 0 0 auto;
padding: 0 15px;
}
}
&__counters {
flex: 1 1 auto;
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column;
&__item {
padding: 15px;
text-align: center;
color: $primary-text-color;
font-weight: 600;
font-size: 15px;
small {
display: block;
color: $darker-text-color;
font-weight: 400;
font-size: 13px;
}
}
}
}
.report-notes {
margin-bottom: 20px;
&__item {
background: $ui-base-color;
position: relative;
padding: 15px;
padding-left: 15px * 2 + 40px;
border-bottom: 1px solid darken($ui-base-color, 8%);
&:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: 0;
}
&:hover {
background-color: lighten($ui-base-color, 4%);
}
&__avatar {
position: absolute;
left: 15px;
top: 15px;
border-radius: 4px;
width: 40px;
height: 40px;
}
&__header {
color: $darker-text-color;
font-size: 15px;
line-height: 20px;
margin-bottom: 4px;
.username a {
color: $primary-text-color;
font-weight: 500;
text-decoration: none;
margin-right: 5px;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
time {
margin-left: 5px;
vertical-align: baseline;
}
}
&__content {
font-size: 15px;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
color: $primary-text-color;
p {
margin-bottom: 20px;
white-space: pre-wrap;
unicode-bidi: plaintext;
&:last-child {
margin-bottom: 0;
}
}
}
&__actions {
position: absolute;
top: 15px;
right: 15px;
text-align: right;
}
}
}
.report-actions {
border: 1px solid darken($ui-base-color, 8%);
&__item {
display: flex;
align-items: center;
line-height: 18px;
border-bottom: 1px solid darken($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
&__button {
flex: 0 0 auto;
width: 100px;
padding: 15px;
padding-right: 0;
.button {
display: block;
width: 100%;
}
}
&__description {
padding: 15px;
font-size: 14px;
color: $dark-text-color;
}
}
}

View File

@ -143,6 +143,21 @@
&:active {
outline: 0 !important;
}
&.disabled {
border-color: $dark-text-color;
&.active {
background: $dark-text-color;
}
&:active,
&:focus,
&:hover {
border-color: $dark-text-color;
border-width: 1px;
}
}
}
&__number {

View File

@ -6,11 +6,11 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
end
def total
Report.resolved.where(updated_at: time_period).count
Report.resolved.where(action_taken_at: time_period).count
end
def previous_total
Report.resolved.where(updated_at: previous_time_period).count
Report.resolved.where(action_taken_at: previous_time_period).count
end
def data
@ -19,8 +19,7 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
WITH resolved_reports AS (
SELECT reports.id
FROM reports
WHERE action_taken
AND date_trunc('day', reports.updated_at)::date = axis.period
WHERE date_trunc('day', reports.action_taken_at)::date = axis.period
)
SELECT count(*) FROM resolved_reports
) AS value

View File

@ -160,11 +160,11 @@ class UserMailer < Devise::Mailer
end
end
def warning(user, warning, status_ids = nil)
def warning(user, warning)
@resource = user
@warning = warning
@instance = Rails.configuration.x.local_domain
@statuses = Status.where(id: status_ids).includes(:account) if status_ids.is_a?(Array)
@statuses = @warning.statuses.includes(:account, :preloadable_poll, :media_attachments, active_mentions: [:account])
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email,

View File

@ -10,14 +10,30 @@
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# report_id :bigint(8)
# status_ids :string is an Array
#
class AccountWarning < ApplicationRecord
enum action: %i(none disable sensitive silence suspend), _suffix: :action
enum action: {
none: 0,
disable: 1_000,
delete_statuses: 1_500,
sensitive: 2_000,
silence: 3_000,
suspend: 4_000,
}, _suffix: :action
belongs_to :account, inverse_of: :account_warnings
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings
belongs_to :target_account, class_name: 'Account', inverse_of: :strikes
belongs_to :report, optional: true
scope :latest, -> { order(created_at: :desc) }
has_one :appeal, dependent: :destroy
scope :latest, -> { order(id: :desc) }
scope :custom, -> { where.not(text: '') }
def statuses
Status.with_discarded.where(id: status_ids || [])
end
end

View File

@ -33,7 +33,7 @@ class Admin::AccountAction
def save!
ApplicationRecord.transaction do
process_action!
process_warning!
process_strike!
end
process_email!
@ -74,20 +74,14 @@ class Admin::AccountAction
end
end
def process_warning!
return unless warnable?
authorize(target_account, :warn?)
@warning = AccountWarning.create!(target_account: target_account,
def process_strike!
@warning = target_account.strikes.create!(
account: current_account,
report: report,
action: type,
text: text_for_warning)
# A log entry is only interesting if the warning contains
# custom text from someone. Otherwise it's just noise.
log_action(:create, warning) if warning.text.present?
text: text_for_warning,
status_ids: status_ids
)
end
def process_reports!
@ -143,7 +137,7 @@ class Admin::AccountAction
end
def process_email!
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
UserMailer.warning(target_account.user, warning).deliver_later! if warnable?
end
def warnable?
@ -151,7 +145,7 @@ class Admin::AccountAction
end
def status_ids
report.status_ids if report && include_statuses
report.status_ids if with_report? && include_statuses
end
def reports

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
class Admin::StatusBatchAction
include ActiveModel::Model
include AccountableConcern
include Authorization
attr_accessor :current_account, :type,
:status_ids, :report_id
def save!
process_action!
end
private
def statuses
Status.with_discarded.where(id: status_ids)
end
def process_action!
return if status_ids.empty?
case type
when 'delete'
handle_delete!
when 'report'
handle_report!
when 'remove_from_report'
handle_remove_from_report!
end
end
def handle_delete!
statuses.each { |status| authorize(status, :destroy?) }
ApplicationRecord.transaction do
statuses.each do |status|
status.discard
log_action(:destroy, status)
end
if with_report?
report.resolve!(current_account)
log_action(:resolve, report)
end
@warning = target_account.strikes.create!(
action: :delete_statuses,
account: current_account,
report: report,
status_ids: status_ids
)
statuses.each { |status| Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) } unless target_account.local?
end
UserMailer.warning(target_account.user, @warning).deliver_later! if target_account.local?
RemovalWorker.push_bulk(status_ids) { |status_id| [status_id, preserve: target_account.local?, immediate: !target_account.local?] }
end
def handle_report!
@report = Report.new(report_params) unless with_report?
@report.status_ids = (@report.status_ids + status_ids.map(&:to_i)).uniq
@report.save!
@report_id = @report.id
end
def handle_remove_from_report!
return unless with_report?
report.status_ids -= status_ids.map(&:to_i)
report.save!
end
def report
@report ||= Report.find(report_id) if report_id.present?
end
def with_report?
!report.nil?
end
def target_account
@target_account ||= statuses.first.account
end
def report_params
{ account: current_account, target_account: target_account }
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
class Admin::StatusFilter
KEYS = %i(
media
id
report_id
).freeze
attr_reader :params
def initialize(account, params)
@account = account
@params = params
end
def results
scope = @account.statuses.where(visibility: [:public, :unlisted])
params.each do |key, value|
next if %w(page report_id).include?(key.to_s)
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
end
scope
end
private
def scope_for(key, value)
case key.to_s
when 'media'
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
when 'id'
Status.where(id: value)
else
raise "Unknown filter: #{key}"
end
end
end

View File

@ -42,7 +42,7 @@ module AccountAssociations
has_many :account_moderation_notes, dependent: :destroy, inverse_of: :account
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
has_many :account_warnings, dependent: :destroy, inverse_of: :account
has_many :targeted_account_warnings, class_name: 'AccountWarning', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
has_many :strikes, class_name: 'AccountWarning', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
# Lists (that the account is on, not owned by the account)
has_many :list_accounts, inverse_of: :account, dependent: :destroy

View File

@ -1,45 +0,0 @@
# frozen_string_literal: true
class Form::StatusBatch
include ActiveModel::Model
include AccountableConcern
attr_accessor :status_ids, :action, :current_account
def save
case action
when 'nsfw_on', 'nsfw_off'
change_sensitive(action == 'nsfw_on')
when 'delete'
delete_statuses
end
end
private
def change_sensitive(sensitive)
media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
ApplicationRecord.transaction do
Status.where(id: media_attached_status_ids).reorder(nil).find_each do |status|
status.update!(sensitive: sensitive)
log_action :update, status
end
end
true
rescue ActiveRecord::RecordInvalid
false
end
def delete_statuses
Status.where(id: status_ids).reorder(nil).find_each do |status|
status.discard
RemovalWorker.perform_async(status.id, immediate: true)
Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
log_action :destroy, status
end
true
end
end

View File

@ -6,7 +6,6 @@
# id :bigint(8) not null, primary key
# status_ids :bigint(8) default([]), not null, is an Array
# comment :text default(""), not null
# action_taken :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint(8) not null
@ -15,9 +14,14 @@
# assigned_account_id :bigint(8)
# uri :string
# forwarded :boolean
# category :integer default("other"), not null
# action_taken_at :datetime
# rule_ids :bigint(8) is an Array
#
class Report < ApplicationRecord
self.ignored_columns = %w(action_taken)
include Paginable
include RateLimitable
@ -30,11 +34,17 @@ class Report < ApplicationRecord
has_many :notes, class_name: 'ReportNote', foreign_key: :report_id, inverse_of: :report, dependent: :destroy
scope :unresolved, -> { where(action_taken: false) }
scope :resolved, -> { where(action_taken: true) }
scope :unresolved, -> { where(action_taken_at: nil) }
scope :resolved, -> { where.not(action_taken_at: nil) }
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
validates :comment, length: { maximum: 1000 }
validates :comment, length: { maximum: 1_000 }
enum category: {
other: 0,
spam: 1_000,
violation: 2_000,
}
def local?
false # Force uri_for to use uri attribute
@ -47,13 +57,17 @@ class Report < ApplicationRecord
end
def statuses
Status.with_discarded.where(id: status_ids).includes(:account, :media_attachments, :mentions)
Status.with_discarded.where(id: status_ids)
end
def media_attachments
MediaAttachment.where(status_id: status_ids)
end
def rules
Rule.with_discarded.where(id: rule_ids)
end
def assign_to_self!(current_account)
update!(assigned_account_id: current_account.id)
end
@ -63,22 +77,19 @@ class Report < ApplicationRecord
end
def resolve!(acting_account)
if account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
# This is an automated report and it is being dismissed, so it's
# a false positive, in which case update the account's trust level
# to prevent further spam checks
target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
end
RemovalWorker.push_bulk(Status.with_discarded.discarded.where(id: status_ids).pluck(:id)) { |status_id| [status_id, { immediate: true }] }
update!(action_taken: true, action_taken_by_account_id: acting_account.id)
update!(action_taken_at: Time.now.utc, action_taken_by_account_id: acting_account.id)
end
def unresolve!
update!(action_taken: false, action_taken_by_account_id: nil)
update!(action_taken_at: nil, action_taken_by_account_id: nil)
end
def action_taken?
action_taken_at.present?
end
alias action_taken action_taken?
def unresolved?
!action_taken?
end
@ -88,29 +99,24 @@ class Report < ApplicationRecord
end
def history
time_range = created_at..updated_at
sql = [
subquery = [
Admin::ActionLog.where(
target_type: 'Report',
target_id: id,
created_at: time_range
).unscope(:order),
target_id: id
).unscope(:order).arel,
Admin::ActionLog.where(
target_type: 'Account',
target_id: target_account_id,
created_at: time_range
).unscope(:order),
target_id: target_account_id
).unscope(:order).arel,
Admin::ActionLog.where(
target_type: 'Status',
target_id: status_ids,
created_at: time_range
).unscope(:order),
].map { |query| "(#{query.to_sql})" }.join(' UNION ALL ')
target_id: status_ids
).unscope(:order).arel,
].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) }
Admin::ActionLog.from("(#{sql}) AS admin_action_logs")
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
end
def set_uri

View File

@ -19,7 +19,7 @@ class ReportFilter
scope = Report.unresolved
params.each do |key, value|
scope = scope.merge scope_for(key, value)
scope = scope.merge scope_for(key, value), rewhere: true
end
scope

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class REST::Admin::ReportSerializer < ActiveModel::Serializer
attributes :id, :action_taken, :comment, :created_at, :updated_at
attributes :id, :action_taken, :category, :comment, :created_at, :updated_at
has_one :account, serializer: REST::Admin::AccountSerializer
has_one :target_account, serializer: REST::Admin::AccountSerializer
@ -9,8 +9,13 @@ class REST::Admin::ReportSerializer < ActiveModel::Serializer
has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer
has_many :statuses, serializer: REST::StatusSerializer
has_many :rules, serializer: REST::RuleSerializer
def id
object.id.to_s
end
def statuses
object.statuses.with_includes
end
end

View File

@ -9,6 +9,7 @@ class RemoveStatusService < BaseService
# @param [Hash] options
# @option [Boolean] :redraft
# @option [Boolean] :immediate
# @option [Boolean] :preserve
# @option [Boolean] :original_removed
def call(status, **options)
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
@ -43,7 +44,7 @@ class RemoveStatusService < BaseService
remove_media
end
@status.destroy! if @options[:immediate] || !@status.reported?
@status.destroy! if permanently?
else
raise Mastodon::RaceConditionError
end
@ -135,11 +136,15 @@ class RemoveStatusService < BaseService
end
def remove_media
return if @options[:redraft] || (!@options[:immediate] && @status.reported?)
return if @options[:redraft] || !permanently?
@status.media_attachments.destroy_all
end
def permanently?
@options[:immediate] || !(@options[:preserve] || @status.reported?)
end
def lock_options
{ redis: Redis.current, key: "distribute:#{@status.id}", autorelease: 5.minutes.seconds }
end

View File

@ -22,7 +22,7 @@
%div.muted-hint.center-text
= t 'admin.action_logs.empty'
- else
.announcements-list
.report-notes
= render partial: 'action_log', collection: @action_logs
= paginate @action_logs

View File

@ -1,7 +1,18 @@
.speech-bubble
.speech-bubble__bubble
.report-notes__item
= image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
.report-notes__item__header
%span.username
= link_to display_name(report_note.account), admin_account_path(report_note.account_id)
%time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
- if report_note.created_at.today?
= t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))
- else
= l report_note.created_at.to_date
.report-notes__item__content
= simple_format(h(report_note.content))
.speech-bubble__owner
= admin_account_link_to report_note.account
%time.formatted{ datetime: report_note.created_at.iso8601 }= l report_note.created_at
= table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
- if can?(:destroy, report_note)
.report-notes__item__actions
= table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete

View File

@ -1,6 +0,0 @@
.speech-bubble.positive
.speech-bubble__bubble
= t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}_html", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target'))
.speech-bubble__owner
= admin_account_link_to(action_log.account)
%time.formatted{ datetime: action_log.created_at.iso8601 }= l action_log.created_at

View File

@ -22,6 +22,9 @@
= react_component :media_gallery, height: 343, sensitive: status.proper.sensitive?, visible: false, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
.detailed-status__meta
- if status.application
= status.application.name
·
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
- if status.discarded?

View File

@ -1,5 +1,6 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
= javascript_pack_tag 'public', async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.reports.report', id: @report.id)
@ -10,122 +11,199 @@
- else
= link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
.table-wrapper
%table.table.inline-table
%tbody
%tr
%th= t('admin.reports.reported_account')
%td= admin_account_link_to @report.target_account
%td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.target_account.targeted_reports.count), admin_reports_path(target_account_id: @report.target_account.id)
%td= table_link_to 'file', t('admin.reports.account.notes', count: @report.target_account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.target_account.id)
%tr
%th= t('admin.reports.reported_by')
- if @report.account.instance_actor?
%td{ colspan: 3 }= site_hostname
- elsif @report.account.local?
%td= admin_account_link_to @report.account
%td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.account.targeted_reports.count), admin_reports_path(target_account_id: @report.account.id)
%td= table_link_to 'file', t('admin.reports.account.notes', count: @report.account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.account.id)
- else
%td{ colspan: 3 }= @report.account.domain
%tr
%th= t('admin.reports.created_at')
%td{ colspan: 3 }
.report-header
.report-header__card
.account-card
.account-card__header
= image_tag @report.target_account.header.url, alt: ''
.account-card__title
.account-card__title__avatar
= image_tag @report.target_account.avatar.url, alt: ''
.display-name
%bdi
%strong.emojify.p-name= display_name(@report.target_account, custom_emojify: true)
%span
= acct(@report.target_account)
= fa_icon('lock') if @report.target_account.locked?
- if @report.target_account.note.present?
.account-card__bio.emojify
= Formatter.instance.simplified_format(@report.target_account, custom_emojify: true)
.account-card__actions
.account-card__counters
.account-card__counters__item
= friendly_number_to_human @report.target_account.statuses_count
%small= t('accounts.posts', count: @report.target_account.statuses_count).downcase
.account-card__counters__item
= friendly_number_to_human @report.target_account.followers_count
%small= t('accounts.followers', count: @report.target_account.followers_count).downcase
.account-card__counters__item
= friendly_number_to_human @report.target_account.following_count
%small= t('accounts.following', count: @report.target_account.following_count).downcase
.account-card__actions__button
= link_to t('admin.reports.view_profile'), admin_account_path(@report.target_account_id), class: 'button'
.report-header__details.report-header__details--horizontal
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.accounts.joined')
.report-header__details__item__content
%time.time-ago{ datetime: @report.target_account.created_at.iso8601, title: l(@report.target_account.created_at) }= l @report.target_account.created_at
.report-header__details__item
.report-header__details__item__header
%strong= t('accounts.last_active')
.report-header__details__item__content
- if @report.target_account.last_status_at.present?
%time.time-ago{ datetime: @report.target_account.last_status_at.to_date.iso8601, title: l(@report.target_account.last_status_at.to_date) }= l @report.target_account.last_status_at
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.accounts.strikes')
.report-header__details__item__content
= @report.target_account.strikes.count
.report-header__details
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.created_at')
.report-header__details__item__content
%time.formatted{ datetime: @report.created_at.iso8601 }
%tr
%th= t('admin.reports.updated_at')
%td{ colspan: 3 }
%time.formatted{ datetime: @report.updated_at.iso8601 }
%tr
%th= t('admin.reports.status')
%td
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.reported_by')
.report-header__details__item__content
- if @report.account.instance_actor?
= site_hostname
- elsif @report.account.local?
= admin_account_link_to @report.account
- else
= @report.account.domain
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.status')
.report-header__details__item__content
- if @report.action_taken?
= t('admin.reports.resolved')
- else
= t('admin.reports.unresolved')
%td{ colspan: 2 }
- if @report.action_taken?
= table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
- unless @report.target_account.local?
%tr
%th= t('admin.reports.forwarded')
%td{ colspan: 3 }
- if @report.forwarded.nil?
\-
- elsif @report.forwarded?
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.forwarded')
.report-header__details__item__content
- if @report.forwarded?
= t('simple_form.yes')
- else
= t('simple_form.no')
- if !@report.action_taken_by_account.nil?
%tr
%th= t('admin.reports.action_taken_by')
%td{ colspan: 3 }
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.action_taken_by')
.report-header__details__item__content
= admin_account_link_to @report.action_taken_by_account
- else
%tr
%th= t('admin.reports.assigned')
%td
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.assigned')
.report-header__details__item__content
- if @report.assigned_account.nil?
\-
= t 'admin.reports.no_one_assigned'
- else
= admin_account_link_to @report.assigned_account
%td
- if @report.assigned_account != current_user.account
= table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(@report), method: :post
%td
- if !@report.assigned_account.nil?
- elsif !@report.assigned_account.nil?
= table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
%hr.spacer
%div.action-buttons
%div
%h3= t 'admin.reports.category'
- if @report.unresolved?
%div
- if @report.target_account.local?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
%p= t 'admin.reports.category_description_html'
%hr.spacer
= react_admin_component :report_reason_selector, id: @report.id, category: @report.category, rule_ids: @report.rule_ids&.map(&:to_s), disabled: @report.action_taken?
.speech-bubble
.speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
.speech-bubble__owner
- if @report.account.local?
= admin_account_link_to @report.account
- if @report.comment.present?
%p= t('admin.reports.comment_description_html', name: content_tag(:strong, @report.account.username, class: 'username'))
.report-notes__item
= image_tag @report.account.avatar.url, class: 'report-notes__item__avatar'
.report-notes__item__header
%span.username
= link_to display_name(@report.account), admin_account_path(@report.account_id)
%time{ datetime: @report.created_at.iso8601, title: l(@report.created_at) }
- if @report.created_at.today?
= t('admin.report_notes.today_at', time: l(@report.created_at, format: :time))
- else
= @report.account.domain
%br/
%time.formatted{ datetime: @report.created_at.iso8601 }
= l @report.created_at.to_date
.report-notes__item__content
= simple_format(h(@report.comment))
- unless @report.statuses.empty?
%hr.spacer/
= form_for(@form, url: admin_report_reported_statuses_path(@report.id)) do |f|
%h3= t 'admin.reports.statuses'
%p
= t 'admin.reports.statuses_description_html'
= link_to safe_join([fa_icon('plus'), t('admin.reports.add_to_report')]), admin_account_statuses_path(@report.target_account_id, report_id: @report.id), class: 'table-action-link'
= form_for(@form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id)) do |f|
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- if !@statuses.empty? && @report.unresolved?
= f.button safe_join([fa_icon('times'), t('admin.statuses.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
= f.button safe_join([fa_icon('trash'), t('admin.reports.delete_and_resolve')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- else
.batch-table__body
= render partial: 'admin/reports/status', collection: @report.statuses, locals: { f: f }
- if @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
- if @report.unresolved?
%hr.spacer/
%p= t 'admin.reports.actions_description_html'
.report-actions
.report-actions__item
.report-actions__item__button
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
.report-actions__item__description
= t('admin.reports.actions.silence_description_html')
.report-actions__item
.report-actions__item__button
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, report_id: @report.id, type: 'suspend'), class: 'button button--destructive'
.report-actions__item__description
= t('admin.reports.actions.suspend_description_html')
.report-actions__item
.report-actions__item__button
= link_to t('admin.accounts.custom'), new_admin_account_action_path(@report.target_account_id, report_id: @report.id), class: 'button'
.report-actions__item__description
= t('admin.reports.actions.other_description_html')
- unless @action_logs.empty?
%hr.spacer/
%h3= t 'admin.reports.action_log'
.report-notes
= render @action_logs
%hr.spacer/
- @report_notes.each do |item|
- if item.is_a?(Admin::ActionLog)
= render partial: 'action_log', locals: { action_log: item }
- else
= render item
%h3= t 'admin.reports.notes.title'
%p= t 'admin.reports.notes_description_html'
.report-notes
= render @report_notes
= simple_form_for @report_note, url: admin_report_notes_path do |f|
= render 'shared/error_messages', object: @report_note
= f.input :report_id, as: :hidden
.field-group

View File

@ -10,28 +10,37 @@
.filter-subset
%strong= t('admin.statuses.media.title')
%ul
%li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected'
%li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
%li= filter_link_to t('generic.all'), media: nil, id: nil
%li= filter_link_to t('admin.statuses.with_media'), media: '1'
.back-link
- if params[:report_id]
= link_to admin_report_path(params[:report_id].to_i) do
= fa_icon 'chevron-left fw'
= t('admin.statuses.back_to_report')
- else
= link_to admin_account_path(@account.id) do
= fa_icon 'chevron-left fw'
= t('admin.statuses.back_to_account')
%hr.spacer/
= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
= hidden_field_tag :page, params[:page]
= hidden_field_tag :media, params[:media]
= form_for(@status_batch_action, url: batch_admin_account_statuses_path(@account.id)) do |f|
= hidden_field_tag :page, params[:page] || 1
- Admin::StatusFilter::KEYS.each do |key|
= hidden_field_tag key, params[key] if params[key].present?
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- unless @statuses.empty?
= f.button safe_join([fa_icon('flag'), t('admin.statuses.batch.report')]), name: :report, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
.batch-table__body
- if @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
= paginate @statuses

View File

@ -1,27 +0,0 @@
- content_for :page_title do
= t('admin.statuses.title')
\-
= "@#{@account.acct}"
.filters
.back-link
= link_to admin_account_path(@account.id) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.statuses.back_to_account')
%hr.spacer/
= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
= hidden_field_tag :page, params[:page]
= hidden_field_tag :media, params[:media]
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
.batch-table__body
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }

View File

@ -1,8 +1,8 @@
<% if status.spoiler_text? %>
<%= raw status.spoiler_text %>
----
> <%= raw word_wrap(status.spoiler_text, break_sequence: "\n> ") %>
> ----
>
<% end %>
<%= raw Formatter.instance.plaintext(status) %>
> <%= raw word_wrap(Formatter.instance.plaintext(status), break_sequence: "\n> ") %>
<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %>

View File

@ -37,16 +37,26 @@
%tr
%td.column-cell.text-center
- unless @warning.none_action?
%p= t "user_mailer.warning.explanation.#{@warning.action}"
%p= t "user_mailer.warning.explanation.#{@warning.action}", instance: @instance
- unless @warning.text.blank?
= Formatter.instance.linkify(@warning.text)
- if !@statuses.nil? && !@statuses.empty?
- if @warning.report && !@warning.report.other?
%p
%strong= t('user_mailer.warning.reason')
= t("user_mailer.warning.categories.#{@warning.report.category}")
- if @warning.report.violation? && @warning.report.rule_ids.present?
%ul.rules-list
- @warning.report.rules.each do |rule|
%li= rule.text
- unless @statuses.empty?
%p
%strong= t('user_mailer.warning.statuses')
- if !@statuses.nil? && !@statuses.empty?
- unless @statuses.empty?
- @statuses.each_with_index do |status, i|
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true

View File

@ -3,11 +3,24 @@
===
<% unless @warning.none_action? %>
<%= t "user_mailer.warning.explanation.#{@warning.action}" %>
<%= t "user_mailer.warning.explanation.#{@warning.action}", instance: @instance %>
<% end %>
<% if @warning.text.present? %>
<%= @warning.text %>
<% if !@statuses.nil? && !@statuses.empty? %>
<% end %>
<% if @warning.report && !@warning.report.other? %>
**<%= t('user_mailer.warning.reason') %>** <%= t("user_mailer.warning.categories.#{@warning.report.category}") %>
<% if @warning.report.violation? && @warning.report.rule_ids.present? %>
<% @warning.report.rules.each do |rule| %>
- <%= rule.text %>
<% end %>
<% end %>
<% end %>
<% if !@statuses.empty? %>
<%= t('user_mailer.warning.statuses') %>
<% @statuses.each do |status| %>

View File

@ -8,6 +8,7 @@ class Scheduler::UserCleanupScheduler
def perform
clean_unconfirmed_accounts!
clean_suspended_accounts!
clean_discarded_statuses!
end
private
@ -24,4 +25,12 @@ class Scheduler::UserCleanupScheduler
Admin::AccountDeletionWorker.perform_async(deletion_request.account_id)
end
end
def clean_discarded_statuses!
Status.discarded.where('deleted_at <= ?', 30.days.ago).find_in_batches do |statuses|
RemovalWorker.push_bulk(statuses) do |status|
[status.id, { immediate: true }]
end
end
end
end

View File

@ -113,6 +113,7 @@ en:
confirm: Confirm
confirmed: Confirmed
confirming: Confirming
custom: Custom
delete: Delete data
deleted: Deleted
demote: Demote
@ -203,6 +204,7 @@ en:
silence: Limit
silenced: Limited
statuses: Posts
strikes: Previous strikes
subscribe: Subscribe
suspended: Suspended
suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had.
@ -549,32 +551,44 @@ en:
report_notes:
created_msg: Report note successfully created!
destroyed_msg: Report note successfully deleted!
today_at: Today at %{time}
reports:
account:
notes:
one: "%{count} note"
other: "%{count} notes"
reports:
one: "%{count} report"
other: "%{count} reports"
action_log: Audit log
action_taken_by: Action taken by
actions:
other_description_html: See more options for controlling the account's behaviour and customize communication to the reported account.
silence_description_html: The profile will be visible only to those who already follow it or manually look it up, severely limiting its reach. Can always be reverted.
suspend_description_html: The profile and all its contents will become inaccessible until it is eventually deleted. Interacting with the account will be impossible. Reversible within 30 days.
actions_description_html: 'If removing the offending content above is insufficient:'
add_to_report: Add more to report
are_you_sure: Are you sure?
assign_to_self: Assign to me
assigned: Assigned moderator
by_target_domain: Domain of reported account
category: Category
category_description_html: The reason this account and/or content was reported will be cited in communication with the reported account
comment:
none: None
comment_description_html: 'To provide more information, %{name} wrote:'
created_at: Reported
delete_and_resolve: Delete and resolve
forwarded: Forwarded
forwarded_to: Forwarded to %{domain}
mark_as_resolved: Mark as resolved
mark_as_unresolved: Mark as unresolved
no_one_assigned: No one
notes:
create: Add note
create_and_resolve: Resolve with note
create_and_unresolve: Reopen with note
delete: Delete
placeholder: Describe what actions have been taken, or any other related updates...
title: Notes
notes_description_html: View and leave notes to other moderators and your future self
reopen: Reopen report
report: 'Report #%{id}'
reported_account: Reported account
@ -582,11 +596,14 @@ en:
resolved: Resolved
resolved_msg: Report successfully resolved!
status: Status
statuses: Reported content
statuses_description_html: Offending content will be cited in communication with the reported account
target_origin: Origin of reported account
title: Reports
unassign: Unassign
unresolved: Unresolved
updated_at: Updated
view_profile: View profile
rules:
add_new: Add rule
delete: Delete
@ -688,15 +705,13 @@ en:
destroyed_msg: Site upload successfully deleted!
statuses:
back_to_account: Back to account page
back_to_report: Back to report page
batch:
delete: Delete
nsfw_off: Mark as not sensitive
nsfw_on: Mark as sensitive
remove_from_report: Remove from report
report: Report
deleted: Deleted
failed_to_execute: Failed to execute
media:
title: Media
no_media: No media
no_status_selected: No posts were changed as none were selected
title: Account posts
with_media: With media
@ -1457,6 +1472,7 @@ en:
formats:
default: "%b %d, %Y, %H:%M"
month: "%b %Y"
time: "%H:%M"
two_factor_authentication:
add: Add
disable: Disable 2FA
@ -1484,24 +1500,31 @@ en:
subject: Please confirm attempted sign in
title: Sign in attempt
warning:
categories:
spam: Spam
violation: Content violates the following community guidelines
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 posts 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}.
delete_statuses: Some of your posts have been found to violate one or more community guidelines and have been subsequently removed by the moderators of %{instance}. Future violations may result in harsher punitive actions against your account.
disable: You can no longer use your account, but your profile and other data remains intact. You can request a backup of your data, change account settings or delete your account.
sensitive: From now on, all your uploaded media files will be marked as sensitive and hidden behind a click-through warning.
silence: You can still use your account but only people who are already following you will see your posts on this server, and you may be excluded from various discovery features. 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 in about 30 days, but we will retain some basic data to prevent you from evading the suspension.
get_in_touch: If you believe this is an error, you can reply to this e-mail to get in touch with the staff of %{instance}.
reason: 'Reason:'
review_server_policies: Review server policies
statuses: 'Specifically, for:'
statuses: 'Posts that have been found in violation:'
subject:
delete_statuses: Your posts on %{acct} have been removed
disable: Your account %{acct} has been frozen
none: Warning for %{acct}
sensitive: Your account %{acct} posting media has been marked as sensitive
sensitive: Your media files on %{acct} will be marked as sensitive from now on
silence: Your account %{acct} has been limited
suspend: Your account %{acct} has been suspended
title:
delete_statuses: Posts removed
disable: Account frozen
none: Warning
sensitive: Your media has been marked as sensitive
sensitive: Media hidden
silence: Account limited
suspend: Account suspended
welcome:

View File

@ -231,8 +231,6 @@ Rails.application.routes.draw do
post :reopen
post :resolve
end
resources :reported_statuses, only: [:create]
end
resources :report_notes, only: [:create, :destroy]
@ -259,7 +257,13 @@ Rails.application.routes.draw do
resource :change_email, only: [:show, :update]
resource :reset, only: [:create]
resource :action, only: [:new, :create], controller: 'account_actions'
resources :statuses, only: [:index, :show, :create, :update, :destroy]
resources :statuses, only: [:index] do
collection do
post :batch
end
end
resources :relationships, only: [:index]
resource :confirmation, only: [:create] do
@ -514,7 +518,7 @@ Rails.application.routes.draw do
resource :action, only: [:create], controller: 'account_actions'
end
resources :reports, only: [:index, :show] do
resources :reports, only: [:index, :update, :show] do
member do
post :assign_to_self
post :unassign

View File

@ -0,0 +1,21 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddCategoryToReports < ActiveRecord::Migration[6.1]
include Mastodon::MigrationHelpers
disable_ddl_transaction!
def up
safety_assured { add_column_with_default :reports, :category, :int, default: 0, allow_null: false }
add_column :reports, :action_taken_at, :datetime
add_column :reports, :rule_ids, :bigint, array: true
safety_assured { execute 'UPDATE reports SET action_taken_at = updated_at WHERE action_taken = TRUE' }
end
def down
safety_assured { execute 'UPDATE reports SET action_taken = TRUE WHERE action_taken_at IS NOT NULL' }
remove_column :reports, :category
remove_column :reports, :action_taken_at
remove_column :reports, :rule_ids
end
end

View File

@ -0,0 +1,6 @@
class AddReportIdToAccountWarnings < ActiveRecord::Migration[6.1]
def change
safety_assured { add_reference :account_warnings, :report, foreign_key: { on_delete: :cascade }, index: false }
add_column :account_warnings, :status_ids, :string, array: true
end
end

View File

@ -0,0 +1,21 @@
class FixAccountWarningActions < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def up
safety_assured do
execute 'UPDATE account_warnings SET action = 1000 WHERE action = 1'
execute 'UPDATE account_warnings SET action = 2000 WHERE action = 2'
execute 'UPDATE account_warnings SET action = 3000 WHERE action = 3'
execute 'UPDATE account_warnings SET action = 4000 WHERE action = 4'
end
end
def down
safety_assured do
execute 'UPDATE account_warnings SET action = 1 WHERE action = 1000'
execute 'UPDATE account_warnings SET action = 2 WHERE action = 2000'
execute 'UPDATE account_warnings SET action = 3 WHERE action = 3000'
execute 'UPDATE account_warnings SET action = 4 WHERE action = 4000'
end
end
end

View File

@ -0,0 +1,7 @@
class AddDeletedAtIndexOnStatuses < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :statuses, :deleted_at, where: 'deleted_at IS NOT NULL', algorithm: :concurrently
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class RemoveActionTakenFromReports < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
safety_assured { remove_column :reports, :action_taken, :boolean, default: false, null: false }
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_12_13_040746) do
ActiveRecord::Schema.define(version: 2022_01_16_202951) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -133,6 +133,8 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do
t.text "text", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "report_id"
t.string "status_ids", array: true
t.index ["account_id"], name: "index_account_warnings_on_account_id"
t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id"
end
@ -747,7 +749,6 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do
create_table "reports", force: :cascade do |t|
t.bigint "status_ids", default: [], null: false, array: true
t.text "comment", default: "", null: false
t.boolean "action_taken", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "account_id", null: false
@ -756,6 +757,9 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do
t.bigint "assigned_account_id"
t.string "uri"
t.boolean "forwarded"
t.integer "category", default: 0, null: false
t.datetime "action_taken_at"
t.bigint "rule_ids", array: true
t.index ["account_id"], name: "index_reports_on_account_id"
t.index ["target_account_id"], name: "index_reports_on_target_account_id"
end
@ -851,6 +855,7 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do
t.bigint "poll_id"
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 ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
@ -1008,6 +1013,7 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do
add_foreign_key "account_statuses_cleanup_policies", "accounts", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", on_delete: :nullify
add_foreign_key "account_warnings", "reports", on_delete: :cascade
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
add_foreign_key "announcement_mutes", "accounts", on_delete: :cascade

View File

@ -12,11 +12,11 @@ describe Admin::ReportNotesController do
describe 'POST #create' do
subject { post :create, params: params }
let(:report) { Fabricate(:report, action_taken: action_taken, action_taken_by_account_id: account_id) }
let(:report) { Fabricate(:report, action_taken_at: action_taken, action_taken_by_account_id: account_id) }
context 'when parameter is valid' do
context 'when report is unsolved' do
let(:action_taken) { false }
let(:action_taken) { nil }
let(:account_id) { nil }
context 'when create_and_resolve flag is on' do
@ -41,7 +41,7 @@ describe Admin::ReportNotesController do
end
context 'when report is resolved' do
let(:action_taken) { true }
let(:action_taken) { Time.now.utc }
let(:account_id) { user.account.id }
context 'when create_and_unresolve flag is on' do
@ -68,7 +68,7 @@ describe Admin::ReportNotesController do
context 'when parameter is invalid' do
let(:params) { { report_note: { content: '', report_id: report.id } } }
let(:action_taken) { false }
let(:action_taken) { nil }
let(:account_id) { nil }
it 'renders admin/reports/show' do

View File

@ -1,59 +0,0 @@
require 'rails_helper'
describe Admin::ReportedStatusesController do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:report) { Fabricate(:report, status_ids: [status.id]) }
let(:status) { Fabricate(:status) }
before do
sign_in user, scope: :user
end
describe 'POST #create' do
subject do
-> { post :create, params: { :report_id => report, action => '', :form_status_batch => { status_ids: status_ids } } }
end
let(:action) { 'nsfw_on' }
let(:status_ids) { [status.id] }
let(:status) { Fabricate(:status, sensitive: !sensitive) }
let(:sensitive) { true }
let!(:media_attachment) { Fabricate(:media_attachment, status: status) }
context 'when action is nsfw_on' do
it 'updates sensitive column' do
is_expected.to change {
status.reload.sensitive
}.from(false).to(true)
end
end
context 'when action is nsfw_off' do
let(:action) { 'nsfw_off' }
let(:sensitive) { false }
it 'updates sensitive column' do
is_expected.to change {
status.reload.sensitive
}.from(true).to(false)
end
end
context 'when action is delete' do
let(:action) { 'delete' }
it 'removes a status' do
allow(RemovalWorker).to receive(:perform_async)
subject.call
expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, immediate: true)
end
end
it 'redirects to report page' do
subject.call
expect(response).to redirect_to(admin_report_path(report))
end
end
end

View File

@ -10,8 +10,8 @@ describe Admin::ReportsController do
describe 'GET #index' do
it 'returns http success with no filters' do
specified = Fabricate(:report, action_taken: false)
Fabricate(:report, action_taken: true)
specified = Fabricate(:report, action_taken_at: nil)
Fabricate(:report, action_taken_at: Time.now.utc)
get :index
@ -22,10 +22,10 @@ describe Admin::ReportsController do
end
it 'returns http success with resolved filter' do
specified = Fabricate(:report, action_taken: true)
Fabricate(:report, action_taken: false)
specified = Fabricate(:report, action_taken_at: Time.now.utc)
Fabricate(:report, action_taken_at: nil)
get :index, params: { resolved: 1 }
get :index, params: { resolved: '1' }
reports = assigns(:reports).to_a
expect(reports.size).to eq 1
@ -54,15 +54,7 @@ describe Admin::ReportsController do
expect(response).to redirect_to(admin_reports_path)
report.reload
expect(report.action_taken_by_account).to eq user.account
expect(report.action_taken).to eq true
end
it 'sets trust level when the report is an antispam one' do
report = Fabricate(:report, account: Account.representative)
put :resolve, params: { id: report }
report.reload
expect(report.target_account.trust_level).to eq Account::TRUST_LEVELS[:trusted]
expect(report.action_taken?).to eq true
end
end
@ -74,7 +66,7 @@ describe Admin::ReportsController do
expect(response).to redirect_to(admin_report_path(report))
report.reload
expect(report.action_taken_by_account).to eq nil
expect(report.action_taken).to eq false
expect(report.action_taken?).to eq false
end
end

View File

@ -18,65 +18,46 @@ describe Admin::StatusesController do
end
describe 'GET #index' do
it 'returns http success with no media' do
context do
before do
get :index, params: { account_id: account.id }
statuses = assigns(:statuses).to_a
expect(statuses.size).to eq 4
expect(statuses.first.id).to eq last_status.id
expect(response).to have_http_status(200)
end
it 'returns http success with media' do
get :index, params: { account_id: account.id, media: true }
statuses = assigns(:statuses).to_a
expect(statuses.size).to eq 2
expect(statuses.first.id).to eq last_media_attached_status.id
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
describe 'POST #create' do
subject do
-> { post :create, params: { :account_id => account.id, action => '', :form_status_batch => { status_ids: status_ids } } }
context 'filtering by media' do
before do
get :index, params: { account_id: account.id, media: '1' }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
end
describe 'POST #batch' do
before do
post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } }
end
let(:action) { 'nsfw_on' }
let(:status_ids) { [media_attached_status.id] }
context 'when action is nsfw_on' do
it 'updates sensitive column' do
is_expected.to change {
media_attached_status.reload.sensitive
}.from(false).to(true)
end
context 'when action is report' do
let(:action) { 'report' }
it 'creates a report' do
report = Report.last
expect(report.target_account_id).to eq account.id
expect(report.status_ids).to eq status_ids
end
context 'when action is nsfw_off' do
let(:action) { 'nsfw_off' }
let(:sensitive) { false }
it 'updates sensitive column' do
is_expected.to change {
media_attached_status.reload.sensitive
}.from(true).to(false)
end
end
context 'when action is delete' do
let(:action) { 'delete' }
it 'removes a status' do
allow(RemovalWorker).to receive(:perform_async)
subject.call
expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, immediate: true)
end
end
it 'redirects to account statuses page' do
subject.call
expect(response).to redirect_to(admin_account_statuses_path(account.id))
it 'redirects to report page' do
expect(response).to redirect_to(admin_report_path(Report.last.id))
end
end
end
end

View File

@ -2,5 +2,5 @@ Fabricator(:report) do
account
target_account { Fabricate(:account) }
comment "You nasty"
action_taken false
action_taken_at nil
end

View File

@ -79,7 +79,7 @@ class UserMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/warning
def warning
UserMailer.warning(User.first, AccountWarning.new(text: '', action: :silence), [Status.first.id])
UserMailer.warning(User.first, AccountWarning.last)
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token

View File

@ -1,52 +0,0 @@
require 'rails_helper'
describe Form::StatusBatch do
let(:form) { Form::StatusBatch.new(action: action, status_ids: status_ids) }
let(:status) { Fabricate(:status) }
describe 'with nsfw action' do
let(:status_ids) { [status.id, nonsensitive_status.id, sensitive_status.id] }
let(:nonsensitive_status) { Fabricate(:status, sensitive: false) }
let(:sensitive_status) { Fabricate(:status, sensitive: true) }
let!(:shown_media_attachment) { Fabricate(:media_attachment, status: nonsensitive_status) }
let!(:hidden_media_attachment) { Fabricate(:media_attachment, status: sensitive_status) }
context 'nsfw_on' do
let(:action) { 'nsfw_on' }
it { expect(form.save).to be true }
it { expect { form.save }.to change { nonsensitive_status.reload.sensitive }.from(false).to(true) }
it { expect { form.save }.not_to change { sensitive_status.reload.sensitive } }
it { expect { form.save }.not_to change { status.reload.sensitive } }
end
context 'nsfw_off' do
let(:action) { 'nsfw_off' }
it { expect(form.save).to be true }
it { expect { form.save }.to change { sensitive_status.reload.sensitive }.from(true).to(false) }
it { expect { form.save }.not_to change { nonsensitive_status.reload.sensitive } }
it { expect { form.save }.not_to change { status.reload.sensitive } }
end
end
describe 'with delete action' do
let(:status_ids) { [status.id] }
let(:action) { 'delete' }
let!(:another_status) { Fabricate(:status) }
before do
allow(RemovalWorker).to receive(:perform_async)
end
it 'call RemovalWorker' do
form.save
expect(RemovalWorker).to have_received(:perform_async).with(status.id, immediate: true)
end
it 'do not call RemovalWorker' do
form.save
expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, immediate: true)
end
end
end

View File

@ -54,7 +54,7 @@ describe Report do
end
describe 'resolve!' do
subject(:report) { Fabricate(:report, action_taken: false, action_taken_by_account_id: nil) }
subject(:report) { Fabricate(:report, action_taken_at: nil, action_taken_by_account_id: nil) }
let(:acting_account) { Fabricate(:account) }
@ -63,12 +63,13 @@ describe Report do
end
it 'records action taken' do
expect(report).to have_attributes(action_taken: true, action_taken_by_account_id: acting_account.id)
expect(report.action_taken?).to be true
expect(report.action_taken_by_account_id).to eq acting_account.id
end
end
describe 'unresolve!' do
subject(:report) { Fabricate(:report, action_taken: true, action_taken_by_account_id: acting_account.id) }
subject(:report) { Fabricate(:report, action_taken_at: Time.now.utc, action_taken_by_account_id: acting_account.id) }
let(:acting_account) { Fabricate(:account) }
@ -77,23 +78,24 @@ describe Report do
end
it 'unresolves' do
expect(report).to have_attributes(action_taken: false, action_taken_by_account_id: nil)
expect(report.action_taken?).to be false
expect(report.action_taken_by_account_id).to be_nil
end
end
describe 'unresolved?' do
subject { report.unresolved? }
let(:report) { Fabricate(:report, action_taken: action_taken) }
let(:report) { Fabricate(:report, action_taken_at: action_taken) }
context 'if action is taken' do
let(:action_taken) { true }
let(:action_taken) { Time.now.utc }
it { is_expected.to be false }
end
context 'if action not is taken' do
let(:action_taken) { false }
let(:action_taken) { nil }
it { is_expected.to be true }
end