mirror of https://framagit.org/tykayn/mastodon
Add notifications for statuses deleted by moderators (#17204)
parent
d5c9feb7b7
commit
14f436c457
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
end
|
||||
|
||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
||||
@form = Form::StatusBatch.new
|
||||
@status_batch_action = Admin::StatusBatchAction.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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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')}` });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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')}` });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
account: current_account,
|
||||
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?
|
||||
def process_strike!
|
||||
@warning = target_account.strikes.create!(
|
||||
account: current_account,
|
||||
report: report,
|
||||
action: type,
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 }] }
|