5 changed files with 163 additions and 79 deletions
@ -0,0 +1,38 @@ |
|||||||
|
import { describe, expect, it } from 'vitest' |
||||||
|
import { |
||||||
|
buildPublicMessagePublishRelayUrls, |
||||||
|
collectRecipientInboxUrls, |
||||||
|
collectSenderOutboxUrls |
||||||
|
} from './public-message-publish-relays' |
||||||
|
|
||||||
|
describe('public-message-publish-relays', () => { |
||||||
|
it('collects sender outbox and recipient inbox only', () => { |
||||||
|
expect( |
||||||
|
collectSenderOutboxUrls({ |
||||||
|
write: ['wss://sender-out.example/'], |
||||||
|
read: ['wss://sender-in.example/'], |
||||||
|
httpWrite: ['https://sender-http.example/'], |
||||||
|
httpRead: ['https://sender-http-in.example/'], |
||||||
|
originalRelays: [] |
||||||
|
}) |
||||||
|
).toEqual(['https://sender-http.example/', 'wss://sender-out.example/']) |
||||||
|
|
||||||
|
expect( |
||||||
|
collectRecipientInboxUrls({ |
||||||
|
write: ['wss://recipient-out.example/'], |
||||||
|
read: ['wss://recipient-in.example/'], |
||||||
|
httpWrite: [], |
||||||
|
httpRead: ['https://recipient-http-in.example/'], |
||||||
|
originalRelays: [] |
||||||
|
}) |
||||||
|
).toEqual(['https://recipient-http-in.example/', 'wss://recipient-in.example/']) |
||||||
|
}) |
||||||
|
|
||||||
|
it('orders author outbox before recipient inbox', () => { |
||||||
|
const urls = buildPublicMessagePublishRelayUrls( |
||||||
|
['wss://sender-out.example/'], |
||||||
|
['wss://recipient-in.example/'] |
||||||
|
) |
||||||
|
expect(urls).toEqual(['wss://sender-out.example/', 'wss://recipient-in.example/']) |
||||||
|
}) |
||||||
|
}) |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
import { |
||||||
|
PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP, |
||||||
|
PUBLIC_MESSAGE_RSVP_PUBLISH_MAX_RELAYS |
||||||
|
} from '@/constants' |
||||||
|
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' |
||||||
|
import { dedupeNormalizeRelayUrlsOrdered, relayUrlsLocalsFirst } from '@/lib/relay-url-priority' |
||||||
|
import { isLocalNetworkUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' |
||||||
|
import type { TRelayList } from '@/types' |
||||||
|
|
||||||
|
/** NIP-65 / 10243 outbox URLs for the sender (includes viewer-local outboxes). */ |
||||||
|
export function collectSenderOutboxUrls( |
||||||
|
relayList: TRelayList | null | undefined, |
||||||
|
extraWriteUrls: readonly string[] = [] |
||||||
|
): string[] { |
||||||
|
const http = (relayList?.httpWrite ?? []) |
||||||
|
.map((u) => normalizeHttpRelayUrl(u) || u) |
||||||
|
.filter((u): u is string => !!u) |
||||||
|
const ws = (relayList?.write ?? []) |
||||||
|
.map((u) => normalizeUrl(u) || u) |
||||||
|
.filter((u): u is string => !!u) |
||||||
|
return dedupeNormalizeRelayUrlsOrdered([...http, ...ws, ...extraWriteUrls]) |
||||||
|
} |
||||||
|
|
||||||
|
/** NIP-65 / 10243 inbox URLs for a recipient (drops other people's LAN/loopback). */ |
||||||
|
export function collectRecipientInboxUrls(relayList: TRelayList | null | undefined): string[] { |
||||||
|
const http = (relayList?.httpRead ?? []) |
||||||
|
.map((u) => normalizeHttpRelayUrl(u) || u) |
||||||
|
.filter((u): u is string => !!u && !isLocalNetworkUrl(u)) |
||||||
|
const ws = (relayList?.read ?? []) |
||||||
|
.map((u) => normalizeUrl(u) || u) |
||||||
|
.filter((u): u is string => !!u && !isLocalNetworkUrl(u)) |
||||||
|
return dedupeNormalizeRelayUrlsOrdered([...http, ...ws]) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Kind 24 / 31925 publish stack: sender outboxes first (capped), then recipient inboxes, then remaining outboxes. |
||||||
|
* No FAST_WRITE, favorites, or read aggregators. |
||||||
|
*/ |
||||||
|
export function buildPublicMessagePublishRelayUrls( |
||||||
|
senderOutbox: readonly string[], |
||||||
|
recipientInbox: readonly string[], |
||||||
|
options?: { blockedRelays?: readonly string[]; maxRelays?: number } |
||||||
|
): string[] { |
||||||
|
const authorWriteOrdered = relayUrlsLocalsFirst([...senderOutbox]) |
||||||
|
const recipientReadDeduped = dedupeNormalizeRelayUrlsOrdered([...recipientInbox]) |
||||||
|
const authorTier1Cap = |
||||||
|
recipientReadDeduped.length > 0 |
||||||
|
? Math.min(PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP, authorWriteOrdered.length) |
||||||
|
: authorWriteOrdered.length |
||||||
|
const authorPrimary = authorWriteOrdered.slice(0, authorTier1Cap) |
||||||
|
const authorOverflow = authorWriteOrdered.slice(authorTier1Cap) |
||||||
|
const publishCap = |
||||||
|
recipientReadDeduped.length > 0 ? PUBLIC_MESSAGE_RSVP_PUBLISH_MAX_RELAYS : MAX_PUBLISH_RELAYS |
||||||
|
|
||||||
|
return feedRelayPolicyUrls( |
||||||
|
[ |
||||||
|
{ source: 'viewer-write', urls: authorPrimary }, |
||||||
|
{ source: 'author-read', urls: recipientReadDeduped }, |
||||||
|
{ source: 'viewer-write', urls: authorOverflow } |
||||||
|
], |
||||||
|
{ |
||||||
|
operation: 'write', |
||||||
|
blockedRelays: options?.blockedRelays, |
||||||
|
maxRelays: options?.maxRelays ?? publishCap, |
||||||
|
nostrLandAggr: 'never', |
||||||
|
applySocialKindBlockedFilter: false, |
||||||
|
allowThirdPartyLocalRelays: true |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue