2023-08-08 18:19:31 +00:00
|
|
|
import { bech32 } from 'bech32'
|
|
|
|
|
2023-02-08 19:38:04 +00:00
|
|
|
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
|
|
|
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
|
|
|
export const NOSTR_MAX_RELAY_NUM = 20
|
2023-08-08 18:19:31 +00:00
|
|
|
export const NOSTR_ZAPPLE_PAY_NPUB = 'npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan'
|
2023-10-04 18:47:09 +00:00
|
|
|
export const DEFAULT_CROSSPOSTING_RELAYS = [
|
|
|
|
'wss://nostrue.com/',
|
|
|
|
'wss://relay.damus.io/',
|
|
|
|
'wss://relay.nostr.band/',
|
|
|
|
'wss://relay.snort.social/',
|
|
|
|
'wss://nostr21.com/'
|
|
|
|
]
|
2023-08-08 18:19:31 +00:00
|
|
|
|
|
|
|
export function hexToBech32 (hex, prefix = 'npub') {
|
|
|
|
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
|
|
|
}
|
|
|
|
|
|
|
|
export function nostrZapDetails (zap) {
|
|
|
|
let { pubkey, content, tags } = zap
|
|
|
|
let npub = hexToBech32(pubkey)
|
|
|
|
if (npub === NOSTR_ZAPPLE_PAY_NPUB) {
|
|
|
|
const znpub = content.match(/^From: nostr:(npub1[02-9ac-hj-np-z]+)$/)?.[1]
|
|
|
|
if (znpub) {
|
|
|
|
npub = znpub
|
|
|
|
// zapple pay does not support user content
|
|
|
|
content = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const event = tags.filter(t => t?.length >= 2 && t[0] === 'e')?.[0]?.[1]
|
|
|
|
const note = event ? hexToBech32(event, 'note') : null
|
|
|
|
|
|
|
|
return { npub, content, note }
|
|
|
|
}
|
2023-10-04 18:47:09 +00:00
|
|
|
|
|
|
|
async function publishNostrEvent (signedEvent, relay) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const timeout = 1000
|
|
|
|
const wsRelay = new window.WebSocket(relay)
|
|
|
|
let timer
|
|
|
|
let isMessageSentSuccessfully = false
|
|
|
|
|
|
|
|
function timedout () {
|
|
|
|
clearTimeout(timer)
|
|
|
|
wsRelay.close()
|
|
|
|
reject(new Error(`relay timeout for ${relay}`))
|
|
|
|
}
|
|
|
|
|
|
|
|
timer = setTimeout(timedout, timeout)
|
|
|
|
|
|
|
|
wsRelay.onopen = function () {
|
|
|
|
clearTimeout(timer)
|
|
|
|
timer = setTimeout(timedout, timeout)
|
|
|
|
wsRelay.send(JSON.stringify(['EVENT', signedEvent]))
|
|
|
|
}
|
|
|
|
|
|
|
|
wsRelay.onmessage = function (msg) {
|
|
|
|
const m = JSON.parse(msg.data)
|
|
|
|
if (m[0] === 'OK') {
|
|
|
|
isMessageSentSuccessfully = true
|
|
|
|
clearTimeout(timer)
|
|
|
|
wsRelay.close()
|
|
|
|
console.log('Successfully sent event to', relay)
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
wsRelay.onerror = function (error) {
|
|
|
|
clearTimeout(timer)
|
|
|
|
console.log(error)
|
|
|
|
reject(new Error(`relay error: Failed to send to ${relay}`))
|
|
|
|
}
|
|
|
|
|
|
|
|
wsRelay.onclose = function () {
|
|
|
|
clearTimeout(timer)
|
|
|
|
if (!isMessageSentSuccessfully) {
|
|
|
|
reject(new Error(`relay error: Failed to send to ${relay}`))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
|
|
|
|
try {
|
2023-12-19 22:01:48 +00:00
|
|
|
const signedEvent = await callWithTimeout(() => window.nostr.signEvent(event), 5000)
|
2023-10-04 18:47:09 +00:00
|
|
|
if (!signedEvent) throw new Error('failed to sign event')
|
|
|
|
|
|
|
|
const promises = relays.map(r => publishNostrEvent(signedEvent, r))
|
|
|
|
const results = await Promise.allSettled(promises)
|
|
|
|
const successfulRelays = []
|
|
|
|
const failedRelays = []
|
|
|
|
|
|
|
|
results.forEach((result, index) => {
|
|
|
|
if (result.status === 'fulfilled') {
|
|
|
|
successfulRelays.push(relays[index])
|
|
|
|
} else {
|
|
|
|
failedRelays.push({ relay: relays[index], error: result.reason })
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-12-19 17:48:48 +00:00
|
|
|
const noteId = hexToBech32(signedEvent.id, 'note')
|
2023-10-04 18:47:09 +00:00
|
|
|
|
2023-12-19 17:48:48 +00:00
|
|
|
return { successfulRelays, failedRelays, noteId }
|
2023-10-04 18:47:09 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.error('Crosspost discussion error:', error)
|
|
|
|
return { error }
|
|
|
|
}
|
|
|
|
}
|
2023-12-19 22:01:48 +00:00
|
|
|
|
|
|
|
export function callWithTimeout (targetFunction, timeoutMs) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
Promise.race([
|
|
|
|
targetFunction(),
|
|
|
|
new Promise((resolve, reject) => setTimeout(() => reject(new Error('timeouted after ' + timeoutMs + ' ms waiting for extension')), timeoutMs))
|
|
|
|
]).then(resolve)
|
|
|
|
.catch(reject)
|
|
|
|
})
|
|
|
|
}
|