15 changed files with 221 additions and 62 deletions
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
import { kinds } from 'nostr-tools' |
||||
import { describe, expect, it } from 'vitest' |
||||
import { |
||||
filterContextAuthorReadRelaysForPublish, |
||||
filterRelaysForEventPublish, |
||||
relayAllowsPublishKind |
||||
} from './relay-publish-filter' |
||||
|
||||
describe('relay-publish-filter', () => { |
||||
it('blocks profile/index mirrors for kind 1 and 7', () => { |
||||
expect(relayAllowsPublishKind('wss://profiles.nostr1.com/', kinds.ShortTextNote)).toBe(false) |
||||
expect(relayAllowsPublishKind('wss://purplepag.es/', kinds.Reaction)).toBe(false) |
||||
expect(relayAllowsPublishKind('wss://indexer.coracle.social/', kinds.ShortTextNote)).toBe(false) |
||||
}) |
||||
|
||||
it('allows profile/index mirrors for kind 0 and 10002', () => { |
||||
expect(relayAllowsPublishKind('wss://profiles.nostrver.se/', kinds.Metadata)).toBe(true) |
||||
expect(relayAllowsPublishKind('wss://indexer.coracle.social/', kinds.RelayList)).toBe(true) |
||||
}) |
||||
|
||||
it('strips read-only aggregators and profile mirrors from publish lists', () => { |
||||
const out = filterRelaysForEventPublish( |
||||
[ |
||||
'wss://nostr.land/', |
||||
'wss://profiles.nostr1.com/', |
||||
'wss://relay.primal.net/', |
||||
'wss://aggr.nostr.land/' |
||||
], |
||||
kinds.ShortTextNote |
||||
) |
||||
expect(out).toEqual(['wss://relay.primal.net/']) |
||||
}) |
||||
|
||||
it('strips profile mirrors from author read hints', () => { |
||||
const out = filterContextAuthorReadRelaysForPublish([ |
||||
'wss://profiles.nostrver.se/', |
||||
'wss://relay.example.com/' |
||||
]) |
||||
expect(out).toEqual(['wss://relay.example.com/']) |
||||
}) |
||||
}) |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
import { |
||||
AUTHOR_PROFILE_VIEW_REPLACEABLE_KINDS, |
||||
READ_ONLY_RELAY_URLS |
||||
} from '@/constants' |
||||
import { normalizeAnyRelayUrl } from '@/lib/url' |
||||
|
||||
/** |
||||
* Profile mirrors and indexers that reject notes, reactions, and other social kinds. |
||||
* Distinct from {@link READ_ONLY_RELAY_URLS} (search/index aggregators) and |
||||
* {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} (subset also listed here for kind 1 / 1111 / 11). |
||||
*/ |
||||
export const PROFILE_INDEX_ONLY_RELAY_URLS = [ |
||||
'wss://profiles.nostr1.com', |
||||
'wss://purplepag.es', |
||||
'wss://profiles.nostrver.se/', |
||||
'wss://indexer.coracle.social/' |
||||
] as const |
||||
|
||||
const profileIndexOnlyKeySet = new Set( |
||||
PROFILE_INDEX_ONLY_RELAY_URLS.map((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase()).filter(Boolean) |
||||
) |
||||
|
||||
const readOnlyKeySet = new Set( |
||||
READ_ONLY_RELAY_URLS.map((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase()).filter(Boolean) |
||||
) |
||||
|
||||
const profileIndexPublishKindSet = new Set<number>(AUTHOR_PROFILE_VIEW_REPLACEABLE_KINDS) |
||||
|
||||
function relayKey(url: string): string { |
||||
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase() |
||||
} |
||||
|
||||
export function isProfileIndexOnlyRelay(url: string): boolean { |
||||
const key = relayKey(url) |
||||
return key.length > 0 && profileIndexOnlyKeySet.has(key) |
||||
} |
||||
|
||||
export function isReadOnlyRelayUrl(url: string): boolean { |
||||
const key = relayKey(url) |
||||
return key.length > 0 && readOnlyKeySet.has(key) |
||||
} |
||||
|
||||
/** True when this relay may receive an EVENT for `eventKind` (profile/list replaceables only on profile mirrors). */ |
||||
export function relayAllowsPublishKind(url: string, eventKind: number): boolean { |
||||
if (!isProfileIndexOnlyRelay(url)) return true |
||||
return profileIndexPublishKindSet.has(eventKind) |
||||
} |
||||
|
||||
export function filterRelaysForEventPublish(urls: readonly string[], eventKind: number): string[] { |
||||
return urls.filter((u) => relayAllowsPublishKind(u, eventKind) && !isReadOnlyRelayUrl(u)) |
||||
} |
||||
|
||||
/** |
||||
* Reply/mention author **read** hints used as publish targets: never LAN/Tor, read-only aggregators, |
||||
* or profile/index mirrors (those are not inboxes for notes or reactions). |
||||
*/ |
||||
export function filterContextAuthorReadRelaysForPublish(urls: readonly string[]): string[] { |
||||
return urls.filter((u) => { |
||||
const key = relayKey(u) |
||||
if (!key) return false |
||||
if (isReadOnlyRelayUrl(u)) return false |
||||
if (isProfileIndexOnlyRelay(u)) return false |
||||
return true |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue