You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

186 lines
6.8 KiB

import { ExtendedKind, PROFILE_RELAY_URLS } from '@/constants'
import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { collectWriteOutboxUrlsFromRelayList } from '@/lib/viewer-write-outboxes'
import logger from '@/lib/logger'
import { NEW_USER_HTTP_RELAY_URL } from '@/lib/new-user-template'
import { normalizeAnyRelayUrl } from '@/lib/url'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import type { TRelayList } from '@/types'
import { Event, kinds } from 'nostr-tools'
const BROADCAST_PENDING_KEY = 'imwaldNewUserTemplateBroadcastPending'
/** Space between replaceable template events — keeps relay publish rate limits from tripping. */
export const NEW_USER_TEMPLATE_BROADCAST_INTERVAL_MS = 20_000
/** Replaceable kinds created during one-click signup, in publish order. */
export const NEW_USER_TEMPLATE_BROADCAST_KINDS = [
kinds.RelayList,
ExtendedKind.HTTP_RELAY_LIST,
ExtendedKind.FAVORITE_RELAYS,
ExtendedKind.BLOCKED_RELAYS,
kinds.Metadata,
10015,
kinds.Contacts,
kinds.Mutelist
] as const
/** Relays that reject bursts or return HTTP 429 on connect during signup publish. */
const NEW_USER_TEMPLATE_PUBLISH_EXCLUDED = [
'wss://relay.layer.systems',
'wss://profiles.nostrver.se/'
] as const
/** Profile mirrors that only mirror kind 10002 (not kind 0 or other lists). */
const RELAY_LIST_ONLY_PROFILE_MIRRORS = ['wss://indexer.coracle.social/'] as const
const broadcastScheduledOrRunning = new Set<string>()
function templateRelayKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url).toLowerCase()
}
function isExcludedFromTemplateBroadcast(url: string): boolean {
const key = templateRelayKey(url)
return NEW_USER_TEMPLATE_PUBLISH_EXCLUDED.some((u) => templateRelayKey(u) === key)
}
function relayAllowsTemplateKind(url: string, kind: number): boolean {
if (kind === kinds.RelayList) return true
const key = templateRelayKey(url)
return !RELAY_LIST_ONLY_PROFILE_MIRRORS.some((u) => templateRelayKey(u) === key)
}
function maxTemplatePublishRelays(kind: number): number {
return kind === kinds.Metadata || kind === kinds.RelayList ? 4 : 3
}
/** Prefer mercury + stable write relays; profile index when kind allows. */
function prioritizeNewUserTemplateRelays(urls: string[]): string[] {
const preferredOrder = [
NEW_USER_HTTP_RELAY_URL,
'wss://profiles.nostr1.com',
'wss://nos.lol',
'wss://relay.primal.net',
'wss://relay.damus.io',
'wss://thecitadel.nostr1.com'
]
const byKey = new Map(urls.map((u) => [templateRelayKey(u), u]))
const ordered: string[] = []
for (const pref of preferredOrder) {
const u = byKey.get(templateRelayKey(pref))
if (u) {
ordered.push(u)
byKey.delete(templateRelayKey(u))
}
}
for (const u of urls) {
const k = templateRelayKey(u)
if (byKey.has(k)) {
ordered.push(u)
byKey.delete(k)
}
}
return ordered
}
export function markNewUserTemplateBroadcastPending(pubkey: string): void {
if (typeof sessionStorage === 'undefined') return
sessionStorage.setItem(BROADCAST_PENDING_KEY, pubkey)
}
function consumeBroadcastPending(pubkey: string): boolean {
if (typeof sessionStorage === 'undefined') return false
if (sessionStorage.getItem(BROADCAST_PENDING_KEY) !== pubkey) return false
sessionStorage.removeItem(BROADCAST_PENDING_KEY)
return true
}
/** Write outboxes from the stored template plus profile index relays where the kind allows it. */
export function newUserTemplatePublishRelays(kind: number, relayList: TRelayList): string[] {
const write = collectWriteOutboxUrlsFromRelayList(relayList)
const merged =
kind === kinds.Metadata || kind === kinds.RelayList
? dedupeNormalizeRelayUrlsOrdered([...write, ...PROFILE_RELAY_URLS])
: write
const filtered = filterRelaysForEventPublish(merged, kind)
.filter((u) => !isExcludedFromTemplateBroadcast(u))
.filter((u) => relayAllowsTemplateKind(u, kind))
return prioritizeNewUserTemplateRelays(filtered).slice(0, maxTemplatePublishRelays(kind))
}
async function loadRelayListForPublish(pubkey: string): Promise<TRelayList> {
const peeked = await client.peekRelayListFromStorage(pubkey)
if (peeked.write.length > 0 || peeked.httpWrite.length > 0) {
return peeked
}
const [relayListEvent, httpRelayListEvent] = await Promise.all([
indexedDb.getReplaceableEvent(pubkey, kinds.RelayList),
indexedDb.getReplaceableEvent(pubkey, ExtendedKind.HTTP_RELAY_LIST)
])
const emptyHttp = {
httpRead: [] as string[],
httpWrite: [] as string[],
httpOriginalRelays: [] as TRelayList['httpOriginalRelays']
}
let base: TRelayList = relayListEvent
? getRelayListFromEvent(relayListEvent, [])
: { write: [], read: [], originalRelays: [], ...emptyHttp }
if (httpRelayListEvent) {
const http = getHttpRelayListFromEvent(httpRelayListEvent, [])
base = {
...base,
httpRead: http.httpRead,
httpWrite: http.httpWrite,
httpOriginalRelays: http.httpOriginalRelays
}
}
return base
}
async function broadcastNewUserTemplateFromStorage(pubkey: string): Promise<void> {
const relayList = await loadRelayListForPublish(pubkey)
for (let i = 0; i < NEW_USER_TEMPLATE_BROADCAST_KINDS.length; i++) {
const kind = NEW_USER_TEMPLATE_BROADCAST_KINDS[i]
const event = (await indexedDb.getReplaceableEvent(pubkey, kind)) as Event | undefined
if (!event) continue
const relays = newUserTemplatePublishRelays(kind, relayList)
if (relays.length === 0) continue
try {
await client.publishEvent(relays, event, {
skipOutboxRetry: true,
publishBatchLabel: 'new user template broadcast'
})
} catch (error) {
logger.warn('[newUserTemplateBroadcast] publish failed', { kind, error })
}
if (i < NEW_USER_TEMPLATE_BROADCAST_KINDS.length - 1) {
await new Promise((resolve) => setTimeout(resolve, NEW_USER_TEMPLATE_BROADCAST_INTERVAL_MS))
}
}
}
/**
* After the user dismisses the backup banner or leaves cache settings, broadcast locally stored
* template events to their write outboxes and profile relays (spaced to avoid relay rate limits).
*/
export function requestNewUserTemplateBroadcast(pubkey: string): void {
if (!pubkey || broadcastScheduledOrRunning.has(pubkey)) return
if (typeof sessionStorage === 'undefined') return
if (sessionStorage.getItem(BROADCAST_PENDING_KEY) !== pubkey) return
broadcastScheduledOrRunning.add(pubkey)
void (async () => {
try {
if (!consumeBroadcastPending(pubkey)) return
await broadcastNewUserTemplateFromStorage(pubkey)
} catch (error) {
logger.error('[newUserTemplateBroadcast] failed', { error })
} finally {
broadcastScheduledOrRunning.delete(pubkey)
}
})()
}