Files
smesh/src/lib/draft-event.ts
2025-04-18 21:51:36 +08:00

303 lines
7.5 KiB
TypeScript

import { ApplicationDataKey, ExtendedKind } from '@/constants'
import client from '@/services/client.service'
import { TDraftEvent, TMailboxRelay, TRelaySet } from '@/types'
import dayjs from 'dayjs'
import { Event, kinds } from 'nostr-tools'
import {
extractCommentMentions,
extractHashtags,
extractImagesFromContent,
extractRelatedEventIds,
getEventCoordinate,
isProtectedEvent,
isReplaceable
} from './event'
// https://github.com/nostr-protocol/nips/blob/master/25.md
export function createReactionDraftEvent(event: Event): TDraftEvent {
const tags: string[][] = []
const hint = client.getEventHint(event.id)
tags.push(['e', event.id, hint, event.pubkey])
tags.push(['p', event.pubkey])
if (event.kind !== kinds.ShortTextNote) {
tags.push(['k', event.kind.toString()])
}
if (isReplaceable(event.kind)) {
tags.push(hint ? ['a', getEventCoordinate(event), hint] : ['a', getEventCoordinate(event)])
}
return {
kind: kinds.Reaction,
content: '+',
tags,
created_at: dayjs().unix()
}
}
// https://github.com/nostr-protocol/nips/blob/master/18.md
export function createRepostDraftEvent(event: Event): TDraftEvent {
const isProtected = isProtectedEvent(event)
const tags = [
['e', event.id, client.getEventHint(event.id), '', event.pubkey],
['p', event.pubkey]
]
return {
kind: kinds.Repost,
content: isProtected ? '' : JSON.stringify(event),
tags,
created_at: dayjs().unix()
}
}
export async function createShortTextNoteDraftEvent(
content: string,
pictureInfos: { url: string; tags: string[][] }[],
mentions: string[],
options: {
parentEvent?: Event
addClientTag?: boolean
protectedEvent?: boolean
} = {}
): Promise<TDraftEvent> {
const { quoteEventIds, rootETag, parentETag } = await extractRelatedEventIds(
content,
options.parentEvent
)
const hashtags = extractHashtags(content)
const tags = hashtags.map((hashtag) => ['t', hashtag])
// imeta tags
const { images } = extractImagesFromContent(content)
if (images && images.length) {
tags.push(...generateImetaTags(images, pictureInfos))
}
// q tags
tags.push(...quoteEventIds.map((eventId) => ['q', eventId, client.getEventHint(eventId)]))
// e tags
if (rootETag.length) {
tags.push(rootETag)
}
if (parentETag.length) {
tags.push(parentETag)
}
// p tags
tags.push(...mentions.map((pubkey) => ['p', pubkey]))
if (options.addClientTag) {
tags.push(['client', 'jumble'])
}
if (options.protectedEvent) {
tags.push(['-'])
}
return {
kind: kinds.ShortTextNote,
content,
tags,
created_at: dayjs().unix()
}
}
// https://github.com/nostr-protocol/nips/blob/master/51.md
export function createRelaySetDraftEvent(relaySet: TRelaySet): TDraftEvent {
return {
kind: kinds.Relaysets,
content: '',
tags: [
['d', relaySet.id],
['title', relaySet.name],
...relaySet.relayUrls.map((url) => ['relay', url])
],
created_at: dayjs().unix()
}
}
export async function createPictureNoteDraftEvent(
content: string,
pictureInfos: { url: string; tags: string[][] }[],
mentions: string[],
options: {
addClientTag?: boolean
protectedEvent?: boolean
} = {}
): Promise<TDraftEvent> {
const { quoteEventIds } = await extractRelatedEventIds(content)
const hashtags = extractHashtags(content)
if (!pictureInfos.length) {
throw new Error('No images found in content')
}
const tags = pictureInfos
.map((info) => ['imeta', ...info.tags.map(([n, v]) => `${n} ${v}`)])
.concat(hashtags.map((hashtag) => ['t', hashtag]))
.concat(quoteEventIds.map((eventId) => ['q', eventId, client.getEventHint(eventId)]))
.concat(mentions.map((pubkey) => ['p', pubkey]))
if (options.addClientTag) {
tags.push(['client', 'jumble'])
}
if (options.protectedEvent) {
tags.push(['-'])
}
return {
kind: ExtendedKind.PICTURE,
content,
tags,
created_at: dayjs().unix()
}
}
export async function createCommentDraftEvent(
content: string,
parentEvent: Event,
pictureInfos: { url: string; tags: string[][] }[],
mentions: string[],
options: {
addClientTag?: boolean
protectedEvent?: boolean
} = {}
): Promise<TDraftEvent> {
const {
quoteEventIds,
rootEventId,
rootEventKind,
rootEventPubkey,
parentEventId,
parentEventKind,
parentEventPubkey
} = await extractCommentMentions(content, parentEvent)
const hashtags = extractHashtags(content)
const tags = hashtags
.map((hashtag) => ['t', hashtag])
.concat(quoteEventIds.map((eventId) => ['q', eventId, client.getEventHint(eventId)]))
const { images } = extractImagesFromContent(content)
if (images && images.length) {
tags.push(...generateImetaTags(images, pictureInfos))
}
tags.push(
...mentions.filter((pubkey) => pubkey !== parentEventPubkey).map((pubkey) => ['p', pubkey])
)
tags.push(
...[
['E', rootEventId, client.getEventHint(rootEventId), rootEventPubkey],
['K', rootEventKind.toString()],
['P', rootEventPubkey],
['e', parentEventId, client.getEventHint(parentEventId), parentEventPubkey],
['k', parentEventKind.toString()],
['p', parentEventPubkey]
]
)
if (options.addClientTag) {
tags.push(['client', 'jumble'])
}
if (options.protectedEvent) {
tags.push(['-'])
}
return {
kind: ExtendedKind.COMMENT,
content,
tags,
created_at: dayjs().unix()
}
}
export function createRelayListDraftEvent(mailboxRelays: TMailboxRelay[]): TDraftEvent {
return {
kind: kinds.RelayList,
content: '',
tags: mailboxRelays.map(({ url, scope }) =>
scope === 'both' ? ['r', url] : ['r', url, scope]
),
created_at: dayjs().unix()
}
}
export function createFollowListDraftEvent(tags: string[][], content?: string): TDraftEvent {
return {
kind: kinds.Contacts,
content: content ?? '',
created_at: dayjs().unix(),
tags
}
}
export function createMuteListDraftEvent(tags: string[][], content?: string): TDraftEvent {
return {
kind: kinds.Mutelist,
content: content ?? '',
created_at: dayjs().unix(),
tags
}
}
export function createProfileDraftEvent(content: string, tags: string[][] = []): TDraftEvent {
return {
kind: kinds.Metadata,
content,
tags,
created_at: dayjs().unix()
}
}
export function createFavoriteRelaysDraftEvent(
favoriteRelays: string[],
relaySetEvents: Event[]
): TDraftEvent {
const tags: string[][] = []
favoriteRelays.forEach((url) => {
tags.push(['relay', url])
})
relaySetEvents.forEach((event) => {
tags.push(['a', getEventCoordinate(event)])
})
return {
kind: ExtendedKind.FAVORITE_RELAYS,
content: '',
tags,
created_at: dayjs().unix()
}
}
export function createSeenNotificationsAtDraftEvent(): TDraftEvent {
return {
kind: kinds.Application,
content: 'Records read time to sync notification status across devices.',
tags: [['d', ApplicationDataKey.NOTIFICATIONS_SEEN_AT]],
created_at: dayjs().unix()
}
}
export function createBookmarkDraftEvent(tags: string[][]): TDraftEvent {
return {
kind: kinds.BookmarkList,
content: '',
tags,
created_at: dayjs().unix()
}
}
function generateImetaTags(imageUrls: string[], pictureInfos: { url: string; tags: string[][] }[]) {
return imageUrls.map((imageUrl) => {
const pictureInfo = pictureInfos.find((info) => info.url === imageUrl)
return pictureInfo
? ['imeta', ...pictureInfo.tags.map(([n, v]) => `${n} ${v}`)]
: ['imeta', `url ${imageUrl}`]
})
}