mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Seed trusted moderation contacts
This commit is contained in:
1 parent
8775aceda7
commit
123431ea7f
5 files changed
+381
-37
No files matched your search
@@ -32,6 +32,7 @@ Thread new moderation behaviors through the same service → stage → app → U
|
||||
- Disable autoplay preview if **≥ 2** F1 friends report `nudity`.
|
||||
- Downrank author when any F1 has them in mute list (10000).
|
||||
- Opt-in admin lists (30000 with `d=bitvid:admin:*`) can hard-hide content.
|
||||
- Trust seeds from `DEFAULT_TRUST_SEED_NPUBS` count as baseline F1 contacts (toggle via `FEATURE_TRUST_SEEDS`).
|
||||
|
||||
> You can override all defaults in **Settings → Safety & Moderation**.
|
||||
|
||||
|
||||
Vendored
+17
@@ -1,10 +1,27 @@
|
||||
// js/bootstrap.js
|
||||
|
||||
import Application from "./app.js";
|
||||
import {
|
||||
DEFAULT_TRUST_SEED_NPUBS,
|
||||
FEATURE_TRUST_SEEDS,
|
||||
} from "./constants.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import r2Service from "./services/r2Service.js";
|
||||
import { loadView } from "./viewManager.js";
|
||||
|
||||
if (
|
||||
FEATURE_TRUST_SEEDS &&
|
||||
moderationService &&
|
||||
typeof moderationService.setTrustedSeeds === "function"
|
||||
) {
|
||||
try {
|
||||
moderationService.setTrustedSeeds(DEFAULT_TRUST_SEED_NPUBS);
|
||||
} catch {
|
||||
// Swallow bootstrap errors so login flow can continue; flag enables quick rollback.
|
||||
}
|
||||
}
|
||||
|
||||
function mergeServices(overrides = {}) {
|
||||
const merged = { nostrService, r2Service };
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export const DEFAULT_TRUST_SEED_NPUBS = Object.freeze([
|
||||
// Rollout: trust seeds count as F1 contacts. Rollback: flip FEATURE_TRUST_SEEDS to false.
|
||||
"npub1424242424242424242424242424242424242424242424242424qamrcaj",
|
||||
"npub1hwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwasxw04hu",
|
||||
]);
|
||||
|
||||
const DEFAULT_FLAGS = Object.freeze({
|
||||
URL_FIRST_ENABLED: true, // try URL before magnet in the player
|
||||
ACCEPT_LEGACY_V1: true, // accept v1 magnet-only notes
|
||||
VIEW_FILTER_INCLUDE_LEGACY_VIDEO: false,
|
||||
FEATURE_WATCH_HISTORY_V2: true,
|
||||
FEATURE_PUBLISH_NIP71: false,
|
||||
FEATURE_TRUST_SEEDS: true, // Rollback: disable to drop baseline trust seeds without code changes.
|
||||
WSS_TRACKERS: Object.freeze([
|
||||
"wss://tracker.openwebtorrent.com",
|
||||
"wss://tracker.fastcast.nz",
|
||||
@@ -25,6 +32,7 @@ const runtimeFlags = (() => {
|
||||
DEFAULT_FLAGS.VIEW_FILTER_INCLUDE_LEGACY_VIDEO,
|
||||
FEATURE_WATCH_HISTORY_V2: DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2,
|
||||
FEATURE_PUBLISH_NIP71: DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71,
|
||||
FEATURE_TRUST_SEEDS: DEFAULT_FLAGS.FEATURE_TRUST_SEEDS,
|
||||
WSS_TRACKERS: [...DEFAULT_FLAGS.WSS_TRACKERS],
|
||||
};
|
||||
if (globalScope) {
|
||||
@@ -115,6 +123,11 @@ export let FEATURE_PUBLISH_NIP71 = coerceBoolean(
|
||||
DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71
|
||||
);
|
||||
|
||||
export let FEATURE_TRUST_SEEDS = coerceBoolean(
|
||||
runtimeFlags.FEATURE_TRUST_SEEDS,
|
||||
DEFAULT_FLAGS.FEATURE_TRUST_SEEDS
|
||||
);
|
||||
|
||||
export let WSS_TRACKERS = freezeTrackers(
|
||||
sanitizeTrackerList(runtimeFlags.WSS_TRACKERS)
|
||||
);
|
||||
@@ -183,6 +196,20 @@ Object.defineProperty(runtimeFlags, "FEATURE_PUBLISH_NIP71", {
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(runtimeFlags, "FEATURE_TRUST_SEEDS", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return FEATURE_TRUST_SEEDS;
|
||||
},
|
||||
set(next) {
|
||||
FEATURE_TRUST_SEEDS = coerceBoolean(
|
||||
next,
|
||||
DEFAULT_FLAGS.FEATURE_TRUST_SEEDS
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(runtimeFlags, "WSS_TRACKERS", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
@@ -200,6 +227,7 @@ runtimeFlags.ACCEPT_LEGACY_V1 = ACCEPT_LEGACY_V1;
|
||||
runtimeFlags.VIEW_FILTER_INCLUDE_LEGACY_VIDEO = VIEW_FILTER_INCLUDE_LEGACY_VIDEO;
|
||||
runtimeFlags.FEATURE_WATCH_HISTORY_V2 = FEATURE_WATCH_HISTORY_V2;
|
||||
runtimeFlags.FEATURE_PUBLISH_NIP71 = FEATURE_PUBLISH_NIP71;
|
||||
runtimeFlags.FEATURE_TRUST_SEEDS = FEATURE_TRUST_SEEDS;
|
||||
runtimeFlags.WSS_TRACKERS = WSS_TRACKERS;
|
||||
|
||||
export function setUrlFirstEnabled(next) {
|
||||
@@ -226,6 +254,11 @@ export function setWssTrackers(next) {
|
||||
return WSS_TRACKERS;
|
||||
}
|
||||
|
||||
export function setTrustSeedsEnabled(next) {
|
||||
runtimeFlags.FEATURE_TRUST_SEEDS = next;
|
||||
return FEATURE_TRUST_SEEDS;
|
||||
}
|
||||
|
||||
export function resetRuntimeFlags() {
|
||||
setUrlFirstEnabled(DEFAULT_FLAGS.URL_FIRST_ENABLED);
|
||||
setAcceptLegacyV1(DEFAULT_FLAGS.ACCEPT_LEGACY_V1);
|
||||
@@ -233,6 +266,7 @@ export function resetRuntimeFlags() {
|
||||
DEFAULT_FLAGS.VIEW_FILTER_INCLUDE_LEGACY_VIDEO
|
||||
);
|
||||
setWatchHistoryV2Enabled(DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2);
|
||||
setTrustSeedsEnabled(DEFAULT_FLAGS.FEATURE_TRUST_SEEDS);
|
||||
setWssTrackers(DEFAULT_FLAGS.WSS_TRACKERS);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,133 @@ function bytesToHex(bytes) {
|
||||
return hex;
|
||||
}
|
||||
|
||||
const BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
const BECH32_CHARSET_MAP = (() => {
|
||||
const map = new Map();
|
||||
for (let index = 0; index < BECH32_CHARSET.length; index += 1) {
|
||||
map.set(BECH32_CHARSET[index], index);
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
function bech32Polymod(values) {
|
||||
let chk = 1;
|
||||
const generators = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
||||
|
||||
for (const value of values) {
|
||||
const top = chk >>> 25;
|
||||
chk = ((chk & 0x1ffffff) << 5) ^ value;
|
||||
for (let bit = 0; bit < generators.length; bit += 1) {
|
||||
if ((top >>> bit) & 1) {
|
||||
chk ^= generators[bit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chk;
|
||||
}
|
||||
|
||||
function bech32HrpExpand(hrp) {
|
||||
const expanded = [];
|
||||
for (let index = 0; index < hrp.length; index += 1) {
|
||||
const code = hrp.charCodeAt(index);
|
||||
expanded.push(code >>> 5);
|
||||
}
|
||||
expanded.push(0);
|
||||
for (let index = 0; index < hrp.length; index += 1) {
|
||||
const code = hrp.charCodeAt(index);
|
||||
expanded.push(code & 31);
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
|
||||
function bech32VerifyChecksum(hrp, data) {
|
||||
return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 1;
|
||||
}
|
||||
|
||||
function bech32Decode(value) {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
const upper = trimmed.toUpperCase();
|
||||
if (trimmed !== lower && trimmed !== upper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = lower;
|
||||
const separatorIndex = normalized.lastIndexOf("1");
|
||||
if (separatorIndex < 1 || separatorIndex + 7 > normalized.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hrp = normalized.slice(0, separatorIndex);
|
||||
const data = [];
|
||||
for (let index = separatorIndex + 1; index < normalized.length; index += 1) {
|
||||
const char = normalized[index];
|
||||
if (!BECH32_CHARSET_MAP.has(char)) {
|
||||
return null;
|
||||
}
|
||||
data.push(BECH32_CHARSET_MAP.get(char));
|
||||
}
|
||||
|
||||
if (!bech32VerifyChecksum(hrp, data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { hrp, words: data.slice(0, -6) };
|
||||
}
|
||||
|
||||
function convertBits(data, fromBits, toBits, pad = true) {
|
||||
const result = [];
|
||||
let acc = 0;
|
||||
let bits = 0;
|
||||
const maxv = (1 << toBits) - 1;
|
||||
const maxAcc = (1 << (fromBits + toBits - 1)) - 1;
|
||||
|
||||
for (const value of data) {
|
||||
if (value < 0 || value >> fromBits) {
|
||||
return null;
|
||||
}
|
||||
acc = ((acc << fromBits) | value) & maxAcc;
|
||||
bits += fromBits;
|
||||
while (bits >= toBits) {
|
||||
bits -= toBits;
|
||||
result.push((acc >>> bits) & maxv);
|
||||
}
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits) {
|
||||
result.push((acc << (toBits - bits)) & maxv);
|
||||
}
|
||||
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function fallbackDecodeNpubToHex(value) {
|
||||
const decoded = bech32Decode(value);
|
||||
if (!decoded || decoded.hrp !== "npub") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const bytes = convertBits(decoded.words, 5, 8, false);
|
||||
if (!bytes || !bytes.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function decodeToHex(candidate) {
|
||||
if (typeof candidate !== "string") {
|
||||
return "";
|
||||
@@ -171,20 +298,21 @@ function decodeToHex(candidate) {
|
||||
try {
|
||||
const tools = getNostrTools();
|
||||
const decoder = tools?.nip19?.decode;
|
||||
if (typeof decoder !== "function") {
|
||||
return "";
|
||||
if (typeof decoder === "function") {
|
||||
const decoded = decoder(trimmed);
|
||||
if (!decoded || decoded.type !== "npub") {
|
||||
return fallbackDecodeNpubToHex(trimmed);
|
||||
}
|
||||
const data = decoded.data;
|
||||
if (typeof data === "string") {
|
||||
const normalized = normalizeHex(data);
|
||||
return normalized || fallbackDecodeNpubToHex(trimmed);
|
||||
}
|
||||
return bytesToHex(data);
|
||||
}
|
||||
const decoded = decoder(trimmed);
|
||||
if (!decoded || decoded.type !== "npub") {
|
||||
return "";
|
||||
}
|
||||
const data = decoded.data;
|
||||
if (typeof data === "string") {
|
||||
return normalizeHex(data);
|
||||
}
|
||||
return bytesToHex(data);
|
||||
return fallbackDecodeNpubToHex(trimmed);
|
||||
} catch (error) {
|
||||
return "";
|
||||
return fallbackDecodeNpubToHex(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +460,8 @@ export class ModerationService {
|
||||
|
||||
this.viewerPubkey = "";
|
||||
this.trustedContacts = new Set();
|
||||
this.trustedSeedContacts = new Set();
|
||||
this.viewerContacts = new Set();
|
||||
this.userBlocks = null;
|
||||
this.accessControl = accessControlService;
|
||||
|
||||
@@ -370,6 +500,99 @@ export class ModerationService {
|
||||
this.userLogger = normalizeUserLogger(channel);
|
||||
}
|
||||
|
||||
mergeSeedsIntoSet(base = null) {
|
||||
const merged = new Set();
|
||||
|
||||
if (base instanceof Set || Array.isArray(base)) {
|
||||
for (const value of base) {
|
||||
const normalized = normalizeHex(value);
|
||||
if (normalized) {
|
||||
merged.add(normalized);
|
||||
}
|
||||
}
|
||||
} else if (base && typeof base[Symbol.iterator] === "function") {
|
||||
for (const value of base) {
|
||||
const normalized = normalizeHex(value);
|
||||
if (normalized) {
|
||||
merged.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.trustedSeedContacts instanceof Set) {
|
||||
for (const seed of this.trustedSeedContacts) {
|
||||
const normalized = normalizeHex(seed);
|
||||
if (normalized) {
|
||||
merged.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
rebuildTrustedContacts(nextContacts = new Set(), { previous = null } = {}) {
|
||||
const sanitized = new Set();
|
||||
|
||||
if (nextContacts instanceof Set || Array.isArray(nextContacts)) {
|
||||
for (const value of nextContacts) {
|
||||
const normalized = normalizeHex(value);
|
||||
if (normalized) {
|
||||
sanitized.add(normalized);
|
||||
}
|
||||
}
|
||||
} else if (nextContacts && typeof nextContacts[Symbol.iterator] === "function") {
|
||||
for (const value of nextContacts) {
|
||||
const normalized = normalizeHex(value);
|
||||
if (normalized) {
|
||||
sanitized.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.viewerContacts = sanitized;
|
||||
|
||||
const previousContacts =
|
||||
previous instanceof Set
|
||||
? new Set(previous)
|
||||
: this.trustedContacts instanceof Set
|
||||
? new Set(this.trustedContacts)
|
||||
: new Set();
|
||||
|
||||
const seededNext = this.mergeSeedsIntoSet(sanitized);
|
||||
this.trustedContacts = seededNext;
|
||||
|
||||
this.emit("contacts", { size: seededNext.size });
|
||||
this.recomputeAllSummaries();
|
||||
this.reconcileTrustedMuteSubscriptions(previousContacts, seededNext);
|
||||
}
|
||||
|
||||
setTrustedSeeds(seeds = []) {
|
||||
const sanitizedSeeds = new Set();
|
||||
|
||||
if (seeds instanceof Set || Array.isArray(seeds)) {
|
||||
for (const candidate of seeds) {
|
||||
const normalized = normalizeToHex(candidate);
|
||||
if (normalized) {
|
||||
sanitizedSeeds.add(normalized);
|
||||
}
|
||||
}
|
||||
} else if (seeds && typeof seeds[Symbol.iterator] === "function") {
|
||||
for (const candidate of seeds) {
|
||||
const normalized = normalizeToHex(candidate);
|
||||
if (normalized) {
|
||||
sanitizedSeeds.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previousContacts =
|
||||
this.trustedContacts instanceof Set ? new Set(this.trustedContacts) : new Set();
|
||||
|
||||
this.trustedSeedContacts = sanitizedSeeds;
|
||||
this.rebuildTrustedContacts(this.viewerContacts, { previous: previousContacts });
|
||||
}
|
||||
|
||||
logThresholdTransitions({
|
||||
eventId = "",
|
||||
reportType = "",
|
||||
@@ -522,7 +745,14 @@ export class ModerationService {
|
||||
return this.userBlockRefreshQueue;
|
||||
}
|
||||
|
||||
clearTrustedMuteTracking() {
|
||||
clearTrustedMuteTracking({ previousContacts = null } = {}) {
|
||||
const previous =
|
||||
previousContacts instanceof Set
|
||||
? new Set(previousContacts)
|
||||
: this.trustedContacts instanceof Set
|
||||
? new Set(this.trustedContacts)
|
||||
: new Set();
|
||||
|
||||
for (const [pubkey, entry] of this.trustedMuteSubscriptions.entries()) {
|
||||
if (entry && typeof entry.unsub === "function") {
|
||||
try {
|
||||
@@ -548,6 +778,8 @@ export class ModerationService {
|
||||
this.viewerMuteUpdatedAt = 0;
|
||||
|
||||
this.emit("trusted-mutes", { total: 0 });
|
||||
|
||||
this.rebuildTrustedContacts(new Set(), { previous });
|
||||
}
|
||||
|
||||
isTrustedMuteOwner(pubkey) {
|
||||
@@ -1244,10 +1476,11 @@ export class ModerationService {
|
||||
return this.viewerPubkey;
|
||||
}
|
||||
|
||||
const previousContacts =
|
||||
this.trustedContacts instanceof Set ? new Set(this.trustedContacts) : new Set();
|
||||
|
||||
this.viewerPubkey = normalized;
|
||||
this.clearTrustedMuteTracking();
|
||||
this.trustedContacts.clear();
|
||||
this.recomputeAllSummaries();
|
||||
this.clearTrustedMuteTracking({ previousContacts });
|
||||
|
||||
if (this.contactSubscription && typeof this.contactSubscription.unsub === "function") {
|
||||
try {
|
||||
@@ -1292,9 +1525,7 @@ export class ModerationService {
|
||||
const previousContacts =
|
||||
this.trustedContacts instanceof Set ? new Set(this.trustedContacts) : new Set();
|
||||
if (!normalized) {
|
||||
this.clearTrustedMuteTracking();
|
||||
this.trustedContacts.clear();
|
||||
this.emit("contacts", { size: 0 });
|
||||
this.clearTrustedMuteTracking({ previousContacts });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1307,9 +1538,7 @@ export class ModerationService {
|
||||
|
||||
const relays = resolveRelayList(this.nostrClient);
|
||||
if (!relays.length) {
|
||||
this.clearTrustedMuteTracking();
|
||||
this.trustedContacts.clear();
|
||||
this.emit("contacts", { size: 0 });
|
||||
this.clearTrustedMuteTracking({ previousContacts });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1324,11 +1553,7 @@ export class ModerationService {
|
||||
}
|
||||
|
||||
if (!Array.isArray(events) || !events.length) {
|
||||
this.clearTrustedMuteTracking();
|
||||
this.trustedContacts.clear();
|
||||
this.emit("contacts", { size: 0 });
|
||||
this.recomputeAllSummaries();
|
||||
this.reconcileTrustedMuteSubscriptions(previousContacts, new Set());
|
||||
this.clearTrustedMuteTracking({ previousContacts });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1346,13 +1571,15 @@ export class ModerationService {
|
||||
}
|
||||
|
||||
applyContactEvent(event, { previous = null } = {}) {
|
||||
const previousContacts = previous instanceof Set ? previous : new Set(this.trustedContacts);
|
||||
const previousContacts =
|
||||
previous instanceof Set
|
||||
? new Set(previous)
|
||||
: this.trustedContacts instanceof Set
|
||||
? new Set(this.trustedContacts)
|
||||
: new Set();
|
||||
|
||||
if (!event || !Array.isArray(event.tags)) {
|
||||
this.clearTrustedMuteTracking();
|
||||
this.trustedContacts.clear();
|
||||
this.emit("contacts", { size: 0 });
|
||||
this.recomputeAllSummaries();
|
||||
this.reconcileTrustedMuteSubscriptions(previousContacts, new Set());
|
||||
this.clearTrustedMuteTracking({ previousContacts });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1367,10 +1594,7 @@ export class ModerationService {
|
||||
}
|
||||
}
|
||||
|
||||
this.trustedContacts = nextSet;
|
||||
this.emit("contacts", { size: nextSet.size });
|
||||
this.recomputeAllSummaries();
|
||||
this.reconcileTrustedMuteSubscriptions(previousContacts, nextSet);
|
||||
this.rebuildTrustedContacts(nextSet, { previous: previousContacts });
|
||||
}
|
||||
|
||||
async subscribeToContactList(pubkey) {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { DEFAULT_TRUST_SEED_NPUBS } from "../../js/constants.js";
|
||||
import {
|
||||
withMockedNostrTools,
|
||||
createModerationServiceHarness,
|
||||
createReportEvent,
|
||||
} from "../helpers/moderation-test-helpers.mjs";
|
||||
|
||||
const [PRIMARY_SEED_NPUB] = DEFAULT_TRUST_SEED_NPUBS;
|
||||
|
||||
function createTrustedMuteEvent({ owner, muted, id = "m".repeat(64), createdAt = 1_700_000_000 } = {}) {
|
||||
return {
|
||||
kind: 10000,
|
||||
id,
|
||||
pubkey: owner,
|
||||
created_at: createdAt,
|
||||
tags: [["p", muted]],
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
test("trusted seeds contribute to trusted mute counts", (t) => {
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const mutedAuthor = "c".repeat(64);
|
||||
|
||||
const { service } = createModerationServiceHarness(t);
|
||||
service.setTrustedSeeds([PRIMARY_SEED_NPUB]);
|
||||
|
||||
const [seedHex] = Array.from(service.trustedContacts);
|
||||
assert.ok(seedHex, "trusted seeds should populate trustedContacts");
|
||||
|
||||
const muteEvent = createTrustedMuteEvent({ owner: seedHex, muted: mutedAuthor });
|
||||
service.applyTrustedMuteEvent(seedHex, muteEvent);
|
||||
|
||||
assert.equal(service.isTrustedMuteOwner(seedHex), true);
|
||||
assert.equal(service.isAuthorMutedByTrusted(mutedAuthor), true);
|
||||
assert.deepEqual(service.getTrustedMutersForAuthor(mutedAuthor), [seedHex]);
|
||||
});
|
||||
|
||||
test("trusted seeds contribute to trusted report counts", (t) => {
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const eventId = "f".repeat(64);
|
||||
|
||||
const { service } = createModerationServiceHarness(t);
|
||||
service.setTrustedSeeds([PRIMARY_SEED_NPUB]);
|
||||
|
||||
const [seedHex] = Array.from(service.trustedContacts);
|
||||
assert.ok(seedHex, "trusted seeds should populate trustedContacts");
|
||||
|
||||
const report = createReportEvent({
|
||||
id: "r".repeat(64),
|
||||
reporter: seedHex,
|
||||
eventId,
|
||||
createdAt: 1_700_000_111,
|
||||
type: "nudity",
|
||||
});
|
||||
|
||||
service.ingestReportEvent(report);
|
||||
|
||||
assert.equal(service.trustedReportCount(eventId, "nudity"), 1);
|
||||
const summary = service.getTrustedReportSummary(eventId);
|
||||
assert.equal(summary?.totalTrusted ?? 0, 1);
|
||||
const reporters = service.getTrustedReporters(eventId, "nudity");
|
||||
assert.deepEqual(reporters.map((entry) => entry.pubkey), [seedHex]);
|
||||
});
|
||||
Reference in new issue
Block a user