0 || !boxChecked}
- onClick={ok}
+ onClick={props.accept}
>
{_("OK") +
(acceptTimer > 0 ? " (" + Math.ceil(acceptTimer / 1000) + ")" : "")}
diff --git a/src/components/AccountWarning/CannedMessages.ts b/src/components/AccountWarning/CannedMessages.ts
new file mode 100644
index 0000000000..0c3e3e7955
--- /dev/null
+++ b/src/components/AccountWarning/CannedMessages.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) Online-Go.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { _, interpolate } from "translate";
+
+export const CANNED_MESSAGES: rest_api.warnings.WarningMessages = {
+ warn_beginner_score_cheat: (game_id) =>
+ interpolate(
+ _(`
+ It appears that you delayed the end of game #{{game_id}}, by clicking
+ on the board to change the score incorrectly. This can frustrate your
+ opponent and prevent them from moving on to the next game.
+
+ Since you are a new player, no action will be taken against
+ your account. We simply ask that you learn when to end a game.
+
+ Until you develop the experience to judge better, if your
+ opponent passes and there are no open borders between your
+ stones then you should also pass.
+
+ After passing, promptly accept the correct score.
+
+ If in doubt about this sort of situation. please ask for help in chat
+ or the forums.`),
+ { game_id },
+ ),
+ warn_score_cheat: (game_id) =>
+ interpolate(
+ _(`
+ Our records show that you attempted to illegally change the score at the end of
+ game #{{game_id}}. This is a form of cheating and is prohibited by the
+ OGS Terms of Service.
+
+ https://online-go.com/docs/terms-of-service
+
+ We ask that you end your games properly by accepting the correct score
+ immediately after passing. Further instances of score cheating will result
+ in suspension of your account.`),
+ { game_id },
+ ),
+ ack_educated_beginner_score_cheat: (reported) =>
+ interpolate(
+ _(`
+ Thanks for the report about {{reported}}. It seems you were playing against a
+ complete beginner - we have tried to explain that games should
+ be ended correctly, to pass when their opponent passes, and to accept promptly,
+ trusting the auto-score.`),
+ { reported },
+ ),
+ ack_educated_beginner_score_cheat_and_annul: (reported) =>
+ interpolate(
+ _(`
+ Thanks for the report about {{reported}}. It seems you were playing against a
+ complete beginner - we have tried to explain that games should
+ be ended correctly, to pass when their opponent passes, and to accept promptly,
+ trusting the auto-score. That incorrectly scored game has been annulled.`),
+ { reported },
+ ),
+ ack_warned_score_cheat: (reported) =>
+ interpolate(
+ _(`
+ Thank you for your report, {{reported}} has been given a formal warning about scoring properly.`),
+ { reported },
+ ),
+ ack_warned_score_cheat_and_annul: (reported) =>
+ interpolate(
+ _(`
+ Thank you for your report, {{reported}} has been given a formal warning about scoring properly, and that cheated game annulled.`),
+ reported,
+ ),
+ no_score_cheating_evident: (reported) =>
+ interpolate(
+ _(`
+ Thank you for bringing the possible instance of score cheating by {{reported}} to
+ our attention. We looked into the report and their actions seemed approprate. If a pattern of
+ complaints emerges, we will investigate further.
+
+ Thank you for helping keep OGS enjoyable for everyone. We appreciate it.`),
+ { reported },
+ ),
+};
diff --git a/src/components/ModerateUser/ModerateUser.styl b/src/components/ModerateUser/ModerateUser.styl
index df3186c589..65c1479231 100644
--- a/src/components/ModerateUser/ModerateUser.styl
+++ b/src/components/ModerateUser/ModerateUser.styl
@@ -59,4 +59,9 @@
#ui-class-extra {
width: 5rem;
}
+
+ // We'll probably lay these out better when there are more of them.
+ .avoid-wrap {
+ font-size: smaller;
+ }
}
diff --git a/src/components/ModerateUser/ModerateUser.tsx b/src/components/ModerateUser/ModerateUser.tsx
index b405067f4c..9321c78070 100644
--- a/src/components/ModerateUser/ModerateUser.tsx
+++ b/src/components/ModerateUser/ModerateUser.tsx
@@ -19,13 +19,11 @@ import * as React from "react";
import * as data from "data";
import { _ } from "translate";
import { put, get, del } from "requests";
-import { errorAlerter } from "misc";
+import { MOD_POWER_HANDLE_SCORE_CHEAT, errorAlerter } from "misc";
import { proRankList } from "rank_utils";
import { Modal, openModal } from "Modal";
import { lookup } from "player_cache";
-import { MOD_POWER_ANNUL } from "misc";
-
interface Events {}
interface ModerateUserProperties {
@@ -52,7 +50,8 @@ export class ModerateUser extends Modal {
this.setState(
Object.assign({ loading: false }, result.user, {
bot_owner: result.user.bot_owner ? result.user.bot_owner.id : null,
- can_annul: result.user.moderator_powers & MOD_POWER_ANNUL,
+ can_handle_score_cheat:
+ result.user.moderator_powers & MOD_POWER_HANDLE_SCORE_CHEAT,
}),
);
})
@@ -95,9 +94,9 @@ export class ModerateUser extends Modal {
settings[f] = this.state[f];
}
- // Can-annul is bit zero of moderator_powers
+ // handle_score_cheat is bit zero of moderator_powers
settings["moderator_powers"] =
- (this.state.moderator_powers & ~1) | this.state.can_annul;
+ (this.state.moderator_powers & ~1) | this.state.can_handle_score_cheat;
settings.moderation_note = reason;
@@ -111,7 +110,7 @@ export class ModerateUser extends Modal {
setLockedUsername = (ev) => this.setState({ locked_username: ev.target.checked });
setSupporter = (ev) => this.setState({ supporter: ev.target.checked });
setAnnouncer = (ev) => this.setState({ is_announcer: ev.target.checked });
- setCanAnnul = (ev) => this.setState({ can_annul: ev.target.checked });
+ setHandleScoreCheat = (ev) => this.setState({ can_handle_score_cheat: ev.target.checked });
setProfessional = (ev) => this.setState({ professional: ev.target.checked });
//setBanned = (ev) => this.setState({ is_banned: ev.target.checked });
setShadowbanned = (ev) => this.setState({ is_shadowbanned: ev.target.checked });
@@ -231,14 +230,16 @@ export class ModerateUser extends Modal {
)}
-
+
diff --git a/src/components/NavBar/NavBar.tsx b/src/components/NavBar/NavBar.tsx
index fcbc7daa82..1a3aad830b 100644
--- a/src/components/NavBar/NavBar.tsx
+++ b/src/components/NavBar/NavBar.tsx
@@ -41,7 +41,7 @@ import { logout } from "auth";
import { useUser, useData } from "hooks";
import { OmniSearch } from "./OmniSearch";
-import { MOD_POWER_ANNUL } from "misc";
+import { MOD_POWER_HANDLE_SCORE_CHEAT } from "misc";
const body = $(document.body);
@@ -277,7 +277,8 @@ export function NavBar(): JSX.Element {
{_("Rating Calculator")}
- {(user.is_moderator || !!(user.moderator_powers & MOD_POWER_ANNUL)) && (
+ {(user.is_moderator ||
+ !!(user.moderator_powers & MOD_POWER_HANDLE_SCORE_CHEAT)) && (
{_("Reports Center")}
diff --git a/src/lib/misc.ts b/src/lib/misc.ts
index fe5671c093..40458695af 100644
--- a/src/lib/misc.ts
+++ b/src/lib/misc.ts
@@ -21,7 +21,7 @@ import { browserHistory } from "ogsHistory";
import * as preferences from "preferences";
import { alert } from "swal_config";
-export const MOD_POWER_ANNUL = 1; // Matches back-end MOD_POWER
+export const MOD_POWER_HANDLE_SCORE_CHEAT = 1; // Matches back-end MOD_POWER
export type Timeout = ReturnType;
diff --git a/src/lib/report_manager.ts b/src/lib/report_manager.tsx
similarity index 84%
rename from src/lib/report_manager.ts
rename to src/lib/report_manager.tsx
index 0d2a7d6980..0a0cb2ed24 100644
--- a/src/lib/report_manager.ts
+++ b/src/lib/report_manager.tsx
@@ -20,24 +20,36 @@
* which is used by our IncidentReportTracker widget and our ReportsCenter view.
*/
+import * as React from "react";
import * as data from "data";
import * as preferences from "preferences";
+import { toast } from "toast";
import { alert } from "swal_config";
import { socket } from "sockets";
+import { pgettext } from "translate";
import { ReportedConversation } from "Report";
import { PlayerCacheEntry } from "player_cache";
import { EventEmitter } from "eventemitter3";
import { emitNotification } from "Notifications";
import { browserHistory } from "ogsHistory";
import { get, post } from "requests";
+import { MOD_POWER_HANDLE_SCORE_CHEAT } from "./misc";
export const DAILY_REPORT_GOAL = 10;
+interface Vote {
+ voter_id: number;
+ action: string;
+}
+
export interface Report {
+ // TBD put this into /models, in a suitable namespace?
+ // TBD: relationship between this and SeverToClient['incident-report']
id: number;
created: string;
updated: string;
state: string;
+ escalated: boolean;
source: string;
report_type: string;
reporting_user: any;
@@ -63,6 +75,9 @@ export interface Report {
automod_to_reporter?: string;
automod_to_reported?: string;
+ available_actions: Array; // community moderator actions
+ voters: Vote[]; // votes from community moderators on this report
+
unclaim: () => void;
claim: () => void;
steal: () => void;
@@ -122,6 +137,7 @@ class ReportManager extends EventEmitter {
}
public updateIncidentReport(report: Report) {
+ const user = data.get("user");
report.id = parseInt(report.id as unknown as string);
if (!(report.id in this.active_incident_reports)) {
@@ -146,7 +162,10 @@ class ReportManager extends EventEmitter {
}
}
- if (report.state === "resolved") {
+ if (
+ report.state === "resolved" ||
+ report.voters?.some((vote) => vote.voter_id === user.id)
+ ) {
delete this.active_incident_reports[report.id];
} else {
this.active_incident_reports[report.id] = report;
@@ -189,7 +208,25 @@ class ReportManager extends EventEmitter {
if (this.getIgnored(report.id)) {
return false;
}
- if (!user.is_moderator && !(report.report_type === "score_cheating")) {
+ if (!user.is_moderator && !user.moderator_powers) {
+ return false;
+ }
+ // Community moderators only get to see score_cheating reports that they
+ // have not yet voted on.
+ if (
+ !user.is_moderator &&
+ user.moderator_powers &&
+ (!(
+ report.report_type === "score_cheating" &&
+ user.moderator_powers & MOD_POWER_HANDLE_SCORE_CHEAT
+ ) ||
+ report.voters?.some((vote) => vote.voter_id === user.id))
+ ) {
+ return false;
+ }
+ // don't hand score cheating reports to full mods unless the report is escalated,
+ // because community moderators are supposed to do these!
+ if (user.is_moderator && report.report_type === "score_cheating" && !report.escalated) {
return false;
}
return !report.moderator || report.moderator?.id === user.id;
@@ -323,6 +360,23 @@ class ReportManager extends EventEmitter {
this.updateIncidentReport(res);
return res;
}
+ public async vote(report_id: number, voted_action: string) {
+ delete this.active_incident_reports[report_id];
+ this.update();
+ const res = await post(`moderation/incident/${report_id}`, {
+ action: "vote", // darn, yes, two different uses of the word "action" collide here
+ voted_action: voted_action,
+ }).then((res) => {
+ toast(
+
+ {pgettext("Thanking a community moderator for voting", "Submitted, thanks!")}
+
,
+ 2000,
+ );
+ return res;
+ });
+ this.updateIncidentReport(res);
+ }
public getHandledTodayCount(): number {
return data.get("user").reports_handled_today || 0;
diff --git a/src/models/warning.d.ts b/src/models/warning.d.ts
new file mode 100644
index 0000000000..506ad19fa2
--- /dev/null
+++ b/src/models/warning.d.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) Online-Go.com
+ * Copyright (C) Ben Jones
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+declare namespace rest_api {
+ namespace warnings {
+ type WarningMessageId =
+ | "warn_beginner_score_cheat"
+ | "warn_score_cheat"
+ | "ack_educated_beginner_score_cheat"
+ | "ack_educated_beginner_score_cheat_and_annul"
+ | "ack_warned_score_cheat"
+ | "ack_warned_score_cheat_and_annul"
+ | "no_score_cheating_evident";
+
+ type Severity = "warning" | "acknowledgement";
+
+ type InterpolatedMessage = (data) => string;
+
+ type WarningMessages = {
+ [K in WarningMessageId]: InterpolatedMessage;
+ };
+
+ interface Warning {
+ id: number;
+ created: string;
+ acknowledged: string | null; // date
+ player_id: number;
+ moderator: number | null;
+ text: string | null;
+ message_id: WarningMessageId | null;
+ severity: Severity;
+ interpolation_data: string | null;
+ }
+ }
+}
diff --git a/src/views/ReportsCenter/ModerationActionSelector.tsx b/src/views/ReportsCenter/ModerationActionSelector.tsx
new file mode 100644
index 0000000000..bcd05068ed
--- /dev/null
+++ b/src/views/ReportsCenter/ModerationActionSelector.tsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) Online-Go.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import * as React from "react";
+import { Report } from "report_manager";
+import { _, pgettext } from "translate";
+
+interface ModerationActionSelectorProps {
+ report: Report;
+ enable: boolean;
+ claim: () => void;
+ submit: (action: string) => void;
+}
+
+const ACTION_PROMPTS = {
+ annul_score_cheat: pgettext(
+ "A label for a moderator to select this option",
+ "Annul the game and warn the cheater.",
+ ),
+ warn_score_cheat: pgettext(
+ "Label for a moderator to select this option",
+ "The accused tried to cheat - warn the cheater.",
+ ),
+ no_score_cheat: pgettext(
+ "Label for a moderator to select this option",
+ "No cheating - inform the reporter.",
+ ),
+ escalate: pgettext(
+ "A label for a community moderator to select this option - send report to to full moderators",
+ "Escalate.",
+ ),
+};
+
+export function ModerationActionSelector({
+ report,
+ enable,
+ claim,
+ submit,
+}: ModerationActionSelectorProps): JSX.Element {
+ const [selectedOption, setSelectedOption] = React.useState("");
+
+ const updateSelectedAction = (e: React.ChangeEvent) => {
+ setSelectedOption(e.target.value);
+ claim();
+ };
+
+ return (
+
+
+ {pgettext(
+ "The heading for community moderators 'action choices' section",
+ "Actions",
+ )}
+