Add community content reports, map bans, and appeals

This commit is contained in:
2026-10-06 13:00:10 +05:00
parent 5f43e97d32
commit dd67b5c0de
18 changed files with 1167 additions and 50 deletions

View File

@@ -6,6 +6,35 @@ export type Json =
| { [key: string]: Json | undefined }
| Json[];
type MapModerationFields = {
report_strikes: number;
report_cycle: number;
banned_at: string | null;
ban_reason: string | null;
};
type ReportModerationFields = {
category: string;
revision_id: string | null;
panorama_id: string | null;
evidence_url: string | null;
report_cycle: number;
reviewer_id: string | null;
reviewed_at: string | null;
review_reason: string | null;
};
type AppealRecord = {
id: string;
dataset_id: string;
owner_id: string;
report_cycle: number;
reason: string;
state: string;
created_at: string;
reviewer_id: string | null;
reviewed_at: string | null;
review_reason: string | null;
};
export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
@@ -14,6 +43,16 @@ export type Database = {
};
public: {
Tables: {
map_appeals: {
Row: AppealRecord;
Insert: Partial<AppealRecord> &
Pick<
AppealRecord,
"dataset_id" | "owner_id" | "report_cycle" | "reason"
>;
Update: Partial<AppealRecord>;
Relationships: [];
};
catalog_servers: {
Row: {
address: string | null;
@@ -89,7 +128,7 @@ export type Database = {
];
};
datasets: {
Row: {
Row: MapModerationFields & {
allow_contributions: boolean;
bounds: Json;
contribution_count: number;
@@ -112,7 +151,7 @@ export type Database = {
update_state: string | null;
updated_at: string;
};
Insert: {
Insert: Partial<MapModerationFields> & {
allow_contributions?: boolean;
bounds: Json;
contribution_count?: number;
@@ -135,7 +174,7 @@ export type Database = {
update_state?: string | null;
updated_at?: string;
};
Update: {
Update: Partial<MapModerationFields> & {
allow_contributions?: boolean;
bounds?: Json;
contribution_count?: number;
@@ -322,7 +361,7 @@ export type Database = {
Relationships: [];
};
reports: {
Row: {
Row: ReportModerationFields & {
created_at: string;
dataset_id: string;
id: string;
@@ -330,7 +369,7 @@ export type Database = {
reason: string;
state: string;
};
Insert: {
Insert: Partial<ReportModerationFields> & {
created_at?: string;
dataset_id: string;
id?: string;
@@ -338,7 +377,7 @@ export type Database = {
reason: string;
state?: string;
};
Update: {
Update: Partial<ReportModerationFields> & {
created_at?: string;
dataset_id?: string;
id?: string;
@@ -481,6 +520,40 @@ export type Database = {
[_ in never]: never;
};
Functions: {
submit_content_report: {
Args: {
p_dataset: string;
p_owner: string;
p_revision: string;
p_panorama: string | null;
p_category: string;
p_reason: string;
p_evidence?: string | null;
};
Returns: string;
};
review_content_report: {
Args: {
p_report: string;
p_reviewer: string;
p_decision: string;
p_reason: string;
};
Returns: undefined;
};
appeal_map_ban: {
Args: { p_dataset: string; p_owner: string; p_reason: string };
Returns: string;
};
review_map_appeal: {
Args: {
p_appeal: string;
p_reviewer: string;
p_decision: string;
p_reason: string;
};
Returns: undefined;
};
refresh_map: {
Args: { p_dataset: string; p_owner: string; p_source: string };
Returns: undefined;

52
shared/moderation.ts Normal file
View File

@@ -0,0 +1,52 @@
import { z } from "zod";
export const reportCategories = {
inappropriate: "Inappropriate or offensive content",
privacy: "Personal or private information",
wrong_world: "Wrong world or misleading map",
broken: "Broken panorama or map",
spam: "Spam or advertising",
other: "Other",
} as const;
export const reportSchema = z
.object({
datasetId: z.string().uuid().optional(),
sessionId: z.string().uuid().optional(),
round: z.number().int().min(0).max(4).optional(),
target: z.enum(["map", "panorama"]),
category: z.enum(
Object.keys(reportCategories) as [
keyof typeof reportCategories,
...Array<keyof typeof reportCategories>,
],
),
reason: z.string().trim().min(10).max(2000),
evidenceUrl: z
.string()
.trim()
.url()
.max(1000)
.refine(
(url) => /^https?:\/\//i.test(url),
"Use an HTTP or HTTPS evidence link",
)
.optional(),
})
.superRefine((report, context) => {
if (Boolean(report.datasetId) === Boolean(report.sessionId))
context.addIssue({
code: "custom",
message: "Choose a map or a game session",
});
if (
report.target === "panorama" &&
(!report.sessionId || report.round === undefined)
)
context.addIssue({
code: "custom",
message: "Choose a panorama from your game",
});
});
export const contributionCutoff = (now = Date.now()) =>
new Date(now - 5 * 24 * 60 * 60 * 1000).toISOString();