Restore legacy subscription list compatibility

This commit is contained in:
thePR0M3TH3AN committed 2025-11-03 09:42:17 -05:00
1 parent e496376236
commit 7db78b07f2
3 files changed
+89 -4

No files matched your search

+3 -2
View File
@@ -147,7 +147,7 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`, clients also read legacy `30078`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
| Watch history index (`NOTE_TYPES.WATCH_HISTORY_INDEX`) | `WATCH_HISTORY_KIND` (default `30079`, clients also read legacy `30078`) | `['d', WATCH_HISTORY_LIST_IDENTIFIER]`, `['snapshot', <id>]`, `['chunks', <total>]`, repeated `['a', <chunk address>]` pointers plus schema append tags | JSON payload `{ snapshot, totalChunks }` (may be empty when using tags only) |
| Watch history chunk (`NOTE_TYPES.WATCH_HISTORY_CHUNK`) | `WATCH_HISTORY_KIND` (default `30079`, clients also read legacy `30078`) | `['d', <snapshotId:index>]`, `['encrypted','nip04']`, `['snapshot', <id>]`, `['chunk', <index>, <total>]`, optional leading `['head','1']` on the first chunk, pointer tags for each item, plus schema append tags | NIP-04 encrypted JSON chunk (`{ version, snapshot, chunkIndex, totalChunks, items[] }`) |
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` | `['d', 'subscriptions']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` (clients also read legacy `30002`) | `['d', 'subscriptions']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
| User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `10000` | `['d', 'user-blocks']` | NIP-04/NIP-44 encrypted JSON `{ blockedPubkeys: string[] }` |
| Hashtag preferences (`NOTE_TYPES.HASHTAG_PREFERENCES`) | `30005` | `['d', 'bitvid:tag-preferences']` plus schema-appended `['encrypted','nip44_v2']` | NIP-44 encrypted JSON `{ version, interests: string[], disinterests: string[] }` |
| Admin moderation list (`NOTE_TYPES.ADMIN_MODERATION_LIST`) | `30000` | `['d', 'bitvid:admin:editors']`, repeated `['p', <pubkey>]` entries | Empty content |
@@ -158,7 +158,8 @@ Subscription lists therefore match the
[NIP-51 follow-set specification](./nips/51.md#sets) by emitting kind `30000`
events with the shared `['d','subscriptions']` identifier. Builders continue to
encrypt the payload so individual follows stay private unless explicitly
revealed by the author.
revealed by the author. Readers still request legacy kind `30002` alongside
`30000` while older clients migrate so existing follow sets remain visible.
NIP-94 compliance: mirror events now normalize the `['m', <mime>]` tag to lowercase—covering both user input and inferred values—so they satisfy [NIP-94's lowercase MIME requirement](./nips/94.md). Related helpers reuse the same normalization to keep future publishers aligned.
+7 -1
View File
@@ -32,6 +32,7 @@ import nostrService from "./services/nostrService.js";
const SUBSCRIPTION_SET_KIND =
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
const LEGACY_SUBSCRIPTION_SET_KIND = 30002;
function normalizeHexPubkey(value) {
if (typeof value !== "string") {
@@ -265,8 +266,13 @@ class SubscriptionsManager {
authors.push(userPubkey);
}
const subscriptionKinds = [SUBSCRIPTION_SET_KIND];
if (!subscriptionKinds.includes(LEGACY_SUBSCRIPTION_SET_KIND)) {
subscriptionKinds.push(LEGACY_SUBSCRIPTION_SET_KIND);
}
const filter = {
kinds: [SUBSCRIPTION_SET_KIND],
kinds: subscriptionKinds,
authors,
"#d": [SUBSCRIPTION_LIST_IDENTIFIER],
limit: 1
+79 -1
View File
@@ -8,6 +8,7 @@ import { subscriptions } from "../js/subscriptions.js";
import { setApplication } from "../js/applicationContext.js";
import nostrService from "../js/services/nostrService.js";
import moderationService from "../js/services/moderationService.js";
import { getNostrEventSchema, NOTE_TYPES } from "../js/nostrEventSchemas.js";
test("loadSubscriptions aggregates relay results when one rejects", async () => {
const SubscriptionsManager = subscriptions.constructor;
@@ -55,7 +56,7 @@ test("loadSubscriptions aggregates relay results when one rejects", async () =>
const listCalls = [];
nostrClient.pool = {
list(urls) {
list(urls, _filters) {
const url = Array.isArray(urls) ? urls[0] : urls;
listCalls.push(url);
if (url === "wss://relay-b.example") {
@@ -130,6 +131,83 @@ test("loadSubscriptions aggregates relay results when one rejects", async () =>
}
});
test("loadSubscriptions queries legacy kind fallback for pre-migration events", async () => {
const SubscriptionsManager = subscriptions.constructor;
const manager = new SubscriptionsManager();
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: nostrClient.relays;
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
const originalPool = nostrClient.pool;
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
const hadWindow = typeof globalThis.window !== "undefined";
if (!hadWindow) {
globalThis.window = {};
}
const originalWindowNostr = globalThis.window.nostr;
const relayUrls = ["wss://relay-legacy.example"];
nostrClient.relays = relayUrls;
nostrClient.writeRelays = relayUrls;
const capturedFilters = [];
nostrClient.pool = {
list(urls, filters) {
const filterArray = Array.isArray(filters) ? filters : [];
if (filterArray.length) {
capturedFilters.push(filterArray[0]);
}
return Promise.resolve([]);
},
};
nostrClient.ensureExtensionPermissions = async () => ({ ok: true });
try {
await manager.loadSubscriptions("user-pubkey-legacy");
assert.ok(
capturedFilters.length > 0,
"loadSubscriptions should pass subscription filters to pool.list",
);
const schemaKind =
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
for (const filter of capturedFilters) {
const kinds = Array.isArray(filter?.kinds) ? filter.kinds : [];
assert.ok(kinds.includes(schemaKind), "filter should include the active follow-set kind");
assert.ok(
kinds.includes(30002),
"filter should include the legacy kind for backwards compatibility",
);
const uniqueKinds = Array.from(new Set(kinds));
assert.equal(
kinds.length,
uniqueKinds.length,
"filter kinds should not contain duplicate entries",
);
}
} finally {
nostrClient.relays = originalRelays;
nostrClient.writeRelays = originalWriteRelays;
nostrClient.pool = originalPool;
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
if (typeof originalWindowNostr === "undefined") {
delete globalThis.window.nostr;
} else {
globalThis.window.nostr = originalWindowNostr;
}
if (!hadWindow) {
delete globalThis.window;
}
}
});
test("loadSubscriptions falls back to nip44 when hinted", async () => {
const SubscriptionsManager = subscriptions.constructor;
const manager = new SubscriptionsManager();