From 82736d63aa349fe5d806d31d25701a3419c05d8f Mon Sep 17 00:00:00 2001 From: GreenAsJade Date: Sat, 5 Oct 2024 17:12:44 +0930 Subject: [PATCH] Support for CMs voting for suspend --- .../AccountWarning/CannedMessages.ts | 18 +++++++- src/components/ModerateUser/ModerateUser.tsx | 13 ++++++ src/lib/moderation.tsx | 2 + src/lib/report_manager.tsx | 45 +++++++++++-------- src/lib/report_util.ts | 9 +++- src/models/warning.d.ts | 4 +- .../ModerationActionSelector.tsx | 8 ++++ src/views/ReportsCenter/ViewReport.tsx | 27 ++++++----- src/views/Settings/ModeratorPreferences.tsx | 15 ++++++- 9 files changed, 106 insertions(+), 35 deletions(-) diff --git a/src/components/AccountWarning/CannedMessages.ts b/src/components/AccountWarning/CannedMessages.ts index 38772ddb30..32261d17d8 100644 --- a/src/components/AccountWarning/CannedMessages.ts +++ b/src/components/AccountWarning/CannedMessages.ts @@ -294,7 +294,23 @@ moderator to let them know. Thanks for your recent report about {{bot}}. We've notified the owner of that bot. - `), +`), { bot }, ), + ack_suspended: (reported) => + interpolate( + _(` +Thank you for your report. {{reported}} is a repeat offender, their account has been suspended. +`), + { reported }, + ), + + ack_suspended_and_annul: (reported) => + interpolate( + _(` +Thank you for your report. {{reported}} is a repeat offender, their has been suspended. \ +The reported game has been annulled. +`), + { reported }, + ), }; diff --git a/src/components/ModerateUser/ModerateUser.tsx b/src/components/ModerateUser/ModerateUser.tsx index 9f6dbddef1..66977d940d 100644 --- a/src/components/ModerateUser/ModerateUser.tsx +++ b/src/components/ModerateUser/ModerateUser.tsx @@ -381,6 +381,19 @@ export class ModerateUser extends Modal { onRetractOffer={this.retractOffer} onRemovePower={this.removePower} /> + )}
diff --git a/src/lib/moderation.tsx b/src/lib/moderation.tsx index 26b93f0ece..79a47316f2 100644 --- a/src/lib/moderation.tsx +++ b/src/lib/moderation.tsx @@ -31,6 +31,7 @@ export enum MODERATOR_POWERS { HANDLE_SCORE_CHEAT = 0b001, HANDLE_ESCAPING = 0b010, HANDLE_STALLING = 0b100, + SUSPEND = 0b1000, } export const MOD_POWER_NAMES: { [key in MODERATOR_POWERS]: string } = { @@ -47,6 +48,7 @@ export const MOD_POWER_NAMES: { [key in MODERATOR_POWERS]: string } = { "A label for a moderator power", "Handle Stalling Reports", ), + [MODERATOR_POWERS.SUSPEND]: pgettext("A label for a moderator power", "Vote for Suspension"), }; export function doAnnul( diff --git a/src/lib/report_manager.tsx b/src/lib/report_manager.tsx index 888aab226b..349117291f 100644 --- a/src/lib/report_manager.tsx +++ b/src/lib/report_manager.tsx @@ -32,6 +32,7 @@ import { EventEmitter } from "eventemitter3"; import { emitNotification } from "@/components/Notifications"; import { browserHistory } from "@/lib/ogsHistory"; import { get, post } from "@/lib/requests"; +import { MODERATOR_POWERS } from "./moderation"; export interface ReportRelation { relationship: string; @@ -87,6 +88,7 @@ class ReportManager extends EventEmitter { const user = data.get("user"); report.id = parseInt(report.id as unknown as string); + console.log("updateIncidentReport", report); if (!(report.id in this.active_incident_reports)) { if ( data.get("user").is_moderator && @@ -109,11 +111,19 @@ class ReportManager extends EventEmitter { } } - if ( - report.state === "resolved" || - report.voters?.some((vote) => vote.voter_id === user.id) || - (user.moderator_powers && report.escalated) - ) { + // They voted if there is a vote from them (obviously) - but: + // if the report is escalated _and_ they have SUSPEND power, we are only interested in votes + // after the escalated_at time + + const they_already_voted = report.voters?.some( + (vote) => + vote.voter_id === user.id && + (!report.escalated || // If the report is not escalated, any vote counts + !(user.moderator_powers & MODERATOR_POWERS.SUSPEND) || // If the user does not have SUSPEND powers, any vote counts + new Date(vote.updated) > new Date(report.escalated_at)), // If the user has SUSPEND powers, vote must be after escalation + ); + + if (report.state === "resolved" || they_already_voted) { delete this.active_incident_reports[report.id]; this.this_user_reported_games = this.this_user_reported_games.filter( (game_id) => game_id !== report.reported_game, @@ -148,6 +158,7 @@ class ReportManager extends EventEmitter { reports.sort(compare_reports); this.sorted_active_incident_reports = reports; + console.log("active reports", reports.length, normal_ct); this.emit("active-count", normal_ct); this.emit("update"); } @@ -162,7 +173,6 @@ class ReportManager extends EventEmitter { // Clients should use getEligibleReports private getAvailableReports(): Report[] { const user = data.get("user"); - return this.sorted_active_incident_reports.filter((report) => { if (!report) { return false; @@ -185,22 +195,19 @@ class ReportManager extends EventEmitter { // that they have not yet voted on, and are not escalated if (user.moderator_powers && !community_mod_can_handle(user, report)) { + console.log("community_mod_can_handle reject", report.id, report.report_type); return false; } - const show_cm_reports = preferences.get("show-cm-reports"); - if (!show_cm_reports) { - // don't hand community moderation reports to full mods unless the report is escalated, - // or they've asked to show them explicitly in settings, - // because community moderators are supposed to do these! - if ( - user.is_moderator && - !(report.moderator?.id === user.id) && // maybe they already have it, so they need to see it - ["escaping", "score_cheating", "stalling"].includes(report.report_type) && - !report.escalated - ) { - return false; - } + // Don't offer community moderation reports to full mods, because community moderators do these. + // (The only way full moderators see CM-class reports is if they go hunting and claim them) + if ( + user.is_moderator && + !(report.moderator?.id === user.id) && // maybe they already have it, so they need to see it + ["escaping", "score_cheating", "stalling"].includes(report.report_type) && + !report.escalated + ) { + return false; } // Never give a claimed report to community moderators diff --git a/src/lib/report_util.ts b/src/lib/report_util.ts index b88b5d6f93..d0222e7bcf 100644 --- a/src/lib/report_util.ts +++ b/src/lib/report_util.ts @@ -23,6 +23,7 @@ import { ReportType } from "@/components/Report"; interface Vote { voter_id: number; action: string; + updated: string; } export interface Report { @@ -33,6 +34,7 @@ export interface Report { updated: string; state: string; escalated: boolean; + escalated_at: string; retyped: boolean; source: string; report_type: ReportType; @@ -100,14 +102,17 @@ export function community_mod_has_power( export function community_mod_can_handle(user: rest_api.UserConfig, report: Report): boolean { // Community moderators only get to see reports that they have the power for and - // that they have not yet voted on, and are not escalated + // that they have not yet voted on... or if it's escalated, they must have suspend power if (!user.moderator_powers) { return false; } + + const they_already_voted = report.voters?.some((vote) => vote.voter_id === user.id); + const they_can_vote_to_suspend = user.moderator_powers & MODERATOR_POWERS.SUSPEND; if ( community_mod_has_power(user.moderator_powers, report.report_type) && - !(report.voters?.some((vote) => vote.voter_id === user.id) || report.escalated) + (!they_already_voted || (report.escalated && they_can_vote_to_suspend)) ) { return true; } diff --git a/src/models/warning.d.ts b/src/models/warning.d.ts index 1daca72378..3874f9679e 100644 --- a/src/models/warning.d.ts +++ b/src/models/warning.d.ts @@ -44,7 +44,9 @@ declare namespace rest_api { | "no_stalling_evident" | "warn_duplicate_report" | "report_type_changed" - | "bot_owner_notified"; + | "bot_owner_notified" + | "ack_suspended" + | "ack_suspended_and_annul"; type Severity = "warning" | "acknowledgement" | "info"; diff --git a/src/views/ReportsCenter/ModerationActionSelector.tsx b/src/views/ReportsCenter/ModerationActionSelector.tsx index 09e5d69396..a9f4ec90b6 100644 --- a/src/views/ReportsCenter/ModerationActionSelector.tsx +++ b/src/views/ReportsCenter/ModerationActionSelector.tsx @@ -102,6 +102,14 @@ const ACTION_PROMPTS = { "Label for a moderator to select this option", "Duplicate report - ask them not to do that.", ), + + suspend_user: pgettext("Label for a moderator to select this option", "Suspend the user."), + + suspend_user_and_annul: pgettext( + "Label for a moderator to select this option", + "Suspend user and annul game.", + ), + // Note: keep this last, so it's positioned above the "note to moderator" input field escalate: pgettext( "A label for a community moderator to select this option - send report to to full moderators", diff --git a/src/views/ReportsCenter/ViewReport.tsx b/src/views/ReportsCenter/ViewReport.tsx index 49e45c277b..41141793e9 100644 --- a/src/views/ReportsCenter/ViewReport.tsx +++ b/src/views/ReportsCenter/ViewReport.tsx @@ -41,6 +41,7 @@ import { ReportTypeSelector } from "./ReportTypeSelector"; import { alert } from "@/lib/swal_config"; import { ErrorBoundary } from "@/components/ErrorBoundary"; import * as DynamicHelp from "react-dynamic-help"; +import { MODERATOR_POWERS } from "@/lib/moderation"; interface ViewReportProps { reports: Report[]; @@ -77,6 +78,15 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): J const { registerTargetItem } = React.useContext(DynamicHelp.Api); const { ref: ignore_button } = registerTargetItem("ignore-button"); + const captureReport = (report: Report) => { + setReport(report); + setModeratorId(report?.moderator?.id); + setReportState(report?.state); + setAnnulQueue(report?.detected_ai_games); + setAvailableActions(report?.available_actions); + setVoteCounts(report?.vote_counts); + }; + React.useEffect(() => { if (report_id) { // For some reason we have to capture the state of the report at the time that report_id goes valid @@ -86,12 +96,7 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): J .getReport(report_id) .then((report) => { setError(null); - setReport(report); - setModeratorId(report?.moderator?.id); - setReportState(report?.state); - setAnnulQueue(report?.detected_ai_games); - setAvailableActions(report?.available_actions); - setVoteCounts(report?.vote_counts); + captureReport(report); }) .catch((err) => { console.error(err); @@ -108,9 +113,7 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): J React.useEffect(() => { const onUpdate = (r: Report) => { if (r.id === report?.id) { - setReport(r); - setModeratorId(r?.moderator?.id); - setReportState(r?.state); + captureReport(r); } }; report_manager.on("incident-report", onUpdate); @@ -550,7 +553,11 @@ export function ViewReport({ report_id, reports, onChange }: ViewReportProps): J .vote(report.id, action, note) .then(() => next()); }} - enable={report.state === "pending" && !report.escalated} + enable={ + report.state === "pending" && + (!report.escalated || + !!(user.moderator_powers & MODERATOR_POWERS.SUSPEND)) + } // clear the selection for subsequent reports key={report.id} report={report} diff --git a/src/views/Settings/ModeratorPreferences.tsx b/src/views/Settings/ModeratorPreferences.tsx index f4f14347dd..462e54488c 100644 --- a/src/views/Settings/ModeratorPreferences.tsx +++ b/src/views/Settings/ModeratorPreferences.tsx @@ -56,6 +56,14 @@ export function ModeratorPreferences(_props: SettingGroupPageProps): JSX.Element _setReportQuota(ev.target.value as any); } } + + // At the moment we want moderators do non-CM reports + React.useEffect(() => { + if (show_cm_reports) { + setShowCMReports(false); + } + }, [show_cm_reports, setShowCMReports]); + if (!user.is_moderator && !user.moderator_powers) { return null; } @@ -89,8 +97,11 @@ export function ModeratorPreferences(_props: SettingGroupPageProps): JSX.Element - - This will include for you reports that CMs can still vote on + {}} /> + + This would include for you reports that CMs can still vote on, but is + not currently available. +