mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
refactor: centralize nostr event schemas
This commit is contained in:
1 parent
b6e1219d09
commit
0e29b07d5b
6 files changed
+634
-91
No files matched your search
@@ -0,0 +1,56 @@
|
||||
# Nostr event schemas
|
||||
|
||||
BitVid now centralizes every note that it publishes to Nostr in
|
||||
[`js/nostrEventSchemas.js`](../js/nostrEventSchemas.js). The module defines the
|
||||
kind, required tags, and content format for each note type so troubleshooting
|
||||
no longer requires hunting through the codebase. It also exposes helpers for
|
||||
building events and for overriding the schema at runtime when you need to
|
||||
experiment.
|
||||
|
||||
## Runtime helpers
|
||||
|
||||
```js
|
||||
import {
|
||||
NOTE_TYPES,
|
||||
getNostrEventSchema,
|
||||
setNostrEventSchemaOverrides,
|
||||
buildVideoPostEvent,
|
||||
} from "./nostrEventSchemas.js";
|
||||
|
||||
// Inspect the current schema
|
||||
console.log(getNostrEventSchema(NOTE_TYPES.VIDEO_POST));
|
||||
|
||||
// Temporarily override the kind while debugging
|
||||
setNostrEventSchemaOverrides({
|
||||
[NOTE_TYPES.VIDEO_POST]: { kind: 30000 },
|
||||
});
|
||||
|
||||
// Build an event with the active schema
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: "debug-video",
|
||||
content: { version: 3, title: "Test", videoRootId: "debug" },
|
||||
});
|
||||
```
|
||||
|
||||
In the browser you can call the same helpers from DevTools via
|
||||
`window.BitVidNostrEvents` and `window.BitVidNostrEventOverrides`.
|
||||
|
||||
## Event catalogue
|
||||
|
||||
| Note | Kind (default) | Tags | Content format |
|
||||
| --- | --- | --- | --- |
|
||||
| Video post (`NOTE_TYPES.VIDEO_POST`) | `30078` | `['t','video']`, `['d', <stable video identifier>]` plus optional schema append tags | JSON payload using Content Schema v3 (`version`, `title`, optional `url`, `magnet`, `thumbnail`, `description`, `mode`, `videoRootId`, `deleted`, `isPrivate`, `enableComments`, `ws`, `xs`) |
|
||||
| NIP-94 mirror (`NOTE_TYPES.VIDEO_MIRROR`) | `1063` | Tags forwarded from `publishVideo` (URL, mime type, thumbnail, alt text, magnet) | Plain text alt description |
|
||||
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30078`) | `['t','view']`, `['video', <pointer id>]`, pointer tag (`['a', ...]` or `['e', ...]`), optional dedupe `['d', <scope>]`, optional `['session','true']` when a session actor signs, plus any extra debugging tags | Optional plaintext message |
|
||||
| Watch history chunk (`NOTE_TYPES.WATCH_HISTORY_CHUNK`) | `WATCH_HISTORY_KIND` | `['d', <chunk identifier>]`, `['encrypted','nip04']`, `['snapshot', <id>]`, `['chunk', <index>, <total>]`, pointer tags for each entry, `['head','1']` + `['a', <address>]` pointers on the first chunk | NIP-04 encrypted JSON chunk (`{ version, snapshot, chunkIndex, totalChunks, items[] }`) |
|
||||
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30002` | `['d', 'subscriptions']` | NIP-04 encrypted JSON `{ subPubkeys: string[] }` |
|
||||
| User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `30002` | `['d', 'user-blocks']` | NIP-04 encrypted JSON `{ blockedPubkeys: string[] }` |
|
||||
| Admin moderation list (`NOTE_TYPES.ADMIN_MODERATION_LIST`) | `30000` | `['d', 'bitvid:admin:editors']`, repeated `['p', <pubkey>]` entries | Empty content |
|
||||
| Admin blacklist (`NOTE_TYPES.ADMIN_BLACKLIST`) | `30000` | `['d', 'bitvid:admin:blacklist']`, repeated `['p', <pubkey>]` entries | Empty content |
|
||||
| Admin whitelist (`NOTE_TYPES.ADMIN_WHITELIST`) | `30000` | `['d', 'bitvid:admin:whitelist']`, repeated `['p', <pubkey>]` entries | Empty content |
|
||||
|
||||
If you introduce a new Nostr feature, add its schema to
|
||||
`js/nostrEventSchemas.js` so that the catalogue stays complete and so existing
|
||||
builders inherit the same debugging knobs.
|
||||
+10
-22
@@ -8,12 +8,10 @@ import {
|
||||
isDevMode,
|
||||
} from "./config.js";
|
||||
import { nostrClient } from "./nostr.js";
|
||||
|
||||
const LIST_IDENTIFIERS = {
|
||||
editors: "editors",
|
||||
whitelist: "whitelist",
|
||||
blacklist: "blacklist",
|
||||
};
|
||||
import {
|
||||
buildAdminListEvent,
|
||||
ADMIN_LIST_IDENTIFIERS,
|
||||
} from "./nostrEventSchemas.js";
|
||||
|
||||
const LEGACY_STORAGE_KEYS = {
|
||||
editors: "bitvid_admin_editors",
|
||||
@@ -319,18 +317,15 @@ async function loadNostrList(identifier) {
|
||||
|
||||
async function loadNostrState() {
|
||||
const [editors, whitelist, blacklist] = await Promise.all([
|
||||
loadNostrList(LIST_IDENTIFIERS.editors),
|
||||
loadNostrList(LIST_IDENTIFIERS.whitelist),
|
||||
loadNostrList(LIST_IDENTIFIERS.blacklist),
|
||||
loadNostrList(ADMIN_LIST_IDENTIFIERS.editors),
|
||||
loadNostrList(ADMIN_LIST_IDENTIFIERS.whitelist),
|
||||
loadNostrList(ADMIN_LIST_IDENTIFIERS.blacklist),
|
||||
]);
|
||||
|
||||
return sanitizeAdminState({ editors, whitelist, blacklist });
|
||||
}
|
||||
|
||||
function buildListEvent(listKey, npubs, actorHex) {
|
||||
const identifier = LIST_IDENTIFIERS[listKey];
|
||||
const dTagValue = `${ADMIN_LIST_NAMESPACE}:${identifier}`;
|
||||
|
||||
const hexPubkeys = Array.from(
|
||||
new Set(npubs.map((npub) => {
|
||||
try {
|
||||
@@ -360,18 +355,11 @@ function buildListEvent(listKey, npubs, actorHex) {
|
||||
}
|
||||
}
|
||||
|
||||
const tags = [["d", dTagValue]];
|
||||
hexPubkeys.forEach((hex) => {
|
||||
tags.push(["p", hex]);
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 30000,
|
||||
return buildAdminListEvent(listKey, {
|
||||
pubkey: actorHex,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
hexPubkeys,
|
||||
});
|
||||
}
|
||||
|
||||
function publishToRelay(url, signedEvent, listKey) {
|
||||
|
||||
+27
-58
@@ -16,6 +16,12 @@ import { accessControl } from "./accessControl.js";
|
||||
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
|
||||
import { deriveTitleFromEvent, magnetFromText } from "./videoEventUtils.js";
|
||||
import { extractMagnetHints } from "./magnet.js";
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildViewEvent,
|
||||
buildWatchHistoryChunkEvent,
|
||||
} from "./nostrEventSchemas.js";
|
||||
|
||||
/**
|
||||
* The usual relays
|
||||
@@ -1935,34 +1941,19 @@ class NostrClient {
|
||||
|
||||
const chunkIdentifier = chunkIdentifiers[index] || WATCH_HISTORY_LIST_IDENTIFIER;
|
||||
|
||||
const tags = [
|
||||
["d", chunkIdentifier],
|
||||
["encrypted", "nip04"],
|
||||
["snapshot", snapshotId],
|
||||
["chunk", String(index), String(totalChunks)],
|
||||
...pointerTags,
|
||||
];
|
||||
if (index === 0) {
|
||||
tags.splice(2, 0, ["head", "1"]);
|
||||
const pointerTagsForChunks = chunkAddresses
|
||||
.map((address) =>
|
||||
typeof address === "string" && address
|
||||
? ["a", address]
|
||||
: null
|
||||
)
|
||||
.filter(Boolean);
|
||||
tags.push(...pointerTagsForChunks);
|
||||
}
|
||||
|
||||
// Ensure created_at stays monotonic so chunk 0 always outranks prior snapshots
|
||||
// even when multiple publishes land within the same wall-clock second.
|
||||
const event = {
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
const event = buildWatchHistoryChunkEvent({
|
||||
pubkey: normalizedActor,
|
||||
created_at: baseTimestamp + (totalChunks - index - 1),
|
||||
tags,
|
||||
chunkIdentifier,
|
||||
snapshotId,
|
||||
chunkIndex: index,
|
||||
totalChunks,
|
||||
pointerTags,
|
||||
chunkAddresses,
|
||||
content: encryptionResult.ciphertext,
|
||||
};
|
||||
});
|
||||
|
||||
let signedEvent;
|
||||
if (useExtension) {
|
||||
@@ -2234,41 +2225,24 @@ class NostrClient {
|
||||
? ["e", pointer.value, pointer.relay]
|
||||
: ["e", pointer.value];
|
||||
|
||||
const tags = [
|
||||
["t", "view"],
|
||||
["video", pointer.value],
|
||||
pointerTag,
|
||||
...additionalTags,
|
||||
];
|
||||
|
||||
const dedupeTagValue = deriveViewEventDedupTag(
|
||||
actorPubkey,
|
||||
pointer,
|
||||
createdAt
|
||||
);
|
||||
if (
|
||||
dedupeTagValue &&
|
||||
!tags.some((tag) => tag[0] === "d" && tag[1] === dedupeTagValue)
|
||||
) {
|
||||
tags.push(["d", dedupeTagValue]);
|
||||
}
|
||||
|
||||
if (
|
||||
usingSessionActor &&
|
||||
!tags.some((tag) => tag[0] === "session" && tag[1] === "true")
|
||||
) {
|
||||
tags.push(["session", "true"]);
|
||||
}
|
||||
const content =
|
||||
typeof options.content === "string" ? options.content : "";
|
||||
|
||||
const event = {
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
const event = buildViewEvent({
|
||||
pubkey: actorPubkey,
|
||||
created_at: createdAt,
|
||||
tags,
|
||||
pointerValue: pointer.value,
|
||||
pointerTag,
|
||||
dedupeTag: dedupeTagValue,
|
||||
includeSessionTag: usingSessionActor,
|
||||
additionalTags,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
let signedEvent = null;
|
||||
|
||||
@@ -3918,16 +3892,12 @@ class NostrClient {
|
||||
contentObject.xs = finalXs;
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey,
|
||||
created_at: createdAt,
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", dTagValue],
|
||||
],
|
||||
content: JSON.stringify(contentObject),
|
||||
};
|
||||
dTagValue,
|
||||
content: contentObject,
|
||||
});
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Publish event with brand-new root:", videoRootId);
|
||||
@@ -3972,13 +3942,12 @@ class NostrClient {
|
||||
mirrorTags.push(["magnet", finalMagnet]);
|
||||
}
|
||||
|
||||
const mirrorEvent = {
|
||||
kind: 1063,
|
||||
const mirrorEvent = buildVideoMirrorEvent({
|
||||
pubkey,
|
||||
created_at: createdAt,
|
||||
tags: mirrorTags,
|
||||
content: altText,
|
||||
};
|
||||
});
|
||||
|
||||
if (isDevMode) {
|
||||
console.log("Prepared NIP-94 mirror event:", mirrorEvent);
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
import {
|
||||
ADMIN_LIST_NAMESPACE,
|
||||
isDevMode,
|
||||
WATCH_HISTORY_KIND,
|
||||
WATCH_HISTORY_LIST_IDENTIFIER,
|
||||
} from "./config.js";
|
||||
|
||||
export const NOTE_TYPES = Object.freeze({
|
||||
VIDEO_POST: "videoPost",
|
||||
VIDEO_MIRROR: "videoMirror",
|
||||
VIEW_EVENT: "viewEvent",
|
||||
WATCH_HISTORY_CHUNK: "watchHistoryChunk",
|
||||
SUBSCRIPTION_LIST: "subscriptionList",
|
||||
USER_BLOCK_LIST: "userBlockList",
|
||||
ADMIN_MODERATION_LIST: "adminModerationList",
|
||||
ADMIN_BLACKLIST: "adminBlacklist",
|
||||
ADMIN_WHITELIST: "adminWhitelist",
|
||||
});
|
||||
|
||||
export const SUBSCRIPTION_LIST_IDENTIFIER = "subscriptions";
|
||||
export const BLOCK_LIST_IDENTIFIER = "user-blocks";
|
||||
export const ADMIN_LIST_IDENTIFIERS = Object.freeze({
|
||||
moderation: "editors",
|
||||
editors: "editors",
|
||||
whitelist: "whitelist",
|
||||
blacklist: "blacklist",
|
||||
});
|
||||
|
||||
const DEFAULT_APPEND_TAGS = [];
|
||||
|
||||
const BASE_SCHEMAS = {
|
||||
[NOTE_TYPES.VIDEO_POST]: {
|
||||
type: NOTE_TYPES.VIDEO_POST,
|
||||
label: "Video post",
|
||||
kind: 30078,
|
||||
topicTag: { name: "t", value: "video" },
|
||||
identifierTag: { name: "d" },
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "json",
|
||||
schemaVersion: 3,
|
||||
fields: [
|
||||
{ key: "version", type: "number", required: true },
|
||||
{ key: "title", type: "string", required: true },
|
||||
{ key: "url", type: "string", required: false },
|
||||
{ key: "magnet", type: "string", required: false },
|
||||
{ key: "thumbnail", type: "string", required: false },
|
||||
{ key: "description", type: "string", required: false },
|
||||
{ key: "mode", type: "string", required: false },
|
||||
{ key: "videoRootId", type: "string", required: true },
|
||||
{ key: "deleted", type: "boolean", required: false },
|
||||
{ key: "isPrivate", type: "boolean", required: false },
|
||||
{ key: "enableComments", type: "boolean", required: false },
|
||||
{ key: "ws", type: "string", required: false },
|
||||
{ key: "xs", type: "string", required: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.VIDEO_MIRROR]: {
|
||||
type: NOTE_TYPES.VIDEO_MIRROR,
|
||||
label: "NIP-94 mirror",
|
||||
kind: 1063,
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description: "Optional alt text carried alongside hosted URL metadata.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.VIEW_EVENT]: {
|
||||
type: NOTE_TYPES.VIEW_EVENT,
|
||||
label: "View counter",
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
topicTag: { name: "t", value: "view" },
|
||||
pointerTagName: "video",
|
||||
identifierTag: { name: "d" },
|
||||
sessionTag: { name: "session", value: "true" },
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description: "Optional plaintext content used for diagnostics.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.WATCH_HISTORY_CHUNK]: {
|
||||
type: NOTE_TYPES.WATCH_HISTORY_CHUNK,
|
||||
label: "Watch history snapshot",
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
identifierTag: {
|
||||
name: "d",
|
||||
value: WATCH_HISTORY_LIST_IDENTIFIER,
|
||||
},
|
||||
encryptionTag: { name: "encrypted", value: "nip04" },
|
||||
snapshotTagName: "snapshot",
|
||||
chunkTagName: "chunk",
|
||||
headTag: { name: "head", value: "1" },
|
||||
headTagIndex: 2,
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "nip04-json",
|
||||
description:
|
||||
"Encrypted JSON payload containing chunked watch history entries.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.SUBSCRIPTION_LIST]: {
|
||||
type: NOTE_TYPES.SUBSCRIPTION_LIST,
|
||||
label: "Subscription list",
|
||||
kind: 30002,
|
||||
identifierTag: {
|
||||
name: "d",
|
||||
value: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
},
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "nip04-json",
|
||||
description: "Encrypted JSON: { subPubkeys: string[] }.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.USER_BLOCK_LIST]: {
|
||||
type: NOTE_TYPES.USER_BLOCK_LIST,
|
||||
label: "User block list",
|
||||
kind: 30002,
|
||||
identifierTag: {
|
||||
name: "d",
|
||||
value: BLOCK_LIST_IDENTIFIER,
|
||||
},
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "nip04-json",
|
||||
description: "Encrypted JSON: { blockedPubkeys: string[] }.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.ADMIN_MODERATION_LIST]: {
|
||||
type: NOTE_TYPES.ADMIN_MODERATION_LIST,
|
||||
label: "Admin moderation list",
|
||||
kind: 30000,
|
||||
identifierTag: {
|
||||
name: "d",
|
||||
value: `${ADMIN_LIST_NAMESPACE}:${ADMIN_LIST_IDENTIFIERS.moderation}`,
|
||||
},
|
||||
participantTagName: "p",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: { format: "empty", description: "Content field unused." },
|
||||
},
|
||||
[NOTE_TYPES.ADMIN_BLACKLIST]: {
|
||||
type: NOTE_TYPES.ADMIN_BLACKLIST,
|
||||
label: "Admin blacklist",
|
||||
kind: 30000,
|
||||
identifierTag: {
|
||||
name: "d",
|
||||
value: `${ADMIN_LIST_NAMESPACE}:${ADMIN_LIST_IDENTIFIERS.blacklist}`,
|
||||
},
|
||||
participantTagName: "p",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: { format: "empty", description: "Content field unused." },
|
||||
},
|
||||
[NOTE_TYPES.ADMIN_WHITELIST]: {
|
||||
type: NOTE_TYPES.ADMIN_WHITELIST,
|
||||
label: "Admin whitelist",
|
||||
kind: 30000,
|
||||
identifierTag: {
|
||||
name: "d",
|
||||
value: `${ADMIN_LIST_NAMESPACE}:${ADMIN_LIST_IDENTIFIERS.whitelist}`,
|
||||
},
|
||||
participantTagName: "p",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: { format: "empty", description: "Content field unused." },
|
||||
},
|
||||
};
|
||||
|
||||
let schemaOverrides = {};
|
||||
|
||||
function clone(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(clone);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const result = {};
|
||||
for (const [key, nested] of Object.entries(value)) {
|
||||
result[key] = clone(nested);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function mergeDeep(base, override) {
|
||||
const result = clone(base);
|
||||
if (!override || typeof override !== "object") {
|
||||
return result;
|
||||
}
|
||||
for (const [key, value] of Object.entries(override)) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
result[key] = mergeDeep(base?.[key] ?? {}, value);
|
||||
} else {
|
||||
result[key] = clone(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveOverride(type) {
|
||||
const local = schemaOverrides?.[type];
|
||||
if (local) {
|
||||
return local;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const runtimeOverrides = window.BitVidNostrEventOverrides;
|
||||
if (runtimeOverrides && runtimeOverrides[type]) {
|
||||
return runtimeOverrides[type];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setNostrEventSchemaOverrides(overrides = {}) {
|
||||
if (overrides && typeof overrides === "object") {
|
||||
schemaOverrides = overrides;
|
||||
} else {
|
||||
schemaOverrides = {};
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.BitVidNostrEventOverrides = schemaOverrides;
|
||||
}
|
||||
}
|
||||
|
||||
export function getNostrEventSchema(type) {
|
||||
const base = BASE_SCHEMAS[type];
|
||||
if (!base) {
|
||||
return null;
|
||||
}
|
||||
const override = resolveOverride(type);
|
||||
return mergeDeep(base, override);
|
||||
}
|
||||
|
||||
export function getAllNostrEventSchemas() {
|
||||
const entries = {};
|
||||
for (const type of Object.keys(BASE_SCHEMAS)) {
|
||||
entries[type] = getNostrEventSchema(type);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function appendSchemaTags(tags, schema) {
|
||||
if (!Array.isArray(schema?.appendTags)) {
|
||||
return tags;
|
||||
}
|
||||
schema.appendTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function buildVideoPostEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
dTagValue,
|
||||
content,
|
||||
additionalTags = [],
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_POST);
|
||||
const tags = [];
|
||||
if (schema?.topicTag?.name && schema?.topicTag?.value) {
|
||||
tags.push([schema.topicTag.name, schema.topicTag.value]);
|
||||
}
|
||||
if (schema?.identifierTag?.name && dTagValue) {
|
||||
tags.push([schema.identifierTag.name, dTagValue]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? 30078,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : JSON.stringify(content ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildVideoMirrorEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
tags = [],
|
||||
content = "",
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_MIRROR);
|
||||
const combinedTags = [];
|
||||
appendSchemaTags(combinedTags, schema);
|
||||
if (Array.isArray(tags)) {
|
||||
tags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
combinedTags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: schema?.kind ?? 1063,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags: combinedTags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildViewEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
pointerValue,
|
||||
pointerTag,
|
||||
dedupeTag,
|
||||
includeSessionTag = false,
|
||||
additionalTags = [],
|
||||
content = "",
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.VIEW_EVENT);
|
||||
const tags = [];
|
||||
if (schema?.topicTag?.name && schema?.topicTag?.value) {
|
||||
tags.push([schema.topicTag.name, schema.topicTag.value]);
|
||||
}
|
||||
|
||||
const pointerTagName = schema?.pointerTagName || "video";
|
||||
if (pointerValue) {
|
||||
tags.push([pointerTagName, pointerValue]);
|
||||
}
|
||||
if (Array.isArray(pointerTag) && pointerTag.length >= 2) {
|
||||
tags.push(pointerTag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (dedupeTag) {
|
||||
const identifierName = schema?.identifierTag?.name || "d";
|
||||
const hasDedupe = tags.some(
|
||||
(tag) => tag[0] === identifierName && tag[1] === dedupeTag
|
||||
);
|
||||
if (!hasDedupe) {
|
||||
tags.push([identifierName, dedupeTag]);
|
||||
}
|
||||
}
|
||||
if (includeSessionTag && schema?.sessionTag?.name && schema?.sessionTag?.value) {
|
||||
const hasSession = tags.some(
|
||||
(tag) => tag[0] === schema.sessionTag.name && tag[1] === schema.sessionTag.value
|
||||
);
|
||||
if (!hasSession) {
|
||||
tags.push([schema.sessionTag.name, schema.sessionTag.value]);
|
||||
}
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? WATCH_HISTORY_KIND,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWatchHistoryChunkEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
chunkIdentifier,
|
||||
snapshotId,
|
||||
chunkIndex,
|
||||
totalChunks,
|
||||
pointerTags = [],
|
||||
chunkAddresses = [],
|
||||
content,
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.WATCH_HISTORY_CHUNK);
|
||||
const tags = [];
|
||||
const identifierName = schema?.identifierTag?.name || "d";
|
||||
const identifierValue = chunkIdentifier || schema?.identifierTag?.value;
|
||||
if (identifierName && identifierValue) {
|
||||
tags.push([identifierName, identifierValue]);
|
||||
}
|
||||
if (schema?.encryptionTag?.name && schema?.encryptionTag?.value) {
|
||||
tags.push([schema.encryptionTag.name, schema.encryptionTag.value]);
|
||||
}
|
||||
const snapshotTagName = schema?.snapshotTagName;
|
||||
if (snapshotTagName && snapshotId) {
|
||||
tags.push([snapshotTagName, snapshotId]);
|
||||
}
|
||||
const chunkTagName = schema?.chunkTagName;
|
||||
if (chunkTagName && typeof chunkIndex === "number" && typeof totalChunks === "number") {
|
||||
tags.push([chunkTagName, String(chunkIndex), String(totalChunks)]);
|
||||
}
|
||||
pointerTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
|
||||
if (chunkIndex === 0 && schema?.headTag?.name && schema?.headTag?.value) {
|
||||
const insertionIndex = Number.isInteger(schema?.headTagIndex)
|
||||
? Math.max(0, Math.min(tags.length, schema.headTagIndex))
|
||||
: Math.min(tags.length, 2);
|
||||
tags.splice(insertionIndex, 0, [schema.headTag.name, schema.headTag.value]);
|
||||
chunkAddresses.forEach((address) => {
|
||||
if (typeof address === "string" && address) {
|
||||
tags.push(["a", address]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? WATCH_HISTORY_KIND,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSubscriptionListEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
content,
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST);
|
||||
const tags = [];
|
||||
const identifierName = schema?.identifierTag?.name || "d";
|
||||
const identifierValue = schema?.identifierTag?.value || SUBSCRIPTION_LIST_IDENTIFIER;
|
||||
if (identifierName && identifierValue) {
|
||||
tags.push([identifierName, identifierValue]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
kind: schema?.kind ?? 30002,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBlockListEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
content,
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.USER_BLOCK_LIST);
|
||||
const tags = [];
|
||||
const identifierName = schema?.identifierTag?.name || "d";
|
||||
const identifierValue = schema?.identifierTag?.value || BLOCK_LIST_IDENTIFIER;
|
||||
if (identifierName && identifierValue) {
|
||||
tags.push([identifierName, identifierValue]);
|
||||
}
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
kind: schema?.kind ?? 30002,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: typeof content === "string" ? content : String(content ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAdminNoteType(listKey) {
|
||||
switch (listKey) {
|
||||
case "moderation":
|
||||
case "editors":
|
||||
return NOTE_TYPES.ADMIN_MODERATION_LIST;
|
||||
case "whitelist":
|
||||
return NOTE_TYPES.ADMIN_WHITELIST;
|
||||
case "blacklist":
|
||||
return NOTE_TYPES.ADMIN_BLACKLIST;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminListEvent(listKey, { pubkey, created_at, hexPubkeys = [] }) {
|
||||
const schema = getNostrEventSchema(resolveAdminNoteType(listKey));
|
||||
if (!schema) {
|
||||
if (isDevMode) {
|
||||
console.warn(`[nostrEventSchemas] Unknown admin list key: ${listKey}`);
|
||||
}
|
||||
return {
|
||||
kind: 30000,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags: [],
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
const tags = [];
|
||||
if (schema?.identifierTag?.name && schema?.identifierTag?.value) {
|
||||
tags.push([schema.identifierTag.name, schema.identifierTag.value]);
|
||||
}
|
||||
const participantTagName = schema?.participantTagName || "p";
|
||||
hexPubkeys.forEach((hex) => {
|
||||
if (typeof hex === "string" && hex.trim()) {
|
||||
tags.push([participantTagName, hex.trim()]);
|
||||
}
|
||||
});
|
||||
appendSchemaTags(tags, schema);
|
||||
return {
|
||||
kind: schema?.kind ?? 30000,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.BitVidNostrEvents = {
|
||||
NOTE_TYPES,
|
||||
getSchema: getNostrEventSchema,
|
||||
getAllSchemas: getAllNostrEventSchemas,
|
||||
setOverrides: setNostrEventSchemaOverrides,
|
||||
};
|
||||
}
|
||||
+7
-5
@@ -5,6 +5,10 @@ import {
|
||||
} from "./nostr.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import {
|
||||
buildSubscriptionListEvent,
|
||||
SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
} from "./nostrEventSchemas.js";
|
||||
|
||||
function getAbsoluteShareUrl(nevent) {
|
||||
if (!nevent) {
|
||||
@@ -58,7 +62,7 @@ class SubscriptionsManager {
|
||||
const filter = {
|
||||
kinds: [30002],
|
||||
authors: [userPubkey],
|
||||
"#d": ["subscriptions"],
|
||||
"#d": [SUBSCRIPTION_LIST_IDENTIFIER],
|
||||
limit: 1,
|
||||
};
|
||||
|
||||
@@ -168,13 +172,11 @@ class SubscriptionsManager {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const evt = {
|
||||
kind: 30002,
|
||||
const evt = buildSubscriptionListEvent({
|
||||
pubkey: userPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["d", "subscriptions"]],
|
||||
content: cipherText,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(evt);
|
||||
|
||||
+3
-6
@@ -1,7 +1,6 @@
|
||||
// js/userBlocks.js
|
||||
import { nostrClient } from "./nostr.js";
|
||||
|
||||
const BLOCK_LIST_IDENTIFIER = "user-blocks";
|
||||
import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER } from "./nostrEventSchemas.js";
|
||||
|
||||
function normalizeHex(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
@@ -250,13 +249,11 @@ class UserBlockListManager {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 30002,
|
||||
const event = buildBlockListEvent({
|
||||
pubkey: normalized,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["d", BLOCK_LIST_IDENTIFIER]],
|
||||
content: cipherText,
|
||||
};
|
||||
});
|
||||
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
|
||||
Reference in new issue
Block a user