feat: NIP-10

This commit is contained in:
codytseng
2025-03-12 17:23:01 +08:00
parent 5f7a99c7b5
commit 4ad96eab66
6 changed files with 309 additions and 212 deletions

View File

@@ -61,8 +61,10 @@ export async function createShortTextNoteDraftEvent(
protectedEvent?: boolean
} = {}
): Promise<TDraftEvent> {
const { otherRelatedEventIds, quoteEventIds, rootEventId, parentEventId } =
await extractRelatedEventIds(content, options.parentEvent)
const { quoteEventIds, rootETag, parentETag } = await extractRelatedEventIds(
content,
options.parentEvent
)
const hashtags = extractHashtags(content)
const tags = hashtags.map((hashtag) => ['t', hashtag])
@@ -77,14 +79,12 @@ export async function createShortTextNoteDraftEvent(
tags.push(...quoteEventIds.map((eventId) => ['q', eventId, client.getEventHint(eventId)]))
// e tags
if (rootEventId) {
tags.push(['e', rootEventId, client.getEventHint(rootEventId), 'root'])
if (rootETag.length) {
tags.push(rootETag)
}
tags.push(...otherRelatedEventIds.map((eventId) => ['e', eventId, client.getEventHint(eventId)]))
if (parentEventId) {
tags.push(['e', parentEventId, client.getEventHint(parentEventId), 'reply'])
if (parentETag.length) {
tags.push(parentETag)
}
// p tags

View File

@@ -5,7 +5,13 @@ import { LRUCache } from 'lru-cache'
import { Event, kinds, nip19 } from 'nostr-tools'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey } from './pubkey'
import { extractImageInfoFromTag, isReplyETag, isRootETag, tagNameEquals } from './tag'
import {
extractImageInfoFromTag,
generateEventIdFromETag,
isReplyETag,
isRootETag,
tagNameEquals
} from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
const EVENT_EMBEDDED_EVENT_IDS_CACHE = new LRUCache<string, string[]>({ max: 10000 })
@@ -56,23 +62,28 @@ export function isSupportedKind(kind: number) {
return [kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(kind)
}
export function getParentEventId(event?: Event) {
export function getParentEventTag(event?: Event) {
if (!event) return undefined
let tag = event.tags.find(isReplyETag)
if (!tag) {
const embeddedEventIds = extractEmbeddedEventIds(event)
tag = event.tags.findLast(
([tagName, tagValue]) => tagName === 'e' && !embeddedEventIds.includes(tagValue)
([tagName, tagValue]) => tagName === 'e' && !!tagValue && !embeddedEventIds.includes(tagValue)
)
}
return tag
}
export function getParentEventHexId(event?: Event) {
const tag = getParentEventTag(event)
return tag?.[1]
}
export function getParentEventId(event?: Event) {
const tag = getParentEventTag(event)
if (!tag) return undefined
try {
const [, id, relay, , author] = tag
return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author })
} catch {
return undefined
}
return generateEventIdFromETag(tag)
}
export function getRootEventTag(event?: Event) {
@@ -81,22 +92,22 @@ export function getRootEventTag(event?: Event) {
if (!tag) {
const embeddedEventIds = extractEmbeddedEventIds(event)
tag = event.tags.find(
([tagName, tagValue]) => tagName === 'e' && !embeddedEventIds.includes(tagValue)
([tagName, tagValue]) => tagName === 'e' && !!tagValue && !embeddedEventIds.includes(tagValue)
)
}
return tag
}
export function getRootEventHexId(event?: Event) {
const tag = getRootEventTag(event)
return tag?.[1]
}
export function getRootEventId(event?: Event) {
const tag = getRootEventTag(event)
if (!tag) return undefined
try {
const [, id, relay, , author] = tag
return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author })
} catch {
return undefined
}
return generateEventIdFromETag(tag)
}
export function isReplaceable(kind: number) {
@@ -231,21 +242,15 @@ export async function extractMentions(content: string, parentEvent?: Event) {
}
export async function extractRelatedEventIds(content: string, parentEvent?: Event) {
const relatedEventIds: string[] = []
const quoteEventIds: string[] = []
let rootEventId: string | undefined
let parentEventId: string | undefined
let rootETag: string[] = []
let parentETag: string[] = []
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g)
const addToSet = (arr: string[], item: string) => {
if (!arr.includes(item)) arr.push(item)
}
const removeFromSet = (arr: string[], item: string) => {
const index = arr.indexOf(item)
if (index !== -1) arr.splice(index, 1)
}
for (const m of matches || []) {
try {
const id = m.split(':')[1]
@@ -261,32 +266,48 @@ export async function extractRelatedEventIds(content: string, parentEvent?: Even
}
if (parentEvent) {
addToSet(relatedEventIds, parentEvent.id)
parentEvent.tags.forEach((tag) => {
if (isRootETag(tag)) {
rootEventId = tag[1]
} else if (tagNameEquals('e')(tag)) {
addToSet(relatedEventIds, tag[1])
const rootEventTag = getRootEventTag(parentEvent)
if (rootEventTag) {
parentETag = [
'e',
parentEvent.id,
client.getEventHint(parentEvent.id),
'reply',
parentEvent.pubkey
]
const [, rootEventHexId, hint, , rootEventPubkey] = rootEventTag
if (rootEventPubkey) {
rootETag = [
'e',
rootEventHexId,
hint ?? client.getEventHint(rootEventHexId),
'root',
rootEventPubkey
]
} else {
const rootEventId = generateEventIdFromETag(rootEventTag)
const rootEvent = rootEventId ? await client.fetchEvent(rootEventId) : undefined
rootETag = rootEvent
? ['e', rootEvent.id, hint ?? client.getEventHint(rootEvent.id), 'root', rootEvent.pubkey]
: ['e', rootEventHexId, hint ?? client.getEventHint(rootEventHexId), 'root']
}
})
if (rootEventId || isReplyNoteEvent(parentEvent)) {
parentEventId = parentEvent.id
} else {
rootEventId = parentEvent.id
// reply to root event
rootETag = [
'e',
parentEvent.id,
client.getEventHint(parentEvent.id),
'root',
parentEvent.pubkey
]
}
}
if (rootEventId) {
removeFromSet(relatedEventIds, rootEventId)
}
if (parentEventId) {
removeFromSet(relatedEventIds, parentEventId)
}
return {
otherRelatedEventIds: relatedEventIds,
quoteEventIds,
rootEventId,
parentEventId
rootETag,
parentETag
}
}

View File

@@ -1,5 +1,6 @@
import { TImageInfo } from '@/types'
import { isBlurhashValid } from 'blurhash'
import { nip19 } from 'nostr-tools'
import { isValidPubkey } from './pubkey'
export function tagNameEquals(tagName: string) {
@@ -18,6 +19,15 @@ export function isMentionETag([tagName, , , marker]: string[]) {
return tagName === 'e' && marker === 'mention'
}
export function generateEventIdFromETag(tag: string[]) {
try {
const [, id, relay, , author] = tag
return nip19.neventEncode({ id, relays: relay ? [relay] : undefined, author })
} catch {
return undefined
}
}
export function extractImageInfoFromTag(tag: string[]): TImageInfo | null {
if (tag[0] !== 'imeta') return null
const urlItem = tag.find((item) => item.startsWith('url '))