Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for CMs voting for suspend #2844

Merged
merged 2 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/components/AccountWarning/CannedMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
_(`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good candidates for llm_pgettext maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(It felt simple and stable to me, but hey why not :) )

Thank you for your report. {{reported}} is a repeat offender, their has been suspended. \
The reported game has been annulled.
`),
{ reported },
),
};
13 changes: 13 additions & 0 deletions src/components/ModerateUser/ModerateUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,19 @@ export class ModerateUser extends Modal<Events, ModerateUserProperties, any> {
onRetractOffer={this.retractOffer}
onRemovePower={this.removePower}
/>
<ModerationOfferControl
ability={pgettext(
"Label for a button to let a community moderator vote for suspension",
"Vote for Suspension",
)}
ability_mask={MODERATOR_POWERS.SUSPEND}
currently_offered={this.state.offered_moderator_powers}
moderator_powers={this.state.moderator_powers}
previously_rejected={this.state.mod_powers_rejected}
onMakeOffer={this.makeOffer}
onRetractOffer={this.retractOffer}
onRemovePower={this.removePower}
/>
</div>
)}
<div className="buttons">
Expand Down
2 changes: 2 additions & 0 deletions src/lib/moderation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {
Expand All @@ -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(
Expand Down
45 changes: 26 additions & 19 deletions src/lib/report_manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,6 +88,7 @@ class ReportManager extends EventEmitter<Events> {
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 &&
Expand All @@ -109,11 +111,19 @@ class ReportManager extends EventEmitter<Events> {
}
}

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,
Expand Down Expand Up @@ -148,6 +158,7 @@ class ReportManager extends EventEmitter<Events> {
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");
}
Expand All @@ -162,7 +173,6 @@ class ReportManager extends EventEmitter<Events> {
// Clients should use getEligibleReports
private getAvailableReports(): Report[] {
const user = data.get("user");

return this.sorted_active_incident_reports.filter((report) => {
if (!report) {
return false;
Expand All @@ -185,22 +195,19 @@ class ReportManager extends EventEmitter<Events> {
// 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
Expand Down
9 changes: 7 additions & 2 deletions src/lib/report_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ReportType } from "@/components/Report";
interface Vote {
voter_id: number;
action: string;
updated: string;
}

export interface Report {
Expand All @@ -33,6 +34,7 @@ export interface Report {
updated: string;
state: string;
escalated: boolean;
escalated_at: string;
retyped: boolean;
source: string;
report_type: ReportType;
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion src/models/warning.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
8 changes: 8 additions & 0 deletions src/views/ReportsCenter/ModerationActionSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 17 additions & 10 deletions src/views/ReportsCenter/ViewReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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}
Expand Down
15 changes: 13 additions & 2 deletions src/views/Settings/ModeratorPreferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -89,8 +97,11 @@ export function ModeratorPreferences(_props: SettingGroupPageProps): JSX.Element
<Toggle checked={hide_claimed_reports} onChange={setHideClaimedReports} />
</PreferenceLine>
<PreferenceLine title="Show un-escalated reports">
<Toggle checked={show_cm_reports} onChange={setShowCMReports} />
<span>This will include for you reports that CMs can still vote on</span>
<Toggle checked={false} onChange={() => {}} />
<span>
This would include for you reports that CMs can still vote on, but is
not currently available.
</span>
</PreferenceLine>
<PreferenceLine title="Join games anonymously">
<Toggle
Expand Down
Loading