feat: polls (#451)
Co-authored-by: silberengel <silberengel7@protonmail.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
import { ApplicationDataKey, ExtendedKind } from '@/constants'
|
||||
import { ApplicationDataKey, ExtendedKind, POLL_TYPE } from '@/constants'
|
||||
import client from '@/services/client.service'
|
||||
import mediaUpload from '@/services/media-upload.service'
|
||||
import { TDraftEvent, TEmoji, TMailboxRelay, TMailboxRelayScope, TRelaySet } from '@/types'
|
||||
import {
|
||||
TDraftEvent,
|
||||
TEmoji,
|
||||
TMailboxRelay,
|
||||
TMailboxRelayScope,
|
||||
TPollCreateData,
|
||||
TRelaySet
|
||||
} from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, kinds, nip19 } from 'nostr-tools'
|
||||
import {
|
||||
@@ -10,6 +17,7 @@ import {
|
||||
isProtectedEvent,
|
||||
isReplaceableEvent
|
||||
} from './event'
|
||||
import { randomString } from './random'
|
||||
import { generateBech32IdFromETag, tagNameEquals } from './tag'
|
||||
|
||||
// https://github.com/nostr-protocol/nips/blob/master/25.md
|
||||
@@ -335,6 +343,93 @@ export function createBlossomServerListDraftEvent(servers: string[]): TDraftEven
|
||||
}
|
||||
}
|
||||
|
||||
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 { quoteEventIds } = await extractRelatedEventIds(question)
|
||||
const hashtags = extractHashtags(question)
|
||||
|
||||
const tags = hashtags.map((hashtag) => buildTTag(hashtag))
|
||||
|
||||
// imeta tags
|
||||
const images = extractImagesFromContent(question)
|
||||
if (images && images.length) {
|
||||
tags.push(...generateImetaTags(images))
|
||||
}
|
||||
|
||||
// q tags
|
||||
tags.push(...quoteEventIds.map((eventId) => buildQTag(eventId)))
|
||||
|
||||
// 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: question.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),
|
||||
...selectedOptionIds.map((optionId) => buildResponseTag(optionId))
|
||||
],
|
||||
created_at: dayjs().unix()
|
||||
}
|
||||
}
|
||||
|
||||
function generateImetaTags(imageUrls: string[]) {
|
||||
return imageUrls
|
||||
.map((imageUrl) => {
|
||||
@@ -545,6 +640,10 @@ function buildImetaTag(nip94Tags: string[][]) {
|
||||
return ['imeta', ...nip94Tags.map(([n, v]) => `${n} ${v}`)]
|
||||
}
|
||||
|
||||
function buildResponseTag(value: string) {
|
||||
return ['response', value]
|
||||
}
|
||||
|
||||
function buildClientTag() {
|
||||
return ['client', 'jumble']
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import { TRelayList, TRelaySet } from '@/types'
|
||||
import { BIG_RELAY_URLS, POLL_TYPE } from '@/constants'
|
||||
import { TPollType, TRelayList, TRelaySet } from '@/types'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { getReplaceableEventIdentifier } from './event'
|
||||
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
|
||||
@@ -262,3 +262,76 @@ export function getCommunityDefinitionFromEvent(event: Event) {
|
||||
|
||||
return { name, description, image }
|
||||
}
|
||||
|
||||
export function getPollMetadataFromEvent(event: Event) {
|
||||
const options: { id: string; label: string }[] = []
|
||||
const relayUrls: string[] = []
|
||||
let pollType: TPollType = POLL_TYPE.SINGLE_CHOICE
|
||||
let endsAt: number | undefined
|
||||
|
||||
for (const [tagName, ...tagValues] of event.tags) {
|
||||
if (tagName === 'option' && tagValues.length >= 2) {
|
||||
const [optionId, label] = tagValues
|
||||
if (optionId && label) {
|
||||
options.push({ id: optionId, label })
|
||||
}
|
||||
} else if (tagName === 'relay' && tagValues[0]) {
|
||||
const normalizedUrl = normalizeUrl(tagValues[0])
|
||||
if (normalizedUrl) relayUrls.push(tagValues[0])
|
||||
} else if (tagName === 'polltype' && tagValues[0]) {
|
||||
if (tagValues[0] === POLL_TYPE.MULTIPLE_CHOICE) {
|
||||
pollType = POLL_TYPE.MULTIPLE_CHOICE
|
||||
}
|
||||
} else if (tagName === 'endsAt' && tagValues[0]) {
|
||||
const timestamp = parseInt(tagValues[0])
|
||||
if (!isNaN(timestamp)) {
|
||||
endsAt = timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
options,
|
||||
pollType,
|
||||
relayUrls,
|
||||
endsAt
|
||||
}
|
||||
}
|
||||
|
||||
export function getPollResponseFromEvent(
|
||||
event: Event,
|
||||
optionIds: string[],
|
||||
isMultipleChoice: boolean
|
||||
) {
|
||||
const selectedOptionIds: string[] = []
|
||||
|
||||
for (const [tagName, ...tagValues] of event.tags) {
|
||||
if (tagName === 'response' && tagValues[0]) {
|
||||
if (optionIds && !optionIds.includes(tagValues[0])) {
|
||||
continue // Skip if the response is not in the provided optionIds
|
||||
}
|
||||
selectedOptionIds.push(tagValues[0])
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid responses are found, return null
|
||||
if (selectedOptionIds.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If multiple responses are selected but the poll is not multiple choice, return null
|
||||
if (selectedOptionIds.length > 1 && !isMultipleChoice) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
selectedOptionIds,
|
||||
created_at: event.created_at
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user