Align moderation reports with NIP-56

This commit is contained in:
thePR0M3TH3AN committed 2025-10-29 21:07:00 -04:00
1 parent 83279c7953
commit 53540a205d
5 files changed
+213 -24

No files matched your search

+25 -8
View File
@@ -2095,7 +2095,13 @@ export class ModerationService {
return Number.isFinite(entry.trusted) ? entry.trusted : 0;
}
async submitReport({ eventId, type, targetPubkey = "", content = "" } = {}) {
async submitReport({
eventId,
type,
targetPubkey = "",
relayHint = "",
content = "",
} = {}) {
const normalizedEventId = normalizeEventId(eventId);
if (!normalizedEventId) {
const error = new Error("invalid-event-id");
@@ -2117,6 +2123,13 @@ export class ModerationService {
throw error;
}
const normalizedTarget = normalizeHex(targetPubkey);
if (!normalizedTarget) {
const error = new Error("invalid-target-pubkey");
error.code = "invalid-target-pubkey";
throw error;
}
const extension = typeof window !== "undefined" ? window.nostr : null;
if (!extension) {
const error = new Error("nostr-extension-missing");
@@ -2137,17 +2150,21 @@ export class ModerationService {
}
}
const sanitizedRelayHint =
typeof relayHint === "string" ? relayHint.trim() : "";
const eventTag = ["e", normalizedEventId];
if (sanitizedRelayHint) {
eventTag.push(sanitizedRelayHint);
}
eventTag.push(normalizedType);
const tags = [
["e", normalizedEventId, "", normalizedType],
["report", normalizedType],
eventTag,
["p", normalizedTarget, normalizedType],
["t", normalizedType],
];
const normalizedTarget = normalizeHex(targetPubkey);
if (normalizedTarget) {
tags.push(["p", normalizedTarget]);
}
const event = {
kind: 1984,
created_at: Math.floor(Date.now() / 1000),
+5 -1
View File
@@ -288,7 +288,11 @@ export function createVideoMoreMenuPanel({
appendMenuAction(doc, list, {
text: "Report",
action: "report",
dataset: { eventId: metadata.id },
dataset: {
eventId: metadata.id,
author: metadata.pubkey,
pointerRelay: pointer && pointer.relay ? pointer.relay : "",
},
});
return panel;
+70 -13
View File
@@ -348,7 +348,12 @@ export default class MoreMenuController {
return modal;
}
openReportModal({ eventId, authorPubkey = "", trigger = null } = {}) {
openReportModal({
eventId,
authorPubkey = "",
relayHint = "",
trigger = null,
} = {}) {
const modal = this.ensureReportModal();
if (!modal) {
this.callbacks.showError(
@@ -357,9 +362,17 @@ export default class MoreMenuController {
return false;
}
const normalizedEventId =
typeof eventId === "string" ? eventId.trim() : "";
const normalizedAuthor =
typeof authorPubkey === "string" ? authorPubkey.trim() : "";
const normalizedRelayHint =
typeof relayHint === "string" ? relayHint.trim() : "";
this.activeReportContext = {
eventId: typeof eventId === "string" ? eventId : "",
authorPubkey: typeof authorPubkey === "string" ? authorPubkey : "",
eventId: normalizedEventId,
authorPubkey: normalizedAuthor,
relayHint: normalizedRelayHint,
};
this.isReportSubmitting = false;
this.setReportModalBusy(false);
@@ -496,6 +509,15 @@ export default class MoreMenuController {
return;
}
const targetPubkey =
typeof context.authorPubkey === "string"
? context.authorPubkey.trim()
: "";
if (!targetPubkey) {
this.showReportError("Unable to determine which creator to report.");
return;
}
const viewerPubkey = this.callbacks.getCurrentUserPubkey?.();
if (!viewerPubkey) {
this.closeReportModal();
@@ -518,7 +540,11 @@ export default class MoreMenuController {
await service.submitReport({
eventId,
type: category,
targetPubkey: context.authorPubkey || "",
targetPubkey,
relayHint:
typeof context.relayHint === "string"
? context.relayHint.trim()
: "",
});
this.callbacks.showSuccess(
@@ -537,6 +563,8 @@ export default class MoreMenuController {
message = "Connect a Nostr account before reporting videos.";
} else if (code === "service-unavailable") {
message = "Reporting is unavailable right now. Please try again later.";
} else if (code === "invalid-target-pubkey") {
message = "Unable to determine which creator to report.";
}
this.showReportError(message);
@@ -1505,11 +1533,14 @@ export default class MoreMenuController {
}
const eventId =
dataset.eventId ||
(context === "modal" ? currentVideo?.id : "") ||
currentVideo?.id || "";
(typeof dataset.eventId === "string" ? dataset.eventId : "") ||
(context === "modal" && typeof currentVideo?.id === "string"
? currentVideo.id
: "") ||
(typeof currentVideo?.id === "string" ? currentVideo.id : "");
if (!eventId) {
const normalizedEventId = eventId ? eventId.trim() : "";
if (!normalizedEventId) {
this.callbacks.showError(
"Unable to determine which video to report.",
);
@@ -1517,15 +1548,41 @@ export default class MoreMenuController {
}
const authorPubkey =
dataset.author ||
(context === "modal" && currentVideo?.pubkey
(typeof dataset.author === "string" ? dataset.author : "") ||
(context === "modal" && typeof currentVideo?.pubkey === "string"
? currentVideo.pubkey
: "") ||
currentVideo?.pubkey || "";
(typeof currentVideo?.pubkey === "string" ? currentVideo.pubkey : "");
const normalizedAuthor = authorPubkey ? authorPubkey.trim() : "";
if (!normalizedAuthor) {
this.callbacks.showError(
"Unable to determine which creator to report.",
);
break;
}
let relayHint =
(typeof dataset.pointerRelay === "string"
? dataset.pointerRelay
: "") || "";
if (relayHint) {
relayHint = relayHint.trim();
}
if (!relayHint && context === "modal") {
const pointer = Array.isArray(currentVideo?.pointer)
? currentVideo.pointer
: null;
const hintCandidate = pointer && pointer.length > 2 ? pointer[2] : "";
if (typeof hintCandidate === "string" && hintCandidate.trim()) {
relayHint = hintCandidate.trim();
}
}
this.openReportModal({
eventId,
authorPubkey,
eventId: normalizedEventId,
authorPubkey: normalizedAuthor,
relayHint,
trigger: dataset.triggerElement || null,
});
break;
+18 -2
View File
@@ -3,6 +3,7 @@ import { ModerationService } from "../../js/services/moderationService.js";
import { getDefaultModerationSettings } from "../../js/state/cache.js";
const DEFAULT_CONTACT_OWNER = "f".repeat(64);
const DEFAULT_REPORT_TARGET = "e".repeat(64);
export function withMockedNostrTools(t) {
const hadWindow = typeof globalThis.window !== "undefined";
@@ -99,6 +100,8 @@ export function createReportEvent({
id,
reporter,
eventId,
targetPubkey = DEFAULT_REPORT_TARGET,
relayHint = "",
createdAt = Math.floor(Date.now() / 1000),
type = "nudity",
} = {}) {
@@ -108,7 +111,19 @@ export function createReportEvent({
: `${Math.random().toString(16).slice(2)}${"0".repeat(64)}`.slice(0, 64);
const reporterPubkey = typeof reporter === "string" && reporter ? reporter : "";
const targetEventId = typeof eventId === "string" && eventId ? eventId : "";
const targetAuthor =
typeof targetPubkey === "string" && targetPubkey
? targetPubkey
: DEFAULT_REPORT_TARGET;
const normalizedType = typeof type === "string" && type ? type : "nudity";
const normalizedRelayHint =
typeof relayHint === "string" && relayHint ? relayHint : "";
const eventTag = ["e", targetEventId];
if (normalizedRelayHint) {
eventTag.push(normalizedRelayHint);
}
eventTag.push(normalizedType);
return {
kind: 1984,
@@ -116,8 +131,9 @@ export function createReportEvent({
pubkey: reporterPubkey,
created_at: Number.isFinite(createdAt) ? Math.floor(createdAt) : Math.floor(Date.now() / 1000),
tags: [
["e", targetEventId],
["report", normalizedType],
eventTag,
["p", targetAuthor, normalizedType],
["t", normalizedType],
],
content: "fixture report",
};
+95
View File
@@ -0,0 +1,95 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ModerationService } from "../../js/services/moderationService.js";
import { withMockedNostrTools } from "../helpers/moderation-test-helpers.mjs";
function createRelayPublishingPool(publishCalls) {
return {
publish(urls, event) {
publishCalls.push({ urls, event });
return {
on(eventName, handler) {
if (eventName === "ok") {
setTimeout(() => handler(), 0);
return true;
}
return false;
},
};
},
};
}
test("submitReport emits NIP-56 compliant report tags", async (t) => {
withMockedNostrTools(t);
const reporterPubkey = "a".repeat(64);
const targetPubkey = "b".repeat(64);
const eventId = "c".repeat(64);
const relayHint = "wss://relay.example";
const extension = {
async signEvent(event) {
return { ...event, id: "signed-event", sig: "sig" };
},
};
const previousExtension = globalThis.window.nostr;
globalThis.window.nostr = extension;
t.after(() => {
if (typeof previousExtension === "undefined") {
delete globalThis.window.nostr;
} else {
globalThis.window.nostr = previousExtension;
}
});
const publishCalls = [];
const nostrClient = {
pubkey: reporterPubkey,
relays: ["wss://relay.one"],
ensurePool: async () => {},
ensureExtensionPermissions: async () => ({ ok: true }),
pool: createRelayPublishingPool(publishCalls),
};
const service = new ModerationService({ nostrClient, logger: () => {} });
const { event, ok } = await service.submitReport({
eventId,
type: "malware",
targetPubkey,
relayHint,
});
assert.equal(ok, true, "submitReport should resolve with ok: true");
const eTag = event.tags.find((tag) => Array.isArray(tag) && tag[0] === "e");
assert.deepEqual(eTag, ["e", eventId, relayHint, "malware"], "e tag should include relay hint before type");
const pTag = event.tags.find((tag) => Array.isArray(tag) && tag[0] === "p");
assert.deepEqual(pTag, ["p", targetPubkey, "malware"], "p tag should include reported author and type");
assert.equal(
event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "report").length,
0,
"legacy report tag should be omitted",
);
const followupId = "d".repeat(64);
const { event: followup } = await service.submitReport({
eventId: followupId,
type: "spam",
targetPubkey,
});
const followupETag = followup.tags.find((tag) => Array.isArray(tag) && tag[0] === "e");
assert.deepEqual(
followupETag,
["e", followupId, "spam"],
"e tag without relay hint should collapse to type as third entry",
);
assert.ok(publishCalls.length >= 2, "submitReport should publish each report");
});