diff --git a/src/components/AccountWarning/CannedMessages.ts b/src/components/AccountWarning/CannedMessages.ts index 92f62a68f6..56727fb08e 100644 --- a/src/components/AccountWarning/CannedMessages.ts +++ b/src/components/AccountWarning/CannedMessages.ts @@ -90,8 +90,7 @@ Thank you for your report, '{{reported}}' has been given a formal warning about llm_pgettext( "Acknowledgement message to a user", ` -Thank you for bringing the possible instance of '{{reported}}' abandoning the game to -our attention. +Thank you for bringing the possible instance of '{{reported}}' abandoning the game to our attention. We looked into the game and did not see them failing to finish the game properly. diff --git a/src/components/IncidentReportTracker/IncidentReportCard.tsx b/src/components/IncidentReportTracker/IncidentReportCard.tsx index a21545b362..a5057ace7c 100644 --- a/src/components/IncidentReportTracker/IncidentReportCard.tsx +++ b/src/components/IncidentReportTracker/IncidentReportCard.tsx @@ -33,12 +33,22 @@ import { Player } from "@/components/Player"; import { errorAlerter } from "@/lib/misc"; import { AutoTranslate } from "@/components/AutoTranslate"; -import { Report } from "@/lib/report_util"; +import { ReportNotification } from "@/lib/report_util"; import { useUser } from "@/lib/hooks"; import { report_categories } from "@/components/Report"; import { openReportedConversationModal } from "@/components/ReportedConversationModal"; -function getReportType(report: Report): string { +export type ActionableReport = ReportNotification & { + unclaim: () => void; + good_report: () => void; + bad_report: () => void; + steal: () => void; + claim: () => void; + cancel: () => void; + set_note: () => void; +}; + +function getReportType(report: ActionableReport): string { if (report.report_type === "appeal") { return "Ban Appeal"; } @@ -49,7 +59,7 @@ function getReportType(report: Report): string { } interface IncidentReportCardProps { - report: Report; + report: ActionableReport; } export function IncidentReportCard({ report }: IncidentReportCardProps): React.ReactElement { @@ -178,13 +188,13 @@ export function IncidentReportCard({ report }: IncidentReportCardProps): React.R )} - {report.reported_conversation && ( + {report.reported_conversation && report.reported_user && (
{ openReportedConversationModal( - report.reported_user?.id, - report.reported_conversation, + report.reported_user!.id, + report.reported_conversation!, ); }} > diff --git a/src/components/IncidentReportTracker/IncidentReportList.tsx b/src/components/IncidentReportTracker/IncidentReportList.tsx index c1f3d46c9f..7b96bf8a98 100644 --- a/src/components/IncidentReportTracker/IncidentReportList.tsx +++ b/src/components/IncidentReportTracker/IncidentReportList.tsx @@ -21,16 +21,17 @@ import { alert } from "@/lib/swal_config"; import { post } from "@/lib/requests"; import { ignore, errorAlerter } from "@/lib/misc"; -import { Report } from "@/lib/report_util"; +import { ReportNotification } from "@/lib/report_util"; -import { IncidentReportCard } from "./IncidentReportCard"; +import { IncidentReportCard, ActionableReport } from "./IncidentReportCard"; -// Define a type for the props type IncidentReportListProps = { - reports: Report[]; + reports: ReportNotification[]; modal?: boolean; }; +// This presents a list of incident reports with a summary and actions. +// It's intended to be built from Report Notifications that we get from the term server via report_manager export function IncidentReportList({ reports, modal = true, @@ -40,14 +41,16 @@ export function IncidentReportList({ } // Attach appropriate actions to each report - reports.forEach((report) => { - if (report.state !== "resolved") { - report.unclaim = () => { + const actionableReports: ActionableReport[] = reports.map((report) => { + const actionableReport = report as ActionableReport; + + if (actionableReport.state !== "resolved") { + actionableReport.unclaim = () => { post(`moderation/incident/${report.id}`, { id: report.id, action: "unclaim" }) .then(ignore) .catch(errorAlerter); }; - report.good_report = () => { + actionableReport.good_report = () => { post(`moderation/incident/${report.id}`, { id: report.id, action: "resolve", @@ -56,7 +59,7 @@ export function IncidentReportList({ .then(ignore) .catch(errorAlerter); }; - report.bad_report = () => { + actionableReport.bad_report = () => { post(`moderation/incident/${report.id}`, { id: report.id, action: "resolve", @@ -65,7 +68,7 @@ export function IncidentReportList({ .then(ignore) .catch(errorAlerter); }; - report.steal = () => { + actionableReport.steal = () => { post(`moderation/incident/${report.id}`, { id: report.id, action: "steal" }) .then((res) => { if (res.vanished) { @@ -74,7 +77,7 @@ export function IncidentReportList({ }) .catch(errorAlerter); }; - report.claim = () => { + actionableReport.claim = () => { post(`moderation/incident/${report.id}`, { id: report.id, action: "claim" }) .then((res) => { if (res.vanished) { @@ -86,17 +89,17 @@ export function IncidentReportList({ }) .catch(errorAlerter); }; - report.cancel = () => { + actionableReport.cancel = () => { post(`moderation/incident/${report.id}`, { id: report.id, action: "cancel" }) .then(ignore) .catch(errorAlerter); }; - report.set_note = () => { + actionableReport.set_note = () => { void alert .fire({ input: "text", - inputValue: report.moderator_note, + inputValue: report.moderator_note || "", showCancelButton: true, }) .then(({ value: txt, isConfirmed }) => { @@ -112,13 +115,14 @@ export function IncidentReportList({ }); }; } + return actionableReport; }); return (
{modal &&
}
- {reports.map((report: Report, index) => ( + {actionableReports.map((report: ActionableReport, index) => ( ))}
diff --git a/src/lib/report_manager.tsx b/src/lib/report_manager.tsx index 73e999796a..108c650a8f 100644 --- a/src/lib/report_manager.tsx +++ b/src/lib/report_manager.tsx @@ -27,20 +27,22 @@ import { toast } from "@/lib/toast"; import { alert } from "@/lib/swal_config"; import { socket } from "@/lib/sockets"; import { pgettext } from "@/lib/translate"; -import { Report, community_mod_can_handle } from "@/lib/report_util"; +import { ReportNotification, community_mod_can_handle } from "@/lib/report_util"; 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"; +type ReportDetail = rest_api.moderation.ReportDetail; + export interface ReportRelation { relationship: string; - report: Report; + report: ReportNotification; } interface Events { - "incident-report": (report: Report) => void; + "incident-report": (report: ReportNotification) => void; "active-count": (count: number) => void; update: () => void; } @@ -50,8 +52,8 @@ interface Events { let post_connect_notification_squelch = true; class ReportManager extends EventEmitter { - active_incident_reports: { [id: string]: Report } = {}; - sorted_active_incident_reports: Report[] = []; + active_incident_reports: { [id: string]: ReportNotification } = {}; + sorted_active_incident_reports: ReportNotification[] = []; this_user_reported_games: number[] = []; constructor() { @@ -72,7 +74,7 @@ class ReportManager extends EventEmitter { } socket.on("incident-report", (report) => - this.updateIncidentReport(report as any as Report), + this.updateIncidentReport(report as any as ReportNotification), ); preferences.watch("moderator.report-settings", () => { @@ -84,11 +86,10 @@ class ReportManager extends EventEmitter { }); } - public updateIncidentReport(report: Report) { + public updateIncidentReport(report: ReportNotification) { 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 && @@ -143,7 +144,7 @@ class ReportManager extends EventEmitter { const prefs = preferences.get("moderator.report-settings"); const user = data.get("user"); - const reports: Report[] = []; + const reports: ReportNotification[] = []; let normal_ct = 0; for (const id in this.active_incident_reports) { const report = this.active_incident_reports[id]; @@ -163,18 +164,18 @@ class ReportManager extends EventEmitter { this.emit("update"); } - public getEligibleReports(): Report[] { + public getEligibleReports(): ReportNotification[] { const quota = preferences.get("moderator.report-quota"); return !quota || this.getHandledTodayCount() < preferences.get("moderator.report-quota") ? this.getAvailableReports() : // Always show the user their own reports this.getAvailableReports().filter( - (report) => report.reporting_user.id === data.get("user").id, + (report) => report.reporting_user?.id === data.get("user").id, ); } // Clients should use getEligibleReports - private getAvailableReports(): Report[] { + private getAvailableReports(): ReportNotification[] { const user = data.get("user"); return this.sorted_active_incident_reports.filter((report) => { if (!report) { @@ -286,11 +287,7 @@ class ReportManager extends EventEmitter { this.update(); } - public async getReport(id: number): Promise { - if (id in this.active_incident_reports) { - return this.active_incident_reports[id]; - } - + public async getReportDetails(id: number): Promise { const res = await get(`moderation/incident/${id}`); if (res) { @@ -300,7 +297,7 @@ class ReportManager extends EventEmitter { throw new Error("Report not found"); } - public async reopen(report_id: number): Promise { + public async reopen(report_id: number): Promise { const res = await post(`moderation/incident/${report_id}`, { id: report_id, action: "reopen", @@ -308,7 +305,7 @@ class ReportManager extends EventEmitter { this.updateIncidentReport(res); return res; } - public async close(report_id: number, helpful: boolean): Promise { + public async close(report_id: number, helpful: boolean): Promise { delete this.active_incident_reports[report_id]; this.update(); const res = await post(`moderation/incident/${report_id}`, { @@ -319,17 +316,17 @@ class ReportManager extends EventEmitter { this.updateIncidentReport(res); return res; } - public async good_report(report_id: number): Promise { + public async good_report(report_id: number): Promise { const res = await this.close(report_id, true); this.updateIncidentReport(res); return res; } - public async bad_report(report_id: number): Promise { + public async bad_report(report_id: number): Promise { const res = await this.close(report_id, false); this.updateIncidentReport(res); return res; } - public async unclaim(report_id: number): Promise { + public async unclaim(report_id: number): Promise { const res = await post(`moderation/incident/${report_id}`, { id: report_id, action: "unclaim", @@ -337,7 +334,7 @@ class ReportManager extends EventEmitter { this.updateIncidentReport(res); return res; } - public async claim(report_id: number): Promise { + public async claim(report_id: number): Promise { const res = await post(`moderation/incident/${report_id}`, { id: report_id, action: "claim", @@ -359,7 +356,7 @@ class ReportManager extends EventEmitter { voted_action: string, escalation_note: string, dissenter_note: string, - ): Promise { + ): Promise { return post(`moderation/incident/${report_id}`, { action: "vote", voted_action: voted_action, @@ -397,7 +394,7 @@ class ReportManager extends EventEmitter { } } -function compare_reports(a: Report, b: Report): number { +function compare_reports(a: ReportNotification, b: ReportNotification): number { const prefs = preferences.get("moderator.report-settings"); const sort_order = preferences.get("moderator.report-sort-order"); const user = data.get("user"); @@ -411,13 +408,13 @@ function compare_reports(a: Report, b: Report): number { return custom_ordering || (sort_order === "newest-first" ? b.id - a.id : a.id - b.id); } if (a.moderator && !b.moderator) { - if (a.moderator.id === user.id) { + if (a.moderator?.id === user.id) { return A_BEFORE_B; } return B_BEFORE_A; } if (b.moderator && !a.moderator) { - if (b.moderator.id === user.id) { + if (b.moderator?.id === user.id) { return B_BEFORE_A; } return A_BEFORE_B; @@ -426,10 +423,10 @@ function compare_reports(a: Report, b: Report): number { // both have moderators, sort our mod reports first, then other // mods, then by id - if (a.moderator.id !== user.id && b.moderator.id === user.id) { + if (a.moderator?.id !== user.id && b.moderator?.id === user.id) { return B_BEFORE_A; } - if (a.moderator.id === user.id && b.moderator.id !== user.id) { + if (a.moderator?.id === user.id && b.moderator?.id !== user.id) { return A_BEFORE_B; } diff --git a/src/lib/report_util.ts b/src/lib/report_util.ts index e38079e60b..3bbaefa6bc 100644 --- a/src/lib/report_util.ts +++ b/src/lib/report_util.ts @@ -15,68 +15,14 @@ * along with this program. If not, see . */ -import { ReportedConversation } from "@/components/Report"; -import { PlayerCacheEntry } from "@/lib/player_cache"; import { MODERATOR_POWERS } from "@/lib/moderation"; import { ReportType } from "@/components/Report"; -interface Vote { - voter_id: number; - action: string; - updated: 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; - escalated_at: string; - retyped: boolean; - source: string; - report_type: ReportType; - reporting_user: PlayerCacheEntry; - reported_user: PlayerCacheEntry; - reported_game: number; - reported_game_move?: number; - reported_review: number; - reported_conversation: ReportedConversation; - url: string; - moderator: PlayerCacheEntry; - cleared_by_user: boolean; - was_helpful: boolean; - reporter_note: string; - reporter_note_translation: { - source_language: string; - target_language: string; - source_text: string; - target_text: string; - }; - moderator_note: string; - system_note: string; - detected_ai_games: Array; - - automod_to_moderator?: string; // Suggestions from "automod" - automod_to_reporter?: string; - automod_to_reported?: string; +import type { ServerToClient } from "submodules/goban/src/engine/protocol/ServerToClient"; - available_actions: Array; // community moderator actions - vote_counts: { [action: string]: number }; - voters: Vote[]; // votes from community moderators on this report - escalation_note: string; - dissenter_note: string; - - unclaim: () => void; - claim: () => void; - steal: () => void; - bad_report: () => void; - good_report: () => void; - cancel: () => void; - set_note: () => void; -} +export type ReportNotification = ServerToClient["incident-report"] extends (data: infer T) => void + ? T + : never; type CommunityModeratorReportTypes = Partial>; @@ -104,7 +50,10 @@ export function community_mod_has_power( ); } -export function community_mod_can_handle(user: rest_api.UserConfig, report: Report): boolean { +export function community_mod_can_handle( + user: rest_api.UserConfig, + report: ReportNotification, +): boolean { // Community moderators only get to see reports that they have the power for and // that they have not yet voted on... or if it's escalated, they must have suspend power // AI report are different - CMs handle them pre-escalation, full moderators after escalation! @@ -116,7 +65,7 @@ export function community_mod_can_handle(user: rest_api.UserConfig, report: Repo 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) && + community_mod_has_power(user.moderator_powers, report.report_type as ReportType) && (!they_already_voted || (report.escalated && they_can_vote_to_suspend && !(report.report_type === "ai_use"))) ) { diff --git a/src/models/moderation.d.ts b/src/models/moderation.d.ts index b66e5e6766..e54942ecf0 100644 --- a/src/models/moderation.d.ts +++ b/src/models/moderation.d.ts @@ -16,6 +16,9 @@ */ declare namespace rest_api { + import { Vote } from "@/lib/report_util"; + import { ReportedConversation } from "@/components/Report"; + namespace moderation { // `/moderation/annul` endpoint interface AnnulList { @@ -50,5 +53,48 @@ declare namespace rest_api { }; note?: string; } + + export interface ReportDetail { + // It's the full information we get from Django when we ask for a report by id + id: number; + created: string; + updated: string; + state: string; + escalated: boolean; + escalated_at: string; + retyped: boolean; + source: string; + report_type: ReportType; + reporting_user: PlayerCacheEntry; + reported_user: PlayerCacheEntry; + reported_game: number; + reported_game_move?: number; + reported_review: number; + reported_conversation: ReportedConversation; + url: string; + moderator: PlayerCacheEntry; + cleared_by_user: boolean; + was_helpful: boolean; + reporter_note: string; + reporter_note_translation: { + source_language: string; + target_language: string; + source_text: string; + target_text: string; + }; + moderator_note: string; + system_note: string; + detected_ai_games: Array; + + automod_to_moderator?: string; // Suggestions from "automod" + automod_to_reporter?: string; + automod_to_reported?: string; + + available_actions: Array; // community moderator actions + vote_counts: { [action: string]: number }; + voters: Vote[]; // votes from community moderators on this report + escalation_note: string; + dissenter_note: string; + } } } diff --git a/src/views/ReportsCenter/ModerationActionSelector.tsx b/src/views/ReportsCenter/ModerationActionSelector.tsx index d59a5ec14b..bd0a690a27 100644 --- a/src/views/ReportsCenter/ModerationActionSelector.tsx +++ b/src/views/ReportsCenter/ModerationActionSelector.tsx @@ -19,8 +19,8 @@ import * as React from "react"; import { _, llm_pgettext } from "@/lib/translate"; import * as DynamicHelp from "react-dynamic-help"; -import { useUser } from "@/lib/hooks"; -import { Report } from "@/lib/report_util"; + +type Report = rest_api.moderation.ReportDetail; interface ModerationActionSelectorProps { available_actions: string[]; @@ -368,9 +368,6 @@ export function ModerationActionSelector({ report, submit, }: ModerationActionSelectorProps): React.ReactElement { - const user = useUser(); - const reportedBySelf = user.id === report.reporting_user.id; - const [voted, setVoted] = React.useState(false); const [selectedOption, setSelectedOption] = React.useState(users_vote || ""); const [escalation_note, setEscalationNote] = React.useState(""); @@ -467,14 +464,6 @@ export function ModerationActionSelector({ /> )} - {((reportedBySelf && enable) || null) && ( - - )} {((action_choices && enable) || null) && (