mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Align moderation reports with NIP-56
This commit is contained in:
1 parent
83279c7953
commit
53540a205d
5 files changed
+213
-24
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
Reference in new issue
Block a user