Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
b72e214b1e
  1. 6
      src/components/PostEditor/PostContent.tsx
  2. 38
      src/lib/public-message-publish-relays.test.ts
  3. 70
      src/lib/public-message-publish-relays.ts
  4. 71
      src/services/client.service.ts
  5. 55
      src/services/relay-selection.service.ts

6
src/components/PostEditor/PostContent.tsx

@ -1341,7 +1341,11 @@ export default function PostContent({
specifiedRelayUrls: relayUrls, specifiedRelayUrls: relayUrls,
additionalRelayUrls: isPoll ? pollCreateData.relays : (isPrivateEvent ? privateRelayUrls : additionalRelayUrls), additionalRelayUrls: isPoll ? pollCreateData.relays : (isPrivateEvent ? privateRelayUrls : additionalRelayUrls),
minPow, minPow,
disableFallbacks: additionalRelayUrls.length > 0 || isPrivateEvent, // Don't use fallbacks if user explicitly selected relays or for private events disableFallbacks:
additionalRelayUrls.length > 0 ||
isPrivateEvent ||
isPublicMessage ||
parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE,
addClientTag addClientTag
}) })
// console.log('Published event:', newEvent) // console.log('Published event:', newEvent)

38
src/lib/public-message-publish-relays.test.ts

@ -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/'])
})
})

70
src/lib/public-message-publish-relays.ts

@ -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
}
)
}

71
src/services/client.service.ts

@ -16,8 +16,6 @@ import {
SOCIAL_KIND_BLOCKED_RELAY_URLS, SOCIAL_KIND_BLOCKED_RELAY_URLS,
MAX_CONCURRENT_RELAY_CONNECTIONS, MAX_CONCURRENT_RELAY_CONNECTIONS,
MAX_PUBLISH_RELAYS, MAX_PUBLISH_RELAYS,
PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP,
PUBLIC_MESSAGE_RSVP_PUBLISH_MAX_RELAYS,
PUBLISH_PRIORITIZE_RELAY_ORDER_TIMEOUT_MS, PUBLISH_PRIORITIZE_RELAY_ORDER_TIMEOUT_MS,
PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS, PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS,
FETCH_RELAY_LIST_UI_TIMEOUT_MS, FETCH_RELAY_LIST_UI_TIMEOUT_MS,
@ -130,11 +128,15 @@ import { collectNip05ValuesFromKind0 } from '@/lib/profile-metadata-search'
import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query' import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query'
import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag' import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import {
buildPublicMessagePublishRelayUrls,
collectRecipientInboxUrls,
collectSenderOutboxUrls
} from '@/lib/public-message-publish-relays'
import { import {
buildPrioritizedWriteRelayUrls, buildPrioritizedWriteRelayUrls,
dedupeNormalizeRelayUrlsOrdered, dedupeNormalizeRelayUrlsOrdered,
filterContextAuthorReadRelaysForPublish, filterContextAuthorReadRelaysForPublish
relayUrlsLocalsFirst
} from '@/lib/relay-url-priority' } from '@/lib/relay-url-priority'
import { import {
IndexRelayTransportError, IndexRelayTransportError,
@ -162,7 +164,6 @@ import {
simplifyUrl simplifyUrl
} from '@/lib/url' } from '@/lib/url'
import { canonicalFeedFilter, canonicalRelayUrls } from '@/features/feed/descriptor' import { canonicalFeedFilter, canonicalRelayUrls } from '@/features/feed/descriptor'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { initRelayPoolIdle, touchRelayPoolActivity } from '@/lib/relay-pool-idle' import { initRelayPoolIdle, touchRelayPoolActivity } from '@/lib/relay-pool-idle'
import { relaySessionStrikes } from '@/lib/relay-strikes' import { relaySessionStrikes } from '@/lib/relay-strikes'
import { isSafari } from '@/lib/utils' import { isSafari } from '@/lib/utils'
@ -1191,44 +1192,12 @@ class ClientService extends EventTarget {
this.fetchRelayListWithPublishTimeout(event.pubkey), this.fetchRelayListWithPublishTimeout(event.pubkey),
recipientListsPromise recipientListsPromise
]) ])
const authorHttpWrites = (authorRelayList?.httpWrite ?? []) const authorWrite = collectSenderOutboxUrls(authorRelayList)
.map((url) => normalizeHttpRelayUrl(url)) const recipientRead = dedupeNormalizeRelayUrlsOrdered(
.filter((url): url is string => !!url) recipientRelayLists.flatMap((rl) => collectRecipientInboxUrls(rl))
const authorWsWrites = (authorRelayList?.write ?? []) )
.map((url) => normalizeUrl(url)) let pubRelays = buildPublicMessagePublishRelayUrls(authorWrite, recipientRead, {
.filter((url): url is string => !!url) blockedRelays: blockedRelayUrls
let authorWrite = dedupeNormalizeRelayUrlsOrdered([...authorHttpWrites, ...authorWsWrites])
if (authorWrite.length === 0) {
authorWrite = useGlobalRelayDefaults ? [...FAST_WRITE_RELAY_URLS] : []
}
let recipientRead: string[] = []
recipientRead = recipientRelayLists.flatMap((rl) => [
...(rl?.httpRead ?? []).map((url) => normalizeHttpRelayUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u)),
...(rl?.read ?? []).map((url) => normalizeUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u))
])
recipientRead = dedupeNormalizeRelayUrlsOrdered(recipientRead)
const authorWriteOrdered = relayUrlsLocalsFirst(authorWrite)
/** Without this, tier‑1 author outboxes can consume all of {@link MAX_PUBLISH_RELAYS} and organizer inboxes never receive RSVPs. */
const recipientReadDeduped = recipientRead
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
let pubRelays = feedRelayPolicyUrls([
{ source: 'viewer-write', urls: authorPrimary },
{ source: 'author-read', urls: recipientReadDeduped },
{ source: 'viewer-write', urls: authorOverflow }
], {
operation: 'write',
blockedRelays: blockedRelayUrls,
maxRelays: publishCap,
nostrLandAggr: 'never',
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
}) })
pubRelays = this.filterPublishingRelays(pubRelays, event) pubRelays = this.filterPublishingRelays(pubRelays, event)
logger.debug('[DetermineTargetRelays] Public message / calendar RSVP: author outbox + recipient inboxes only', { logger.debug('[DetermineTargetRelays] Public message / calendar RSVP: author outbox + recipient inboxes only', {
@ -1237,21 +1206,7 @@ class ClientService extends EventTarget {
authorWriteCount: authorWrite.length, authorWriteCount: authorWrite.length,
recipientReadCount: recipientRead.length recipientReadCount: recipientRead.length
}) })
if (pubRelays.length > 0) return pubRelays return pubRelays
if (!useGlobalRelayDefaults) {
return this.filterPublishingRelays([], event)
}
return this.filterPublishingRelays(
feedRelayPolicyUrls([{ source: 'fast-write', urls: relayUrlsLocalsFirst([...FAST_WRITE_RELAY_URLS]) }], {
operation: 'write',
blockedRelays: blockedRelayUrls,
maxRelays: MAX_PUBLISH_RELAYS,
nostrLandAggr: 'never',
applySocialKindBlockedFilter: false,
allowThirdPartyLocalRelays: true
}),
event
)
} }
let relays: string[] let relays: string[]

55
src/services/relay-selection.service.ts

@ -1,6 +1,10 @@
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants' import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import {
collectRecipientInboxUrls,
collectSenderOutboxUrls
} from '@/lib/public-message-publish-relays'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -101,6 +105,7 @@ class RelaySelectionService {
): Promise<{ relays: string[]; relayTypes: Record<string, RelaySourceType>; randomRelayUrls: string[] }> { ): Promise<{ relays: string[]; relayTypes: Record<string, RelaySourceType>; randomRelayUrls: string[] }> {
const { const {
userWriteRelays, userWriteRelays,
userHttpWriteRelays,
favoriteRelays, favoriteRelays,
relaySets, relaySets,
parentEvent, parentEvent,
@ -108,6 +113,24 @@ class RelaySelectionService {
openFrom openFrom
} = context } = context
if (
isPublicMessage ||
(parentEvent != null && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE)
) {
const pmRelays = await this.getPublicMessageRelays(context)
const filtered = this.filterPublishPickerRelays(
this.filterBlockedRelays(pmRelays, context.blockedRelays)
)
const relayTypes: Record<string, RelaySourceType> = {}
const httpSet = new Set(
(userHttpWriteRelays ?? []).map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
)
filtered.forEach((url) => {
relayTypes[url] = httpSet.has(url) ? 'http_relay_list' : 'relay_list'
})
return { relays: filtered, relayTypes, randomRelayUrls: [] }
}
const order: { url: string; type: RelaySourceType }[] = [] const order: { url: string; type: RelaySourceType }[] = []
const seen = new Set<string>() const seen = new Set<string>()
@ -385,9 +408,10 @@ class RelaySelectionService {
else if (parentEvent && (parentEvent.kind === ExtendedKind.DISCUSSION || parentEvent.kind === ExtendedKind.COMMENT)) { else if (parentEvent && (parentEvent.kind === ExtendedKind.DISCUSSION || parentEvent.kind === ExtendedKind.COMMENT)) {
selectedRelays = await this.getDiscussionReplyRelays(context) selectedRelays = await this.getDiscussionReplyRelays(context)
} }
// For public messages, use sender outboxes + receiver inboxes // For public messages, use sender outboxes + receiver inboxes only
else if (isPublicMessage || (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE)) { else if (isPublicMessage || (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE)) {
selectedRelays = await this.getPublicMessageRelays(context) selectedRelays = await this.getPublicMessageRelays(context)
return this.filterPublishPickerRelays(this.filterBlockedRelays(selectedRelays, context.blockedRelays))
} }
// For regular replies, use user's write relays + mention relays // For regular replies, use user's write relays + mention relays
else if (parentEvent && this.isRegularReply(parentEvent)) { else if (parentEvent && this.isRegularReply(parentEvent)) {
@ -456,25 +480,19 @@ class RelaySelectionService {
const allMembers = new Set<string>() const allMembers = new Set<string>()
try { try {
// Get sender's outboxes (write relays) // Get sender's outboxes (write + HTTP write relays)
if (userPubkey) { if (userPubkey) {
allMembers.add(userPubkey) allMembers.add(userPubkey)
let senderRelays = userWriteRelays let senderRelays = collectSenderOutboxUrls(
null,
// If userWriteRelays is empty, try to fetch the user's relay list [...(context.userHttpWriteRelays ?? []), ...userWriteRelays]
)
if (senderRelays.length === 0) { if (senderRelays.length === 0) {
try { try {
const userRelayList = await this.getCachedRelayList(userPubkey) const userRelayList = await this.getCachedRelayList(userPubkey)
if (userRelayList?.write && userRelayList.write.length > 0) { senderRelays = collectSenderOutboxUrls(userRelayList)
senderRelays = userRelayList.write
} else {
// Only fall back to fast write relays if we truly have no user relays
senderRelays = FAST_WRITE_RELAY_URLS
}
} catch (error) { } catch (error) {
logger.warn('Failed to fetch user relay list for PM', { error, userPubkey }) logger.warn('Failed to fetch user relay list for PM', { error, userPubkey })
// Fall back to fast write relays if fetch fails
senderRelays = FAST_WRITE_RELAY_URLS
} }
} }
@ -523,9 +541,7 @@ class RelaySelectionService {
// Use cached version from IndexedDB // Use cached version from IndexedDB
const relayList = await this.getCachedRelayList(pubkey) const relayList = await this.getCachedRelayList(pubkey)
if (!relayList) return [] if (!relayList) return []
const userRelays = relayList.read || [] return this.filterLocalRelaysFromOthers(collectRecipientInboxUrls(relayList))
// Filter out local relays from other users
return this.filterLocalRelaysFromOthers(userRelays)
} catch (error) { } catch (error) {
logger.warn('Failed to fetch relay list', { pubkey, error }) logger.warn('Failed to fetch relay list', { pubkey, error })
return [] return []
@ -609,9 +625,10 @@ class RelaySelectionService {
return Array.from(new Set(normalizedRelays)) return Array.from(new Set(normalizedRelays))
} catch (error) { } catch (error) {
logger.error('Failed to get public message relays', { error, parentEvent: context.parentEvent?.id }) logger.error('Failed to get public message relays', { error, parentEvent: context.parentEvent?.id })
// Fallback to sender's write relays return collectSenderOutboxUrls(null, [
const senderRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS ...(context.userHttpWriteRelays ?? []),
return senderRelays.map(url => normalizeAnyRelayUrl(url) || url).filter(Boolean) ...userWriteRelays
])
} }
} }

Loading…
Cancel
Save