diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md index 7a6cd54a..8451aaeb 100644 --- a/docs/nostr-event-schemas.md +++ b/docs/nostr-event-schemas.md @@ -311,3 +311,27 @@ device, etc.) should remain on-device via the Refer to the [`WatchHistoryService`](../js/watchHistoryService.js) for queue management hooks and metadata toggle controls that complement these schema definitions.【F:js/watchHistoryService.js†L695-L776】【F:js/watchHistoryService.js†L1040-L1093】 + +## Validation + +bitvid now enforces schema validation at runtime during development. Every event +builder automatically runs the generated event against its definition in +`validateEventAgainstSchema` when `isDevMode` is true. Violations (missing +required tags, invalid JSON content, etc.) are logged to the console via +`devLogger`. + +### Continuous Integration + +A standalone validation suite runs in CI to ensure that all builders produce +valid events: + +```bash +# Run the validation script +node scripts/agent/validate-events.mjs + +# Run the unit test suite +npm run test:unit +``` + +When modifying schemas or builders, use `scripts/agent/validate-events.mjs` to +verify your changes locally. diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index 3b582e86..7d751998 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -870,13 +870,19 @@ export function buildVideoMirrorEvent(params) { } }); } - return { + const event = { kind: schema?.kind ?? 1063, pubkey, created_at, tags: combinedTags, content: typeof content === "string" ? content : String(content ?? ""), }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.VIDEO_MIRROR, event); + } + + return event; } export function buildRepostEvent(params) { @@ -986,13 +992,19 @@ export function buildRepostEvent(params) { } } - return { + const event = { kind: resolvedKind, pubkey, created_at, tags, content, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.REPOST, event); + } + + return event; } export function buildShareEvent(params) { @@ -1093,13 +1105,19 @@ export function buildShareEvent(params) { const resolvedContent = ensureValidUtf8Content(content); - return { + const event = { kind: schema?.kind ?? 1, pubkey, created_at, tags, content: resolvedContent, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.SHARE, event); + } + + return event; } export function buildRelayListEvent(params) { @@ -1164,13 +1182,19 @@ export function buildRelayListEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 10002, pubkey, created_at, tags, content: "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.RELAY_LIST, event); + } + + return event; } export function buildDmRelayListEvent(params) { @@ -1200,13 +1224,19 @@ export function buildDmRelayListEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 10050, pubkey, created_at, tags, content: "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.DM_RELAY_LIST, event); + } + + return event; } export function buildProfileMetadataEvent(params) { @@ -1236,13 +1266,19 @@ export function buildProfileMetadataEvent(params) { } } - return { + const event = { kind: schema?.kind ?? 0, pubkey, created_at, tags, content: serializedContent, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.PROFILE_METADATA, event); + } + + return event; } export function buildMuteListEvent(params) { @@ -1277,13 +1313,19 @@ export function buildMuteListEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 10000, pubkey, created_at, tags, content: typeof content === "string" ? content : "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.MUTE_LIST, event); + } + + return event; } export function buildDeletionEvent(params) { @@ -1322,13 +1364,19 @@ export function buildDeletionEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 5, pubkey, created_at, tags, content: typeof reason === "string" ? reason : "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.DELETION, event); + } + + return event; } export function buildLegacyDirectMessageEvent(params) { @@ -1353,13 +1401,19 @@ export function buildLegacyDirectMessageEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 4, pubkey, created_at, tags, content: typeof ciphertext === "string" ? ciphertext : "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.LEGACY_DM, event); + } + + return event; } export function buildDmAttachmentEvent(params) { @@ -1403,13 +1457,19 @@ export function buildDmAttachmentEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 15, pubkey, created_at, tags, content: "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.DM_ATTACHMENT, event); + } + + return event; } export function buildDmReadReceiptEvent(params) { @@ -1444,13 +1504,19 @@ export function buildDmReadReceiptEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 20001, pubkey, created_at, tags, content: "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.DM_READ_RECEIPT, event); + } + + return event; } export function buildDmTypingIndicatorEvent(params) { @@ -1489,13 +1555,19 @@ export function buildDmTypingIndicatorEvent(params) { tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice())); } - return { + const event = { kind: schema?.kind ?? 20002, pubkey, created_at, tags, content: "", }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.DM_TYPING, event); + } + + return event; } export function buildViewEvent(params) { @@ -1550,13 +1622,19 @@ export function buildViewEvent(params) { const resolvedContent = ensureValidUtf8Content(content); - return { + const event = { kind: schema?.kind ?? WATCH_HISTORY_KIND, pubkey, created_at, tags, content: resolvedContent, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.VIEW_EVENT, event); + } + + return event; } export function buildZapRequestEvent(params) { @@ -1624,13 +1702,19 @@ export function buildZapRequestEvent(params) { const resolvedContent = ensureValidUtf8Content(content); - return { + const event = { kind: schema?.kind ?? 9734, pubkey, created_at, tags, content: resolvedContent, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.ZAP_REQUEST, event); + } + + return event; } export function buildReactionEvent(params) { @@ -1756,13 +1840,19 @@ export function buildReactionEvent(params) { const resolvedContent = ensureValidUtf8Content(content); - return { + const event = { kind: schema?.kind ?? 7, pubkey, created_at, tags, content: resolvedContent, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.VIDEO_REACTION, event); + } + + return event; } export function buildCommentEvent(params) { @@ -2001,13 +2091,19 @@ export function buildCommentEvent(params) { const resolvedContent = ensureValidUtf8Content(content); - return { + const event = { kind: schema?.kind ?? 1111, pubkey, created_at, tags, content: resolvedContent, }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.VIDEO_COMMENT, event); + } + + return event; } export function buildWatchHistoryEvent(params) { @@ -2043,13 +2139,19 @@ export function buildWatchHistoryEvent(params) { appendSchemaTags(tags, schema); - return { + const event = { kind: schema?.kind ?? WATCH_HISTORY_KIND, pubkey, created_at: Number.isFinite(created_at) ? created_at : Math.floor(Date.now() / 1000), tags, content: typeof content === "string" ? content : String(content ?? ""), }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.WATCH_HISTORY, event); + } + + return event; } export function buildSubscriptionListEvent(params) { @@ -2072,13 +2174,20 @@ export function buildSubscriptionListEvent(params) { tags.push(["encrypted", normalizedEncryption]); } appendSchemaTags(tags, schema); - return { + + const event = { kind: schema?.kind ?? 30000, pubkey, created_at, tags, content: typeof content === "string" ? content : String(content ?? ""), }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.SUBSCRIPTION_LIST, event); + } + + return event; } export function buildBlockListEvent(params) { @@ -2101,13 +2210,20 @@ export function buildBlockListEvent(params) { tags.push(["encrypted", normalizedEncryption]); } appendSchemaTags(tags, schema); - return { + + const event = { kind: schema?.kind ?? 10000, pubkey, created_at, tags, content: typeof content === "string" ? content : String(content ?? ""), }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.USER_BLOCK_LIST, event); + } + + return event; } export function buildHashtagPreferenceEvent(params) { @@ -2124,13 +2240,20 @@ export function buildHashtagPreferenceEvent(params) { tags.push([identifierName, identifierValue]); } appendSchemaTags(tags, schema); - return { + + const event = { kind: schema?.kind ?? 30015, pubkey, created_at, tags, content: typeof content === "string" ? content : String(content ?? ""), }; + + if (isDevMode) { + validateEventAgainstSchema(NOTE_TYPES.HASHTAG_PREFERENCES, event); + } + + return event; } function resolveAdminNoteType(listKey) { @@ -2178,13 +2301,23 @@ export function buildAdminListEvent(listKey, params) { } }); appendSchemaTags(tags, schema); - return { + + const event = { kind: schema?.kind ?? 30000, pubkey, created_at, tags, content: "", }; + + if (isDevMode) { + const resolvedType = resolveAdminNoteType(listKey); + if (resolvedType) { + validateEventAgainstSchema(resolvedType, event); + } + } + + return event; } function hasTag(tags, tagName, tagValue = null) { diff --git a/scripts/agent/validate-events.mjs b/scripts/agent/validate-events.mjs new file mode 100644 index 00000000..64d59296 --- /dev/null +++ b/scripts/agent/validate-events.mjs @@ -0,0 +1,240 @@ +import { strict as assert } from 'node:assert'; + +// Hook console.warn to capture validation warnings +const originalWarn = console.warn; +const warnings = []; +console.warn = (...args) => { + // Capture only [schema] or [nostrEventSchemas] warnings + const msg = args.join(' '); + if (msg.includes('[schema]') || msg.includes('[nostrEventSchemas]')) { + warnings.push(msg); + } + // We can still print to stdout if needed, but for validation we want to capture + // originalWarn.apply(console, args); +}; + +// Import schemas AFTER hooking console +const schemas = await import('../../js/nostrEventSchemas.js'); +const { + validateEventAgainstSchema, + NOTE_TYPES, + buildVideoPostEvent, + buildVideoMirrorEvent, + buildRepostEvent, + buildShareEvent, + buildRelayListEvent, + buildDmRelayListEvent, + buildProfileMetadataEvent, + buildMuteListEvent, + buildDeletionEvent, + buildLegacyDirectMessageEvent, + buildDmAttachmentEvent, + buildDmReadReceiptEvent, + buildDmTypingIndicatorEvent, + buildViewEvent, + buildZapRequestEvent, + buildReactionEvent, + buildCommentEvent, + buildWatchHistoryEvent, + buildSubscriptionListEvent, + buildBlockListEvent, + buildHashtagPreferenceEvent, + buildAdminListEvent +} = schemas; + +console.log("Running Event Schema Validator..."); + +const failures = []; + +function check(name, builder, params, expectedType) { + warnings.length = 0; // Clear warnings + try { + const event = builder(params); + validateEventAgainstSchema(expectedType || event.kind, event); // Pass type if known, else relies on implementation or event.kind mapping + + // Some builders might not set the type explicitly in validate calls if we don't instrument them yet. + // So we manually call validateEventAgainstSchema with the expected NOTE_TYPE. + if (expectedType) { + validateEventAgainstSchema(expectedType, event); + } + + if (warnings.length > 0) { + failures.push({ name, error: `Generated warnings: ${JSON.stringify(warnings)}` }); + } + + // specific checks can go here + if (!event.created_at) failures.push({ name, error: "Missing created_at" }); + if (!event.pubkey) failures.push({ name, error: "Missing pubkey" }); + + } catch (e) { + failures.push({ name, error: `Threw exception: ${e.message}` }); + } +} + +const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; +const created_at = Math.floor(Date.now() / 1000); + +// Test cases +check("buildVideoPostEvent", buildVideoPostEvent, { + pubkey, + created_at, + dTagValue: "video-123", + content: { + version: 3, + title: "Test Video", + videoRootId: "root-123", + url: "https://example.com/video.mp4" + } +}, NOTE_TYPES.VIDEO_POST); + +check("buildVideoMirrorEvent", buildVideoMirrorEvent, { + pubkey, + created_at, + tags: [["t", "test"]], + content: "Mirror description" +}, NOTE_TYPES.VIDEO_MIRROR); + +check("buildRepostEvent", buildRepostEvent, { + pubkey, + created_at, + eventId: "1111111111111111111111111111111111111111111111111111111111111111", + eventRelay: "wss://relay.example.com", + serializedEvent: JSON.stringify({ id: "111", kind: 1 }) +}, NOTE_TYPES.REPOST); + +check("buildShareEvent", buildShareEvent, { + pubkey, + created_at, + content: "Check this out", + video: { id: "2222222222222222222222222222222222222222222222222222222222222222" } +}, NOTE_TYPES.SHARE); + +check("buildRelayListEvent", buildRelayListEvent, { + pubkey, + created_at, + relays: ["wss://relay.example.com", { url: "wss://read.example.com", mode: "read" }] +}, NOTE_TYPES.RELAY_LIST); + +check("buildDmRelayListEvent", buildDmRelayListEvent, { + pubkey, + created_at, + relays: ["wss://dm.example.com"] +}, NOTE_TYPES.DM_RELAY_LIST); + +check("buildProfileMetadataEvent", buildProfileMetadataEvent, { + pubkey, + created_at, + metadata: { name: "Test User", about: "Testing" } +}, NOTE_TYPES.PROFILE_METADATA); + +check("buildMuteListEvent", buildMuteListEvent, { + pubkey, + created_at, + pTags: ["3333333333333333333333333333333333333333333333333333333333333333"] +}, NOTE_TYPES.MUTE_LIST); + +check("buildDeletionEvent", buildDeletionEvent, { + pubkey, + created_at, + eventIds: ["4444444444444444444444444444444444444444444444444444444444444444"], + reason: "Mistake" +}, NOTE_TYPES.DELETION); + +check("buildLegacyDirectMessageEvent", buildLegacyDirectMessageEvent, { + pubkey, + created_at, + recipientPubkey: "5555555555555555555555555555555555555555555555555555555555555555", + ciphertext: "encryped_content" +}, NOTE_TYPES.LEGACY_DM); + +check("buildDmAttachmentEvent", buildDmAttachmentEvent, { + pubkey, + created_at, + recipientPubkey: "6666666666666666666666666666666666666666666666666666666666666666", + attachment: { url: "https://example.com/file.jpg", x: "hash" } +}, NOTE_TYPES.DM_ATTACHMENT); + +check("buildDmReadReceiptEvent", buildDmReadReceiptEvent, { + pubkey, + created_at, + recipientPubkey: "7777777777777777777777777777777777777777777777777777777777777777", + eventId: "8888888888888888888888888888888888888888888888888888888888888888" +}, NOTE_TYPES.DM_READ_RECEIPT); + +check("buildDmTypingIndicatorEvent", buildDmTypingIndicatorEvent, { + pubkey, + created_at, + recipientPubkey: "9999999999999999999999999999999999999999999999999999999999999999" +}, NOTE_TYPES.DM_TYPING); + +check("buildViewEvent", buildViewEvent, { + pubkey, + created_at, + pointerValue: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + pointerTag: ["e", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], + dedupeTag: "session-123" +}, NOTE_TYPES.VIEW_EVENT); + +check("buildZapRequestEvent", buildZapRequestEvent, { + pubkey, + created_at, + recipientPubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + amountSats: 100, + relays: ["wss://zap.relay.com"] +}, NOTE_TYPES.ZAP_REQUEST); + +check("buildReactionEvent", buildReactionEvent, { + pubkey, + created_at, + pointerTag: ["e", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"], + content: "+" +}, NOTE_TYPES.VIDEO_REACTION); + +check("buildCommentEvent", buildCommentEvent, { + pubkey, + created_at, + videoEventId: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + content: "Nice video!" +}, NOTE_TYPES.VIDEO_COMMENT); + +check("buildWatchHistoryEvent", buildWatchHistoryEvent, { + pubkey, + created_at, + monthIdentifier: "2023-10", + content: JSON.stringify({ version: 2, month: "2023-10", items: [] }) +}, NOTE_TYPES.WATCH_HISTORY); + +check("buildSubscriptionListEvent", buildSubscriptionListEvent, { + pubkey, + created_at, + content: "encrypted_subs" +}, NOTE_TYPES.SUBSCRIPTION_LIST); + +check("buildBlockListEvent", buildBlockListEvent, { + pubkey, + created_at, + content: "encrypted_blocks" +}, NOTE_TYPES.USER_BLOCK_LIST); + +check("buildHashtagPreferenceEvent", buildHashtagPreferenceEvent, { + pubkey, + created_at, + content: "encrypted_prefs" +}, NOTE_TYPES.HASHTAG_PREFERENCES); + +check("buildAdminListEvent (moderation)", (p) => buildAdminListEvent("moderation", p), { + pubkey, + created_at, + hexPubkeys: ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"] +}, NOTE_TYPES.ADMIN_MODERATION_LIST); + +if (failures.length > 0) { + console.error("\n❌ Validation Failed:"); + failures.forEach(f => { + console.error(`- ${f.name}: ${f.error}`); + }); + process.exit(1); +} else { + console.log("\n✅ All events validated successfully."); + process.exit(0); +} diff --git a/tests/event-schemas.test.mjs b/tests/event-schemas.test.mjs new file mode 100644 index 00000000..1f04a148 --- /dev/null +++ b/tests/event-schemas.test.mjs @@ -0,0 +1,225 @@ +import { describe, it, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { + validateEventAgainstSchema, + NOTE_TYPES, + buildVideoPostEvent, + buildVideoMirrorEvent, + buildRepostEvent, + buildShareEvent, + buildRelayListEvent, + buildDmRelayListEvent, + buildProfileMetadataEvent, + buildMuteListEvent, + buildDeletionEvent, + buildLegacyDirectMessageEvent, + buildDmAttachmentEvent, + buildDmReadReceiptEvent, + buildDmTypingIndicatorEvent, + buildViewEvent, + buildZapRequestEvent, + buildReactionEvent, + buildCommentEvent, + buildWatchHistoryEvent, + buildSubscriptionListEvent, + buildBlockListEvent, + buildHashtagPreferenceEvent, + buildAdminListEvent +} from '../js/nostrEventSchemas.js'; + +describe('Nostr Event Schemas', () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + const created_at = Math.floor(Date.now() / 1000); + let originalWarn; + let warnings = []; + + before(() => { + originalWarn = console.warn; + console.warn = (...args) => { + const msg = args.join(' '); + if (msg.includes('[schema]') || msg.includes('[nostrEventSchemas]')) { + warnings.push(msg); + } + // originalWarn.apply(console, args); + }; + }); + + after(() => { + console.warn = originalWarn; + }); + + function check(name, builder, params, expectedType) { + it(`should validate ${name}`, () => { + warnings.length = 0; + const event = builder(params); + + // Since we instrumented the builders, they run validation internally if IS_DEV_MODE is true. + // We also run it explicitly here to be sure. + validateEventAgainstSchema(expectedType || event.kind, event); + if (expectedType) { + validateEventAgainstSchema(expectedType, event); + } + + if (warnings.length > 0) { + assert.fail(`Validation warnings for ${name}: ${JSON.stringify(warnings)}`); + } + + assert.ok(event.created_at, "Missing created_at"); + assert.ok(event.pubkey, "Missing pubkey"); + assert.ok(Array.isArray(event.tags), "Tags should be an array"); + }); + } + + check("buildVideoPostEvent", buildVideoPostEvent, { + pubkey, + created_at, + dTagValue: "video-123", + content: { + version: 3, + title: "Test Video", + videoRootId: "root-123", + url: "https://example.com/video.mp4" + } + }, NOTE_TYPES.VIDEO_POST); + + check("buildVideoMirrorEvent", buildVideoMirrorEvent, { + pubkey, + created_at, + tags: [["t", "test"]], + content: "Mirror description" + }, NOTE_TYPES.VIDEO_MIRROR); + + check("buildRepostEvent", buildRepostEvent, { + pubkey, + created_at, + eventId: "1111111111111111111111111111111111111111111111111111111111111111", + eventRelay: "wss://relay.example.com", + serializedEvent: JSON.stringify({ id: "111", kind: 1 }) + }, NOTE_TYPES.REPOST); + + check("buildShareEvent", buildShareEvent, { + pubkey, + created_at, + content: "Check this out", + video: { id: "2222222222222222222222222222222222222222222222222222222222222222" } + }, NOTE_TYPES.SHARE); + + check("buildRelayListEvent", buildRelayListEvent, { + pubkey, + created_at, + relays: ["wss://relay.example.com", { url: "wss://read.example.com", mode: "read" }] + }, NOTE_TYPES.RELAY_LIST); + + check("buildDmRelayListEvent", buildDmRelayListEvent, { + pubkey, + created_at, + relays: ["wss://dm.example.com"] + }, NOTE_TYPES.DM_RELAY_LIST); + + check("buildProfileMetadataEvent", buildProfileMetadataEvent, { + pubkey, + created_at, + metadata: { name: "Test User", about: "Testing" } + }, NOTE_TYPES.PROFILE_METADATA); + + check("buildMuteListEvent", buildMuteListEvent, { + pubkey, + created_at, + pTags: ["3333333333333333333333333333333333333333333333333333333333333333"] + }, NOTE_TYPES.MUTE_LIST); + + check("buildDeletionEvent", buildDeletionEvent, { + pubkey, + created_at, + eventIds: ["4444444444444444444444444444444444444444444444444444444444444444"], + reason: "Mistake" + }, NOTE_TYPES.DELETION); + + check("buildLegacyDirectMessageEvent", buildLegacyDirectMessageEvent, { + pubkey, + created_at, + recipientPubkey: "5555555555555555555555555555555555555555555555555555555555555555", + ciphertext: "encryped_content" + }, NOTE_TYPES.LEGACY_DM); + + check("buildDmAttachmentEvent", buildDmAttachmentEvent, { + pubkey, + created_at, + recipientPubkey: "6666666666666666666666666666666666666666666666666666666666666666", + attachment: { url: "https://example.com/file.jpg", x: "hash" } + }, NOTE_TYPES.DM_ATTACHMENT); + + check("buildDmReadReceiptEvent", buildDmReadReceiptEvent, { + pubkey, + created_at, + recipientPubkey: "7777777777777777777777777777777777777777777777777777777777777777", + eventId: "8888888888888888888888888888888888888888888888888888888888888888" + }, NOTE_TYPES.DM_READ_RECEIPT); + + check("buildDmTypingIndicatorEvent", buildDmTypingIndicatorEvent, { + pubkey, + created_at, + recipientPubkey: "9999999999999999999999999999999999999999999999999999999999999999" + }, NOTE_TYPES.DM_TYPING); + + check("buildViewEvent", buildViewEvent, { + pubkey, + created_at, + pointerValue: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + pointerTag: ["e", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], + dedupeTag: "session-123" + }, NOTE_TYPES.VIEW_EVENT); + + check("buildZapRequestEvent", buildZapRequestEvent, { + pubkey, + created_at, + recipientPubkey: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + amountSats: 100, + relays: ["wss://zap.relay.com"] + }, NOTE_TYPES.ZAP_REQUEST); + + check("buildReactionEvent", buildReactionEvent, { + pubkey, + created_at, + pointerTag: ["e", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"], + content: "+" + }, NOTE_TYPES.VIDEO_REACTION); + + check("buildCommentEvent", buildCommentEvent, { + pubkey, + created_at, + videoEventId: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + content: "Nice video!" + }, NOTE_TYPES.VIDEO_COMMENT); + + check("buildWatchHistoryEvent", buildWatchHistoryEvent, { + pubkey, + created_at, + monthIdentifier: "2023-10", + content: JSON.stringify({ version: 2, month: "2023-10", items: [] }) + }, NOTE_TYPES.WATCH_HISTORY); + + check("buildSubscriptionListEvent", buildSubscriptionListEvent, { + pubkey, + created_at, + content: "encrypted_subs" + }, NOTE_TYPES.SUBSCRIPTION_LIST); + + check("buildBlockListEvent", buildBlockListEvent, { + pubkey, + created_at, + content: "encrypted_blocks" + }, NOTE_TYPES.USER_BLOCK_LIST); + + check("buildHashtagPreferenceEvent", buildHashtagPreferenceEvent, { + pubkey, + created_at, + content: "encrypted_prefs" + }, NOTE_TYPES.HASHTAG_PREFERENCES); + + check("buildAdminListEvent (moderation)", (p) => buildAdminListEvent("moderation", p), { + pubkey, + created_at, + hexPubkeys: ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"] + }, NOTE_TYPES.ADMIN_MODERATION_LIST); +});