mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(admin): add warning given report outcome (#3500)
This commit is contained in:
1 parent
61a4e3895a
commit
9000a9f4c7
9 files changed
+34
-14
No files matched your search
Generated
+3
-2
@@ -11027,14 +11027,15 @@
|
||||
},
|
||||
"outcome": {
|
||||
"description": "What happened to the reported content or account. Required for actioned, where no_violation is refused. Optional for no_violation, where it can only be no_violation. Refused for duplicate. The reporter is told it",
|
||||
"x-enumNames": ["removed", "restricted", "action_taken", "no_violation"],
|
||||
"x-enumNames": ["removed", "warned", "restricted", "action_taken", "no_violation"],
|
||||
"x-enumDescriptions": [
|
||||
"The reported content, profile field or community was removed",
|
||||
"The account responsible for the reported item was warned",
|
||||
"The reported account was restricted and the content was left up",
|
||||
"Action was taken under the rules, and the reporter is not told which",
|
||||
"The reported content or account does not break the rules and was left as it is"
|
||||
],
|
||||
"enum": ["removed", "restricted", "action_taken", "no_violation"],
|
||||
"enum": ["removed", "warned", "restricted", "action_taken", "no_violation"],
|
||||
"type": "string"
|
||||
},
|
||||
"duplicate_of": {
|
||||
|
||||
@@ -601,13 +601,14 @@ fn resolve_report_form(base: &str, report_id: &str, csrf_token: &str) -> Markup
|
||||
class={(FORM_CONTROL_CLASS) " h-8 appearance-none px-3 py-1.5 pr-10"} {
|
||||
option value="" selected disabled { "Choose one" }
|
||||
option value="removed" { "Content removed" }
|
||||
option value="warned" { "Warning given" }
|
||||
option value="restricted" { "Account restricted" }
|
||||
option value="action_taken" { "Other action taken" }
|
||||
}
|
||||
(select_chevron())
|
||||
}
|
||||
p class="text-neutral-500 text-xs" {
|
||||
"What happened to the reported content or account. The reporter is told it. Other action taken, such as a warning, tells them we acted but not how."
|
||||
"What happened to the reported content or account. The reporter is told it. Warning given and other action taken tell them we acted but not how."
|
||||
}
|
||||
}
|
||||
fieldset class="flex flex-col gap-3" data-show-for-resolution="duplicate" hidden disabled {
|
||||
|
||||
@@ -186,6 +186,8 @@ async fn report_resolve_sends_notify_reporter_from_the_checkbox() {
|
||||
assert_eq!(duplicate.body["notify_reporter"], json!(true));
|
||||
assert!(duplicate.body.get("outcome").is_none());
|
||||
assert_eq!(duplicate.body["duplicate_of"], json!("1400000000000000001"));
|
||||
let warned = submit(&uri, "resolution=actioned&outcome=warned").await;
|
||||
assert_eq!(warned.body["outcome"], json!("warned"));
|
||||
let other_action = submit(&uri, "resolution=actioned&outcome=action_taken").await;
|
||||
assert_eq!(other_action.body["outcome"], json!("action_taken"));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ interface ReporterNoticeResult {
|
||||
skipped: ReporterNoticeSkipReason | null;
|
||||
}
|
||||
|
||||
type ReporterToldOutcome = Exclude<ReportItemOutcome, 'warned'>;
|
||||
|
||||
function skippedNotice(reason: ReporterNoticeSkipReason): ReporterNoticeResult {
|
||||
return {dmSent: false, emailSent: false, dsaEmailSent: false, skipped: reason};
|
||||
}
|
||||
@@ -59,17 +61,18 @@ export class ReporterResolutionNotifier {
|
||||
outcome: ReportItemOutcome,
|
||||
): Promise<ReporterNoticeResult> {
|
||||
const comment = publicComment ?? '';
|
||||
const told = outcome === 'warned' ? 'action_taken' : outcome;
|
||||
if (report.reporterId) {
|
||||
return this.notifyAccountReporter(report, report.reporterId, comment, outcome);
|
||||
return this.notifyAccountReporter(report, report.reporterId, comment, told);
|
||||
}
|
||||
return this.notifyDsaReporter(report, comment, outcome);
|
||||
return this.notifyDsaReporter(report, comment, told);
|
||||
}
|
||||
|
||||
private async notifyAccountReporter(
|
||||
report: IARSubmission,
|
||||
reporterId: UserID,
|
||||
comment: string,
|
||||
outcome: ReportItemOutcome,
|
||||
outcome: ReporterToldOutcome,
|
||||
): Promise<ReporterNoticeResult> {
|
||||
const {users: userRepository, email: emailService} = this.deps.apiContext.services;
|
||||
const reportId = report.reportId.toString();
|
||||
@@ -115,7 +118,7 @@ export class ReporterResolutionNotifier {
|
||||
private async notifyDsaReporter(
|
||||
report: IARSubmission,
|
||||
comment: string,
|
||||
outcome: ReportItemOutcome,
|
||||
outcome: ReporterToldOutcome,
|
||||
): Promise<ReporterNoticeResult> {
|
||||
const email = report.reporterEmail;
|
||||
if (!email) {
|
||||
@@ -134,7 +137,7 @@ export class ReporterResolutionNotifier {
|
||||
reporter: User,
|
||||
reportId: string,
|
||||
comment: string,
|
||||
outcome: ReportItemOutcome,
|
||||
outcome: ReporterToldOutcome,
|
||||
): Promise<boolean> {
|
||||
const systemDm = this.deps.systemDm;
|
||||
if (!systemDm) {
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('Admin staff notifications', () => {
|
||||
expect(dm?.content).toContain('We found that what you reported broke our rules, and we took action.');
|
||||
expect(dm?.content).not.toMatch(/warn|restricted|suspend/i);
|
||||
const metadata = await auditMetadata('resolve_report', reportId);
|
||||
expect(metadata?.get('outcome')).toBe('action_taken');
|
||||
expect(metadata?.get('outcome')).toBe('warned');
|
||||
expect(metadata?.get('reporter_dm_sent')).toBe('true');
|
||||
});
|
||||
|
||||
@@ -347,6 +347,18 @@ describe('Admin staff notifications', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('a DSA notice resolved with a warning emails other action taken', async () => {
|
||||
const {email, reportId} = await fileDsaGuildNotice();
|
||||
await clearTestEmails(harness);
|
||||
await resolve(reportId, {outcome: 'warned'});
|
||||
const sent = await listTestEmails(harness, {recipient: email});
|
||||
expect(sent.map((entry) => [entry.type, entry.metadata.outcome])).toEqual([
|
||||
['dsa_report_resolved', 'action_taken'],
|
||||
]);
|
||||
const metadata = await auditMetadata('resolve_report', reportId);
|
||||
expect(metadata?.get('outcome')).toBe('warned');
|
||||
});
|
||||
|
||||
test('a DSA notice resolved with notify_reporter false gets the decision email without the comment', async () => {
|
||||
const {email, reportId} = await fileDsaGuildNotice();
|
||||
await clearTestEmails(harness);
|
||||
|
||||
@@ -239,7 +239,7 @@ function reportOutcome(
|
||||
(removeEvidence && evidence.some((message) => message.message_id === messageId));
|
||||
if (removed) return 'removed';
|
||||
}
|
||||
if (kind === 'warning') return 'action_taken';
|
||||
if (kind === 'warning') return 'warned';
|
||||
return RESTRICTING_KINDS.has(kind) ? 'restricted' : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Removed content cannot be restored, so these decisions end only as `reversed`.
|
||||
|
||||
### Reporter outcomes
|
||||
|
||||
A decision that resolves linked reports tells each reporter what happened to the item. `removed` covers a reported message the decision removed, a profile report met by a profile reset, and a community removal. `restricted` covers a suspension, closure or feature limit. `action_taken` covers a warning and tells the reporter that Fluxer acted on what they reported, without naming the action or saying the account was restricted. A content removal or profile reset that leaves the reported item in place leaves the report pending. A manual resolution through [Update report](/admin-api/reports/#update-report) names the outcome itself, and a duplicate takes the outcome stored on the report it repeats.
|
||||
A decision that resolves linked reports tells each reporter what happened to the item. `removed` covers a reported message the decision removed, a profile report met by a profile reset, and a community removal. `restricted` covers a suspension, closure or feature limit. `warned` covers a warning and is stored as such, but the reporter is told the same as for `action_taken`: that Fluxer acted on what they reported, without naming the action or saying the account was restricted. A content removal or profile reset that leaves the reported item in place leaves the report pending. A manual resolution through [Update report](/admin-api/reports/#update-report) names the outcome itself, and a duplicate takes the outcome stored on the report it repeats.
|
||||
|
||||
### Network blocks
|
||||
|
||||
|
||||
@@ -685,7 +685,7 @@ Resolves one report and returns a [report resolution](#report-resolution-object)
|
||||
| public_comment?<sup>1</sup> | string | The comment shown to the reporter (0-512 characters) |
|
||||
| notify_reporter?<sup>2</sup> | boolean | Whether the notice to the reporter includes `public_comment` (default true) |
|
||||
| resolution<sup>3</sup> | string | How the report was resolved, one of `actioned`, `no_violation`, or `duplicate` |
|
||||
| outcome?<sup>4</sup> | string | What happened to the reported item, `removed`, `restricted`, `action_taken`, or `no_violation` |
|
||||
| outcome?<sup>4</sup> | string | What happened to the reported item, `removed`, `warned`, `restricted`, `action_taken`, or `no_violation` |
|
||||
| duplicate_of?<sup>5</sup> | snowflake | The report this one repeats |
|
||||
|
||||
<sup>1</sup> Stored as null when it is omitted or empty. Otherwise it is stored, and the resolution notice shows it to the reporter unless `notify_reporter` is false
|
||||
@@ -694,11 +694,11 @@ Resolves one report and returns a [report resolution](#report-resolution-object)
|
||||
|
||||
<sup>3</sup> Not stored on the report and not returned by any report route. It is recorded in the audit entry
|
||||
|
||||
<sup>4</sup> Required for `actioned`, which refuses `no_violation`. Optional for `no_violation`, which sets `no_violation` and refuses any other value. Refused for `duplicate`. A mistake fails with `REPORT_OUTCOME_INVALID`. `removed` means the reported content, profile field or community was removed. `restricted` means the reported account was restricted and the item was left up. `action_taken` means Fluxer acted under its rules in another way, such as a warning, and the reporter is not told how. `no_violation` means the item breaks no rule and was left as it is. The outcome is stored on the report and the reporter is told it
|
||||
<sup>4</sup> Required for `actioned`, which refuses `no_violation`. Optional for `no_violation`, which sets `no_violation` and refuses any other value. Refused for `duplicate`. A mistake fails with `REPORT_OUTCOME_INVALID`. `removed` means the reported content, profile field or community was removed. `warned` means the account responsible was warned, and the reporter is told only that action was taken. `restricted` means the reported account was restricted and the item was left up. `action_taken` means Fluxer acted under its rules in another way, and the reporter is not told how. `no_violation` means the item breaks no rule and was left as it is. The outcome is stored on the report and the reporter is told it
|
||||
|
||||
<sup>5</sup> Required for `duplicate` and refused otherwise, as is a report naming itself, with `REPORT_OUTCOME_INVALID`. The duplicate takes the outcome stored on that report and the reporter is told it. While that report has no outcome, because it is still pending or was resolved before outcomes were stored, the duplicate is resolved without a notice to its reporter. An unknown report returns 404 `UNKNOWN_REPORT`
|
||||
|
||||
The admin panel's resolve form has a required Resolution select with three choices: Action taken (`actioned`), No violation (`no_violation`), and Duplicate (`duplicate`). Action taken also shows a required Outcome select: Content removed (`removed`), Account restricted (`restricted`), and Other action taken (`action_taken`). No violation sends the outcome `no_violation`. Duplicate shows a required Duplicate of field for the report ID. The panel refuses a submission without a resolution, without an outcome for Action taken, or without a report ID for Duplicate, and the report stays pending. After a resolve, the panel confirms the choice in its message, such as "Report resolved: No violation". Staff find the choice in the `resolution` and `outcome` metadata of the `resolve_report` entry in the audit log.
|
||||
The admin panel's resolve form has a required Resolution select with three choices: Action taken (`actioned`), No violation (`no_violation`), and Duplicate (`duplicate`). Action taken also shows a required Outcome select: Content removed (`removed`), Warning given (`warned`), Account restricted (`restricted`), and Other action taken (`action_taken`). No violation sends the outcome `no_violation`. Duplicate shows a required Duplicate of field for the report ID. The panel refuses a submission without a resolution, without an outcome for Action taken, or without a report ID for Duplicate, and the report stays pending. After a resolve, the panel confirms the choice in its message, such as "Report resolved: No violation". Staff find the choice in the `resolution` and `outcome` metadata of the `resolve_report` entry in the audit log.
|
||||
|
||||
A [decision](/admin-api/enforcement/) that links pending reports resolves them as `actioned` itself, with the outcome its kind implies, so no separate update is needed.
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ const ReportResolutionEnum = createNamedStringLiteralUnion(
|
||||
const ReportItemOutcomeEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['removed', 'removed', 'The reported content, profile field or community was removed'],
|
||||
['warned', 'warned', 'The account responsible for the reported item was warned'],
|
||||
['restricted', 'restricted', 'The reported account was restricted and the content was left up'],
|
||||
['action_taken', 'action_taken', 'Action was taken under the rules, and the reporter is not told which'],
|
||||
['no_violation', 'no_violation', 'The reported content or account does not break the rules and was left as it is'],
|
||||
|
||||
Reference in new issue
Block a user