This commit is contained in:
codytseng
2025-07-19 17:24:51 +08:00
parent 28ec943a52
commit 78725c1d14
45 changed files with 698 additions and 766 deletions

View File

@@ -3,16 +3,14 @@ import client from '@/services/client.service'
import mediaUpload from '@/services/media-upload.service'
import { TDraftEvent, TEmoji, TMailboxRelay, TRelaySet } from '@/types'
import dayjs from 'dayjs'
import { Event, kinds } from 'nostr-tools'
import { Event, kinds, nip19 } from 'nostr-tools'
import {
extractCommentMentions,
extractHashtags,
extractImagesFromContent,
extractRelatedEventIds,
getEventCoordinate,
getReplaceableEventCoordinate,
getRootETag,
isProtectedEvent,
isReplaceable
isReplaceableEvent
} from './event'
import { generateBech32IdFromETag, tagNameEquals } from './tag'
import { normalizeHttpUrl } from './url'
// https://github.com/nostr-protocol/nips/blob/master/25.md
@@ -25,8 +23,12 @@ export function createReactionDraftEvent(event: Event, emoji: TEmoji | string =
tags.push(['k', event.kind.toString()])
}
if (isReplaceable(event.kind)) {
tags.push(hint ? ['a', getEventCoordinate(event), hint] : ['a', getEventCoordinate(event)])
if (isReplaceableEvent(event.kind)) {
tags.push(
hint
? ['a', getReplaceableEventCoordinate(event), hint]
: ['a', getReplaceableEventCoordinate(event)]
)
}
let content: string
@@ -81,7 +83,7 @@ export async function createShortTextNoteDraftEvent(
const tags = hashtags.map((hashtag) => ['t', hashtag])
// imeta tags
const { images } = extractImagesFromContent(content)
const images = extractImagesFromContent(content)
if (images && images.length) {
tags.push(...generateImetaTags(images))
}
@@ -209,7 +211,7 @@ export async function createCommentDraftEvent(
.map((hashtag) => ['t', hashtag])
.concat(quoteEventIds.map((eventId) => ['q', eventId, client.getEventHint(eventId)]))
const { images } = extractImagesFromContent(content)
const images = extractImagesFromContent(content)
if (images && images.length) {
tags.push(...generateImetaTags(images))
}
@@ -319,7 +321,7 @@ export function createFavoriteRelaysDraftEvent(
tags.push(['relay', url])
})
relaySetEvents.forEach((event) => {
tags.push(['a', getEventCoordinate(event)])
tags.push(['a', getReplaceableEventCoordinate(event)])
})
return {
kind: ExtendedKind.FAVORITE_RELAYS,
@@ -364,3 +366,155 @@ function generateImetaTags(imageUrls: string[]) {
})
.filter(Boolean) as string[][]
}
async function extractRelatedEventIds(content: string, parentEvent?: Event) {
const quoteEventIds: string[] = []
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)
}
for (const m of matches || []) {
try {
const id = m.split(':')[1]
const { type, data } = nip19.decode(id)
if (type === 'nevent') {
addToSet(quoteEventIds, data.id)
} else if (type === 'note') {
addToSet(quoteEventIds, data)
}
} catch (e) {
console.error(e)
}
}
if (parentEvent) {
const _rootETag = getRootETag(parentEvent)
if (_rootETag) {
parentETag = [
'e',
parentEvent.id,
client.getEventHint(parentEvent.id),
'reply',
parentEvent.pubkey
]
const [, rootEventHexId, hint, , rootEventPubkey] = _rootETag
if (rootEventPubkey) {
rootETag = [
'e',
rootEventHexId,
hint ?? client.getEventHint(rootEventHexId),
'root',
rootEventPubkey
]
} else {
const rootEventId = generateBech32IdFromETag(_rootETag)
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']
}
} else {
// reply to root event
rootETag = [
'e',
parentEvent.id,
client.getEventHint(parentEvent.id),
'root',
parentEvent.pubkey
]
}
}
return {
quoteEventIds,
rootETag,
parentETag
}
}
async function extractCommentMentions(content: string, parentEvent: Event) {
const quoteEventIds: string[] = []
const parentEventIsReplaceable = isReplaceableEvent(parentEvent.kind)
const rootCoordinateTag =
parentEvent.kind === ExtendedKind.COMMENT
? parentEvent.tags.find(tagNameEquals('A'))
: isReplaceableEvent(parentEvent.kind)
? ['A', getReplaceableEventCoordinate(parentEvent), client.getEventHint(parentEvent.id)]
: undefined
const rootEventId =
parentEvent.kind === ExtendedKind.COMMENT
? parentEvent.tags.find(tagNameEquals('E'))?.[1]
: parentEvent.id
const rootKind =
parentEvent.kind === ExtendedKind.COMMENT
? parentEvent.tags.find(tagNameEquals('K'))?.[1]
: parentEvent.kind
const rootPubkey =
parentEvent.kind === ExtendedKind.COMMENT
? parentEvent.tags.find(tagNameEquals('P'))?.[1]
: parentEvent.pubkey
const rootUrl =
parentEvent.kind === ExtendedKind.COMMENT
? parentEvent.tags.find(tagNameEquals('I'))?.[1]
: undefined
const parentEventId = parentEvent.id
const parentCoordinate = parentEventIsReplaceable
? getReplaceableEventCoordinate(parentEvent)
: undefined
const parentKind = parentEvent.kind
const parentPubkey = parentEvent.pubkey
const addToSet = (arr: string[], item: string) => {
if (!arr.includes(item)) arr.push(item)
}
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g)
for (const m of matches || []) {
try {
const id = m.split(':')[1]
const { type, data } = nip19.decode(id)
if (type === 'nevent') {
addToSet(quoteEventIds, data.id)
} else if (type === 'note') {
addToSet(quoteEventIds, data)
}
} catch (e) {
console.error(e)
}
}
return {
quoteEventIds,
rootEventId,
rootCoordinateTag,
rootKind,
rootPubkey,
rootUrl,
parentEventId,
parentCoordinate,
parentKind,
parentPubkey
}
}
function extractHashtags(content: string) {
const hashtags: string[] = []
const matches = content.match(/#[\p{L}\p{N}\p{M}]+/gu)
matches?.forEach((m) => {
const hashtag = m.slice(1).toLowerCase()
if (hashtag) {
hashtags.push(hashtag)
}
})
return hashtags
}
function extractImagesFromContent(content: string) {
return content.match(/https?:\/\/[^\s"']+\.(jpg|jpeg|png|gif|webp|heic)/gi)
}