694 lines
18 KiB
TypeScript
694 lines
18 KiB
TypeScript
import { ApplicationDataKey, EMBEDDED_EVENT_REGEX, ExtendedKind, POLL_TYPE } from '@/constants'
|
|
import client from '@/services/client.service'
|
|
import customEmojiService from '@/services/custom-emoji.service'
|
|
import mediaUpload from '@/services/media-upload.service'
|
|
import {
|
|
TDraftEvent,
|
|
TEmoji,
|
|
TMailboxRelay,
|
|
TMailboxRelayScope,
|
|
TPollCreateData,
|
|
TRelaySet
|
|
} from '@/types'
|
|
import dayjs from 'dayjs'
|
|
import { Event, kinds, nip19 } from 'nostr-tools'
|
|
import {
|
|
getReplaceableCoordinate,
|
|
getReplaceableCoordinateFromEvent,
|
|
getRootETag,
|
|
isProtectedEvent,
|
|
isReplaceableEvent
|
|
} from './event'
|
|
import { randomString } from './random'
|
|
import { generateBech32IdFromETag, tagNameEquals } from './tag'
|
|
|
|
// https://github.com/nostr-protocol/nips/blob/master/25.md
|
|
export function createReactionDraftEvent(event: Event, emoji: TEmoji | string = '+'): TDraftEvent {
|
|
const tags: string[][] = []
|
|
tags.push(buildETag(event.id, event.pubkey))
|
|
tags.push(buildPTag(event.pubkey))
|
|
if (event.kind !== kinds.ShortTextNote) {
|
|
tags.push(buildKTag(event.kind))
|
|
}
|
|
|
|
if (isReplaceableEvent(event.kind)) {
|
|
tags.push(buildATag(event))
|
|
}
|
|
|
|
let content: string
|
|
if (typeof emoji === 'string') {
|
|
content = emoji
|
|
} else {
|
|
content = `:${emoji.shortcode}:`
|
|
tags.push(buildEmojiTag(emoji))
|
|
}
|
|
|
|
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 = [buildETag(event.id, event.pubkey), buildPTag(event.pubkey)]
|
|
|
|
if (isReplaceableEvent(event.kind)) {
|
|
tags.push(buildATag(event))
|
|
}
|
|
|
|
return {
|
|
kind: kinds.Repost,
|
|
content: isProtected ? '' : JSON.stringify(event),
|
|
tags,
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
const shortTextNoteDraftEventCache: Map<string, TDraftEvent> = new Map()
|
|
export async function createShortTextNoteDraftEvent(
|
|
content: string,
|
|
mentions: string[],
|
|
options: {
|
|
parentEvent?: Event
|
|
addClientTag?: boolean
|
|
protectedEvent?: boolean
|
|
isNsfw?: boolean
|
|
} = {}
|
|
): Promise<TDraftEvent> {
|
|
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
|
|
const { quoteEventHexIds, quoteReplaceableCoordinates, rootETag, parentETag } =
|
|
await extractRelatedEventIds(transformedEmojisContent, options.parentEvent)
|
|
const hashtags = extractHashtags(transformedEmojisContent)
|
|
|
|
const tags = emojiTags.concat(hashtags.map((hashtag) => buildTTag(hashtag)))
|
|
|
|
// imeta tags
|
|
const images = extractImagesFromContent(transformedEmojisContent)
|
|
if (images && images.length) {
|
|
tags.push(...generateImetaTags(images))
|
|
}
|
|
|
|
// q tags
|
|
tags.push(...quoteEventHexIds.map((eventId) => buildQTag(eventId)))
|
|
tags.push(...quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
|
|
|
|
// e tags
|
|
if (rootETag.length) {
|
|
tags.push(rootETag)
|
|
}
|
|
|
|
if (parentETag.length) {
|
|
tags.push(parentETag)
|
|
}
|
|
|
|
// p tags
|
|
tags.push(...mentions.map((pubkey) => buildPTag(pubkey)))
|
|
|
|
if (options.addClientTag) {
|
|
tags.push(buildClientTag())
|
|
}
|
|
|
|
if (options.isNsfw) {
|
|
tags.push(buildNsfwTag())
|
|
}
|
|
|
|
if (options.protectedEvent) {
|
|
tags.push(buildProtectedTag())
|
|
}
|
|
|
|
const baseDraft = {
|
|
kind: kinds.ShortTextNote,
|
|
content: transformedEmojisContent,
|
|
tags
|
|
}
|
|
const cacheKey = JSON.stringify(baseDraft)
|
|
const cache = shortTextNoteDraftEventCache.get(cacheKey)
|
|
if (cache) {
|
|
return cache
|
|
}
|
|
const draftEvent = { ...baseDraft, created_at: dayjs().unix() }
|
|
shortTextNoteDraftEventCache.set(cacheKey, draftEvent)
|
|
|
|
return draftEvent
|
|
}
|
|
|
|
// https://github.com/nostr-protocol/nips/blob/master/51.md
|
|
export function createRelaySetDraftEvent(relaySet: Omit<TRelaySet, 'aTag'>): TDraftEvent {
|
|
return {
|
|
kind: kinds.Relaysets,
|
|
content: '',
|
|
tags: [
|
|
buildDTag(relaySet.id),
|
|
buildTitleTag(relaySet.name),
|
|
...relaySet.relayUrls.map((url) => buildRelayTag(url))
|
|
],
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
const commentDraftEventCache: Map<string, TDraftEvent> = new Map()
|
|
export async function createCommentDraftEvent(
|
|
content: string,
|
|
parentEvent: Event,
|
|
mentions: string[],
|
|
options: {
|
|
addClientTag?: boolean
|
|
protectedEvent?: boolean
|
|
isNsfw?: boolean
|
|
} = {}
|
|
): Promise<TDraftEvent> {
|
|
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
|
|
const {
|
|
quoteEventHexIds,
|
|
quoteReplaceableCoordinates,
|
|
rootEventId,
|
|
rootCoordinateTag,
|
|
rootKind,
|
|
rootPubkey,
|
|
rootUrl
|
|
} = await extractCommentMentions(transformedEmojisContent, parentEvent)
|
|
const hashtags = extractHashtags(transformedEmojisContent)
|
|
|
|
const tags = emojiTags
|
|
.concat(hashtags.map((hashtag) => buildTTag(hashtag)))
|
|
.concat(quoteEventHexIds.map((eventId) => buildQTag(eventId)))
|
|
.concat(quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
|
|
|
|
const images = extractImagesFromContent(transformedEmojisContent)
|
|
if (images && images.length) {
|
|
tags.push(...generateImetaTags(images))
|
|
}
|
|
|
|
tags.push(
|
|
...mentions.filter((pubkey) => pubkey !== parentEvent.pubkey).map((pubkey) => buildPTag(pubkey))
|
|
)
|
|
|
|
if (rootCoordinateTag) {
|
|
tags.push(rootCoordinateTag)
|
|
} else if (rootEventId) {
|
|
tags.push(buildETag(rootEventId, rootPubkey, '', true))
|
|
}
|
|
if (rootPubkey) {
|
|
tags.push(buildPTag(rootPubkey, true))
|
|
}
|
|
if (rootKind) {
|
|
tags.push(buildKTag(rootKind, true))
|
|
}
|
|
if (rootUrl) {
|
|
tags.push(buildITag(rootUrl, true))
|
|
}
|
|
tags.push(
|
|
...[
|
|
isReplaceableEvent(parentEvent.kind)
|
|
? buildATag(parentEvent)
|
|
: buildETag(parentEvent.id, parentEvent.pubkey),
|
|
buildKTag(parentEvent.kind),
|
|
buildPTag(parentEvent.pubkey)
|
|
]
|
|
)
|
|
|
|
if (options.addClientTag) {
|
|
tags.push(buildClientTag())
|
|
}
|
|
|
|
if (options.isNsfw) {
|
|
tags.push(buildNsfwTag())
|
|
}
|
|
|
|
if (options.protectedEvent) {
|
|
tags.push(buildProtectedTag())
|
|
}
|
|
|
|
const baseDraft = {
|
|
kind: ExtendedKind.COMMENT,
|
|
content: transformedEmojisContent,
|
|
tags
|
|
}
|
|
const cacheKey = JSON.stringify(baseDraft)
|
|
const cache = commentDraftEventCache.get(cacheKey)
|
|
if (cache) {
|
|
return cache
|
|
}
|
|
const draftEvent = { ...baseDraft, created_at: dayjs().unix() }
|
|
commentDraftEventCache.set(cacheKey, draftEvent)
|
|
|
|
return draftEvent
|
|
}
|
|
|
|
export function createRelayListDraftEvent(mailboxRelays: TMailboxRelay[]): TDraftEvent {
|
|
return {
|
|
kind: kinds.RelayList,
|
|
content: '',
|
|
tags: mailboxRelays.map(({ url, scope }) => buildRTag(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[],
|
|
relaySetEventsOrATags: Event[] | string[][]
|
|
): TDraftEvent {
|
|
const tags: string[][] = []
|
|
favoriteRelays.forEach((url) => {
|
|
tags.push(buildRelayTag(url))
|
|
})
|
|
relaySetEventsOrATags.forEach((eventOrATag) => {
|
|
if (Array.isArray(eventOrATag)) {
|
|
tags.push(eventOrATag)
|
|
} else {
|
|
tags.push(buildATag(eventOrATag))
|
|
}
|
|
})
|
|
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: [buildDTag(ApplicationDataKey.NOTIFICATIONS_SEEN_AT)],
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
export function createBookmarkDraftEvent(tags: string[][], content = ''): TDraftEvent {
|
|
return {
|
|
kind: kinds.BookmarkList,
|
|
content,
|
|
tags,
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
export function createBlossomServerListDraftEvent(servers: string[]): TDraftEvent {
|
|
return {
|
|
kind: ExtendedKind.BLOSSOM_SERVER_LIST,
|
|
content: '',
|
|
tags: servers.map((server) => buildServerTag(server)),
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
const pollDraftEventCache: Map<string, TDraftEvent> = new Map()
|
|
export async function createPollDraftEvent(
|
|
author: string,
|
|
question: string,
|
|
mentions: string[],
|
|
{ isMultipleChoice, relays, options, endsAt }: TPollCreateData,
|
|
{
|
|
addClientTag,
|
|
isNsfw
|
|
}: {
|
|
addClientTag?: boolean
|
|
isNsfw?: boolean
|
|
} = {}
|
|
): Promise<TDraftEvent> {
|
|
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(question)
|
|
const { quoteEventHexIds, quoteReplaceableCoordinates } =
|
|
await extractRelatedEventIds(transformedEmojisContent)
|
|
const hashtags = extractHashtags(transformedEmojisContent)
|
|
|
|
const tags = emojiTags.concat(hashtags.map((hashtag) => buildTTag(hashtag)))
|
|
|
|
// imeta tags
|
|
const images = extractImagesFromContent(transformedEmojisContent)
|
|
if (images && images.length) {
|
|
tags.push(...generateImetaTags(images))
|
|
}
|
|
|
|
// q tags
|
|
tags.push(...quoteEventHexIds.map((eventId) => buildQTag(eventId)))
|
|
tags.push(...quoteReplaceableCoordinates.map((coordinate) => buildReplaceableQTag(coordinate)))
|
|
|
|
// p tags
|
|
tags.push(...mentions.map((pubkey) => buildPTag(pubkey)))
|
|
|
|
const validOptions = options.filter((opt) => opt.trim())
|
|
tags.push(...validOptions.map((option) => ['option', randomString(9), option.trim()]))
|
|
tags.push(['polltype', isMultipleChoice ? POLL_TYPE.MULTIPLE_CHOICE : POLL_TYPE.SINGLE_CHOICE])
|
|
|
|
if (endsAt) {
|
|
tags.push(['endsAt', endsAt.toString()])
|
|
}
|
|
|
|
if (relays.length) {
|
|
relays.forEach((relay) => tags.push(buildRelayTag(relay)))
|
|
} else {
|
|
const relayList = await client.fetchRelayList(author)
|
|
relayList.read.slice(0, 4).forEach((relay) => {
|
|
tags.push(buildRelayTag(relay))
|
|
})
|
|
}
|
|
|
|
if (addClientTag) {
|
|
tags.push(buildClientTag())
|
|
}
|
|
|
|
if (isNsfw) {
|
|
tags.push(buildNsfwTag())
|
|
}
|
|
|
|
const baseDraft = {
|
|
content: transformedEmojisContent.trim(),
|
|
kind: ExtendedKind.POLL,
|
|
tags
|
|
}
|
|
const cacheKey = JSON.stringify(baseDraft)
|
|
const cache = pollDraftEventCache.get(cacheKey)
|
|
if (cache) {
|
|
return cache
|
|
}
|
|
const draftEvent = { ...baseDraft, created_at: dayjs().unix() }
|
|
pollDraftEventCache.set(cacheKey, draftEvent)
|
|
|
|
return draftEvent
|
|
}
|
|
|
|
export function createPollResponseDraftEvent(
|
|
pollEvent: Event,
|
|
selectedOptionIds: string[]
|
|
): TDraftEvent {
|
|
return {
|
|
content: '',
|
|
kind: ExtendedKind.POLL_RESPONSE,
|
|
tags: [
|
|
buildETag(pollEvent.id, pollEvent.pubkey),
|
|
buildPTag(pollEvent.pubkey),
|
|
...selectedOptionIds.map((optionId) => buildResponseTag(optionId))
|
|
],
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
export function createDeletionRequestDraftEvent(event: Event): TDraftEvent {
|
|
const tags: string[][] = [buildKTag(event.kind)]
|
|
if (isReplaceableEvent(event.kind)) {
|
|
tags.push(['a', getReplaceableCoordinateFromEvent(event)])
|
|
} else {
|
|
tags.push(['e', event.id])
|
|
}
|
|
|
|
return {
|
|
kind: kinds.EventDeletion,
|
|
content: 'Request for deletion of the event.',
|
|
tags,
|
|
created_at: dayjs().unix()
|
|
}
|
|
}
|
|
|
|
function generateImetaTags(imageUrls: string[]) {
|
|
return imageUrls
|
|
.map((imageUrl) => {
|
|
const tag = mediaUpload.getImetaTagByUrl(imageUrl)
|
|
return tag ?? null
|
|
})
|
|
.filter(Boolean) as string[][]
|
|
}
|
|
|
|
async function extractRelatedEventIds(content: string, parentEvent?: Event) {
|
|
const quoteEventHexIds: string[] = []
|
|
const quoteReplaceableCoordinates: string[] = []
|
|
let rootETag: string[] = []
|
|
let parentETag: string[] = []
|
|
const matches = content.match(EMBEDDED_EVENT_REGEX)
|
|
|
|
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(quoteEventHexIds, data.id)
|
|
} else if (type === 'note') {
|
|
addToSet(quoteEventHexIds, data)
|
|
} else if (type === 'naddr') {
|
|
addToSet(
|
|
quoteReplaceableCoordinates,
|
|
getReplaceableCoordinate(data.kind, data.pubkey, data.identifier)
|
|
)
|
|
}
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
if (parentEvent) {
|
|
const _rootETag = getRootETag(parentEvent)
|
|
if (_rootETag) {
|
|
parentETag = buildETagWithMarker(parentEvent.id, parentEvent.pubkey, '', 'reply')
|
|
|
|
const [, rootEventHexId, hint, , rootEventPubkey] = _rootETag
|
|
if (rootEventPubkey) {
|
|
rootETag = buildETagWithMarker(rootEventHexId, rootEventPubkey, hint, 'root')
|
|
} else {
|
|
const rootEventId = generateBech32IdFromETag(_rootETag)
|
|
const rootEvent = rootEventId ? await client.fetchEvent(rootEventId) : undefined
|
|
rootETag = rootEvent
|
|
? buildETagWithMarker(rootEvent.id, rootEvent.pubkey, hint, 'root')
|
|
: buildETagWithMarker(rootEventHexId, rootEventPubkey, hint, 'root')
|
|
}
|
|
} else {
|
|
// reply to root event
|
|
rootETag = buildETagWithMarker(parentEvent.id, parentEvent.pubkey, '', 'root')
|
|
}
|
|
}
|
|
|
|
return {
|
|
quoteEventHexIds,
|
|
quoteReplaceableCoordinates,
|
|
rootETag,
|
|
parentETag
|
|
}
|
|
}
|
|
|
|
async function extractCommentMentions(content: string, parentEvent: Event) {
|
|
const quoteEventHexIds: string[] = []
|
|
const quoteReplaceableCoordinates: string[] = []
|
|
const isComment = [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(parentEvent.kind)
|
|
const rootCoordinateTag = isComment
|
|
? parentEvent.tags.find(tagNameEquals('A'))
|
|
: isReplaceableEvent(parentEvent.kind)
|
|
? buildATag(parentEvent, true)
|
|
: undefined
|
|
const rootEventId = isComment ? parentEvent.tags.find(tagNameEquals('E'))?.[1] : parentEvent.id
|
|
const rootKind = isComment ? parentEvent.tags.find(tagNameEquals('K'))?.[1] : parentEvent.kind
|
|
const rootPubkey = isComment ? parentEvent.tags.find(tagNameEquals('P'))?.[1] : parentEvent.pubkey
|
|
const rootUrl = isComment ? parentEvent.tags.find(tagNameEquals('I'))?.[1] : undefined
|
|
|
|
const addToSet = (arr: string[], item: string) => {
|
|
if (!arr.includes(item)) arr.push(item)
|
|
}
|
|
|
|
const matches = content.match(EMBEDDED_EVENT_REGEX)
|
|
for (const m of matches || []) {
|
|
try {
|
|
const id = m.split(':')[1]
|
|
const { type, data } = nip19.decode(id)
|
|
if (type === 'nevent') {
|
|
addToSet(quoteEventHexIds, data.id)
|
|
} else if (type === 'note') {
|
|
addToSet(quoteEventHexIds, data)
|
|
} else if (type === 'naddr') {
|
|
addToSet(
|
|
quoteReplaceableCoordinates,
|
|
getReplaceableCoordinate(data.kind, data.pubkey, data.identifier)
|
|
)
|
|
}
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
return {
|
|
quoteEventHexIds,
|
|
quoteReplaceableCoordinates,
|
|
rootEventId,
|
|
rootCoordinateTag,
|
|
rootKind,
|
|
rootPubkey,
|
|
rootUrl,
|
|
parentEvent
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
export function transformCustomEmojisInContent(content: string) {
|
|
const emojiTags: string[][] = []
|
|
let processedContent = content
|
|
const matches = content.match(/:[a-zA-Z0-9]+:/g)
|
|
|
|
const emojiIdSet = new Set<string>()
|
|
matches?.forEach((m) => {
|
|
if (emojiIdSet.has(m)) return
|
|
emojiIdSet.add(m)
|
|
|
|
const emoji = customEmojiService.getEmojiById(m.slice(1, -1))
|
|
if (emoji) {
|
|
emojiTags.push(buildEmojiTag(emoji))
|
|
processedContent = processedContent.replace(new RegExp(m, 'g'), `:${emoji.shortcode}:`)
|
|
}
|
|
})
|
|
|
|
return {
|
|
emojiTags,
|
|
content: processedContent
|
|
}
|
|
}
|
|
|
|
export function buildATag(event: Event, upperCase: boolean = false) {
|
|
const coordinate = getReplaceableCoordinateFromEvent(event)
|
|
const hint = client.getEventHint(event.id)
|
|
return trimTagEnd([upperCase ? 'A' : 'a', coordinate, hint])
|
|
}
|
|
|
|
function buildDTag(identifier: string) {
|
|
return ['d', identifier]
|
|
}
|
|
|
|
function buildETag(
|
|
eventHexId: string,
|
|
pubkey: string = '',
|
|
hint: string = '',
|
|
upperCase: boolean = false
|
|
) {
|
|
if (!hint) {
|
|
hint = client.getEventHint(eventHexId)
|
|
}
|
|
return trimTagEnd([upperCase ? 'E' : 'e', eventHexId, hint, pubkey])
|
|
}
|
|
|
|
function buildETagWithMarker(
|
|
eventHexId: string,
|
|
pubkey: string = '',
|
|
hint: string = '',
|
|
marker: 'root' | 'reply' | '' = ''
|
|
) {
|
|
if (!hint) {
|
|
hint = client.getEventHint(eventHexId)
|
|
}
|
|
return trimTagEnd(['e', eventHexId, hint, marker, pubkey])
|
|
}
|
|
|
|
function buildITag(url: string, upperCase: boolean = false) {
|
|
return [upperCase ? 'I' : 'i', url]
|
|
}
|
|
|
|
function buildKTag(kind: number | string, upperCase: boolean = false) {
|
|
return [upperCase ? 'K' : 'k', kind.toString()]
|
|
}
|
|
|
|
function buildPTag(pubkey: string, upperCase: boolean = false) {
|
|
return [upperCase ? 'P' : 'p', pubkey]
|
|
}
|
|
|
|
function buildQTag(eventHexId: string) {
|
|
return trimTagEnd(['q', eventHexId, client.getEventHint(eventHexId)]) // TODO: pubkey
|
|
}
|
|
|
|
function buildReplaceableQTag(coordinate: string) {
|
|
return trimTagEnd(['q', coordinate])
|
|
}
|
|
|
|
function buildRTag(url: string, scope: TMailboxRelayScope) {
|
|
return scope !== 'both' ? ['r', url, scope] : ['r', url]
|
|
}
|
|
|
|
function buildTTag(hashtag: string) {
|
|
return ['t', hashtag]
|
|
}
|
|
|
|
function buildEmojiTag(emoji: TEmoji) {
|
|
return ['emoji', emoji.shortcode, emoji.url]
|
|
}
|
|
|
|
function buildTitleTag(title: string) {
|
|
return ['title', title]
|
|
}
|
|
|
|
function buildRelayTag(url: string) {
|
|
return ['relay', url]
|
|
}
|
|
|
|
function buildServerTag(url: string) {
|
|
return ['server', url]
|
|
}
|
|
|
|
function buildResponseTag(value: string) {
|
|
return ['response', value]
|
|
}
|
|
|
|
function buildClientTag() {
|
|
return ['client', 'jumble']
|
|
}
|
|
|
|
function buildNsfwTag() {
|
|
return ['content-warning', 'NSFW']
|
|
}
|
|
|
|
function buildProtectedTag() {
|
|
return ['-']
|
|
}
|
|
|
|
function trimTagEnd(tag: string[]) {
|
|
let endIndex = tag.length - 1
|
|
while (endIndex >= 0 && tag[endIndex] === '') {
|
|
endIndex--
|
|
}
|
|
|
|
return tag.slice(0, endIndex + 1)
|
|
}
|