Merge pull request #1662 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
This commit is contained in:
Claire
2022-01-19 14:22:59 +01:00
committed by GitHub
68 changed files with 2217 additions and 1088 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')
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

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 'flavours/glitch/util/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

@ -595,39 +595,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;
}
@ -643,6 +648,14 @@ body,
text-decoration: none;
font-weight: 500;
}
a {
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
a.name-tag,
@ -671,8 +684,9 @@ a.inline-name-tag,
a.name-tag,
.name-tag {
display: flex;
display: inline-flex;
align-items: center;
vertical-align: top;
.avatar {
display: block;
@ -1130,3 +1144,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

@ -150,6 +150,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

@ -3,7 +3,7 @@ export const profileLink = '/settings/profile';
export const signOutLink = '/auth/sign_out';
export const termsLink = '/terms';
export const accountAdminLink = (id) => `/admin/accounts/${id}`;
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`;
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses?id=${status_id}`;
export const filterEditLink = (id) => `/filters/${id}/edit`;
export const relationshipsLink = '/relationships';
export const securityLink = '/auth/edit';

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

@ -595,39 +595,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;
}
@ -643,6 +648,14 @@ body,
text-decoration: none;
font-weight: 500;
}
a {
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
a.name-tag,
@ -671,8 +684,9 @@ a.inline-name-tag,
a.name-tag,
.name-tag {
display: flex;
display: inline-flex;
align-items: center;
vertical-align: top;
.avatar {
display: block;
@ -1130,3 +1144,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,
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

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

@ -48,6 +48,6 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
end
def local_follower
@local_follower ||= account.followers.local.without_suspended.first
@local_follower ||= @account.followers.local.without_suspended.first
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)
@ -44,7 +45,7 @@ class RemoveStatusService < BaseService
remove_media
end
@status.destroy! if @options[:immediate] || !@status.reported?
@status.destroy! if permanently?
else
raise Mastodon::RaceConditionError
end
@ -143,11 +144,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

@ -19,7 +19,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

@ -7,122 +7,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')
.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 }
.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?
%td{ colspan: 3 }= site_hostname
= 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)
= admin_account_link_to @report.account
- else
%td{ colspan: 3 }= @report.account.domain
%tr
%th= t('admin.reports.created_at')
%td{ colspan: 3 }
%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
- if @report.action_taken?
= t('admin.reports.resolved')
= @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')
- unless @report.target_account.local?
.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('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?
= 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 }
= admin_account_link_to @report.action_taken_by_account
- else
%tr
%th= t('admin.reports.assigned')
%td
- if @report.assigned_account.nil?
\-
- 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?
= table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
%hr.spacer
%div.action-buttons
%div
- 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'
%hr.spacer
.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
= t('simple_form.no')
- if !@report.action_taken_by_account.nil?
.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
= @report.account.domain
%br/
%time.formatted{ datetime: @report.created_at.iso8601 }
.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
- 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
- elsif !@report.assigned_account.nil?
= table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
- unless @report.statuses.empty?
%hr.spacer/
%hr.spacer
= form_for(@form, url: admin_report_reported_statuses_path(@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') }
.batch-table__body
= render partial: 'admin/reports/status', collection: @report.statuses, locals: { f: f }
%h3= t 'admin.reports.category'
%p= t 'admin.reports.category_description_html'
= react_admin_component :report_reason_selector, id: @report.id, category: @report.category, rule_ids: @report.rule_ids&.map(&:to_s), disabled: @report.action_taken?
- 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
= l @report.created_at.to_date
.report-notes__item__content
= simple_format(h(@report.comment))
%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.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
- 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
- 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/
%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

@ -7,28 +7,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
= link_to admin_account_path(@account.id) do
= fa_icon 'chevron-left fw'
= t('admin.statuses.back_to_account')
- 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
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
- 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